diff --git a/app.json b/app.json
index 0ac3df179..c3fd8bf56 100644
--- a/app.json
+++ b/app.json
@@ -1,11 +1,11 @@
{
"expo": {
- "version": "2.0.9",
+ "version": "2.1.0",
"ios": {
- "buildNumber": "36"
+ "buildNumber": "40"
},
"android": {
- "versionCode": 234
+ "versionCode": 239
}
}
}
diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx
deleted file mode 100644
index 58e0d30ca..000000000
--- a/components/Chat/Chat.tsx
+++ /dev/null
@@ -1,638 +0,0 @@
-// import { useSelect } from "@data/store/storeHelpers";
-// import { FlashList } from "@shopify/flash-list";
-// import { itemSeparatorColor, tertiaryBackgroundColor } from "@styles/colors";
-// import { useAppTheme } from "@theme/useAppTheme";
-// import { getCleanAddress } from "@utils/evm/address";
-// import { FrameWithType, messageHasFrames } from "@utils/frames";
-// import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
-// import React, { useCallback, useEffect, useMemo, useRef } from "react";
-// import {
-// ColorSchemeName,
-// Dimensions,
-// FlatList,
-// Keyboard,
-// Platform,
-// StyleSheet,
-// View,
-// useColorScheme,
-// } from "react-native";
-// import Animated, {
-// useAnimatedStyle,
-// useDerivedValue,
-// useSharedValue,
-// } from "react-native-reanimated";
-// import { useSafeAreaInsets } from "react-native-safe-area-context";
-// import { useShallow } from "zustand/react/shallow";
-
-// import {
-// useChatStore,
-// useCurrentAccount,
-// useProfilesStore,
-// useRecommendationsStore,
-// } from "../../data/store/accountsStore";
-// import { XmtpConversationWithUpdate } from "../../data/store/chatStore";
-// import { useFramesStore } from "../../data/store/framesStore";
-// import { ExternalWalletPicker } from "../../features/ExternalWalletPicker/ExternalWalletPicker";
-// import { ExternalWalletPickerContextProvider } from "../../features/ExternalWalletPicker/ExternalWalletPicker.context";
-// import {
-// ReanimatedFlashList,
-// ReanimatedFlatList,
-// ReanimatedView,
-// } from "../../utils/animations";
-// import { useKeyboardAnimation } from "../../utils/animations/keyboardAnimation";
-// import { isAttachmentMessage } from "@utils/attachment/isAttachmentMessage";
-// import { useConversationContext } from "../../utils/conversation";
-// import { converseEventEmitter } from "../../utils/events";
-// import { getProfile, getProfileData } from "../../utils/profile";
-// import { UUID_REGEX } from "../../utils/regex";
-// import { isContentType } from "../../utils/xmtpRN/contentTypes";
-// import { Recommendation } from "../Recommendations/Recommendation";
-// import ChatPlaceholder from "./ChatPlaceholder/ChatPlaceholder";
-// import ConsentPopup from "./ConsentPopup/ConsentPopup";
-// import { GroupConsentPopup } from "./ConsentPopup/GroupConsentPopup";
-// import ChatInput from "./Input/Input";
-// import CachedChatMessage, { MessageToDisplay } from "./Message/Message";
-// import { useMessageReactionsStore } from "./Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store";
-// import { MessageReactionsDrawer } from "./Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer";
-
-// const usePeerSocials = () => {
-// const conversation = useConversationContext("conversation");
-// const peerSocials = useProfilesStore(
-// useShallow((s) =>
-// conversation?.peerAddress
-// ? getProfile(conversation.peerAddress, s.profiles)?.socials
-// : undefined
-// )
-// );
-
-// return peerSocials;
-// };
-
-// const useRenderItem = ({
-// xmtpAddress,
-// conversation,
-// messageFramesMap,
-// colorScheme,
-// }: {
-// xmtpAddress: string;
-// conversation: XmtpConversationWithUpdate | undefined;
-// messageFramesMap: {
-// [messageId: string]: FrameWithType[];
-// };
-// colorScheme: ColorSchemeName;
-// }) => {
-// return useCallback(
-// ({ item }: { item: MessageToDisplay }) => {
-// return (
-//
-// );
-// },
-// [colorScheme, xmtpAddress, conversation?.isGroup, messageFramesMap]
-// );
-// };
-
-// const getItemType = (item: MessageToDisplay) => {
-// const fromMeString = item.fromMe ? "fromMe" : "notFromMe";
-// return `${item.contentType}-${fromMeString}`;
-// };
-
-// const getListArray = (
-// xmtpAddress?: string,
-// conversation?: XmtpConversationWithUpdate,
-// lastMessages?: number // Optional parameter to limit the number of messages
-// ) => {
-// const messageAttachments = useChatStore.getState().messageAttachments;
-
-// const isAttachmentLoading = (messageId: string) => {
-// const attachment = messageAttachments && messageAttachments[messageId];
-// return attachment?.loading;
-// };
-
-// if (!conversation) return [];
-// const reverseArray = [];
-// // Filter out unwanted content types before list or reactions out of order can mess up the logic
-// const filteredMessageIds = conversation.messagesIds.filter((messageId) => {
-// const message = conversation.messages.get(messageId) as MessageToDisplay;
-// if (!message || (!message.content && !message.contentFallback))
-// return false;
-
-// // Reactions & read receipts are not displayed in the flow
-// const notDisplayedContentTypes = [
-// "xmtp.org/reaction:",
-// "xmtp.org/readReceipt:",
-// ];
-
-// if (isAttachmentMessage(message.contentType) && UUID_REGEX.test(message.id))
-// return false;
-
-// return !notDisplayedContentTypes.some((c) =>
-// message.contentType.startsWith(c)
-// );
-// });
-
-// let latestSettledFromMeIndex = -1;
-// let latestSettledFromPeerIndex = -1;
-
-// for (let index = filteredMessageIds.length - 1; index >= 0; index--) {
-// const messageId = filteredMessageIds[index];
-// const message = conversation.messages.get(messageId) as MessageToDisplay;
-
-// message.fromMe =
-// !!xmtpAddress &&
-// xmtpAddress.toLowerCase() === message.senderAddress.toLowerCase();
-
-// message.hasNextMessageInSeries = false;
-// message.hasPreviousMessageInSeries = false;
-
-// if (index > 0) {
-// const previousMessageId = filteredMessageIds[index - 1];
-// const previousMessage = conversation.messages.get(previousMessageId);
-
-// if (previousMessage) {
-// message.dateChange =
-// differenceInCalendarDays(message.sent, previousMessage.sent) > 0;
-
-// if (
-// previousMessage.senderAddress.toLowerCase() ===
-// message.senderAddress.toLowerCase() &&
-// !message.dateChange &&
-// !isContentType("groupUpdated", previousMessage.contentType)
-// ) {
-// message.hasPreviousMessageInSeries = true;
-// }
-// }
-// } else {
-// message.dateChange = true;
-// }
-
-// if (index < filteredMessageIds.length - 1) {
-// const nextMessageId = filteredMessageIds[index + 1];
-// const nextMessage = conversation.messages.get(nextMessageId);
-
-// if (nextMessage) {
-// // Here we need to check if next message has a date change
-// const nextMessageDateChange =
-// differenceInCalendarDays(nextMessage.sent, message.sent) > 0;
-
-// if (
-// nextMessage.senderAddress.toLowerCase() ===
-// message.senderAddress.toLowerCase() &&
-// !nextMessageDateChange &&
-// !isContentType("groupUpdated", nextMessage.contentType)
-// ) {
-// message.hasNextMessageInSeries = true;
-// }
-// }
-// }
-
-// if (
-// message.fromMe &&
-// message.status !== "sending" &&
-// message.status !== "prepared" &&
-// latestSettledFromMeIndex === -1
-// ) {
-// latestSettledFromMeIndex = reverseArray.length;
-// }
-
-// if (!message.fromMe && latestSettledFromPeerIndex === -1) {
-// latestSettledFromPeerIndex = reverseArray.length;
-// }
-
-// message.isLatestSettledFromMe =
-// reverseArray.length === latestSettledFromMeIndex;
-// message.isLatestSettledFromPeer =
-// reverseArray.length === latestSettledFromPeerIndex;
-
-// if (index === filteredMessageIds.length - 1) {
-// message.isLoadingAttachment =
-// isAttachmentMessage(message.contentType) &&
-// isAttachmentLoading(message.id);
-// }
-
-// if (index === filteredMessageIds.length - 2) {
-// const nextMessageId = filteredMessageIds[index + 1];
-// const nextMessage = conversation.messages.get(nextMessageId);
-// message.nextMessageIsLoadingAttachment =
-// isAttachmentMessage(nextMessage?.contentType) &&
-// isAttachmentLoading(nextMessageId);
-// }
-
-// reverseArray.push(message);
-// }
-
-// // If lastMessages is defined, slice the array to return only the last n messages
-// if (lastMessages !== undefined) {
-// return reverseArray.slice(0, lastMessages);
-// }
-
-// return reverseArray;
-// };
-
-// const useAnimatedListView = (
-// conversation: XmtpConversationWithUpdate | undefined
-// ) => {
-// // The first message was really buggy on iOS & Android and this is due to FlashList
-// // so we keep FlatList for new convos and switch to FlashList for bigger convos
-// // that need great perf.
-// return useMemo(() => {
-// const isConversationNotPending = conversation && !conversation.pending;
-// return isConversationNotPending ? ReanimatedFlashList : ReanimatedFlatList;
-// }, [conversation]);
-// };
-
-// const useIsShowingPlaceholder = ({
-// messages,
-// isBlockedPeer,
-// conversation,
-// }: {
-// messages: MessageToDisplay[];
-// isBlockedPeer: boolean;
-// conversation: XmtpConversationWithUpdate | undefined;
-// }): boolean => {
-// return messages.length === 0 || isBlockedPeer || !conversation;
-// };
-
-// const keyExtractor = (item: MessageToDisplay) => item.id;
-
-// export function Chat() {
-// const conversation = useConversationContext("conversation");
-// const AnimatedListView = useAnimatedListView(conversation);
-// const isBlockedPeer = useConversationContext("isBlockedPeer");
-// const onReadyToFocus = useConversationContext("onReadyToFocus");
-// const frameTextInputFocused = useConversationContext("frameTextInputFocused");
-// const rolledUpReactions =
-// useMessageReactionsStore.getState().rolledUpReactions;
-
-// const xmtpAddress = useCurrentAccount() as string;
-// const peerSocials = usePeerSocials();
-// const recommendationsData = useRecommendationsStore(
-// useShallow((s) =>
-// conversation?.peerAddress ? s.frens[conversation.peerAddress] : undefined
-// )
-// );
-
-// const colorScheme = useColorScheme();
-// const styles = useStyles();
-
-// const messageAttachmentsLength = useChatStore(
-// useShallow((s) => Object.keys(s.messageAttachments).length)
-// );
-
-// const listArray = useMemo(
-// () => getListArray(xmtpAddress, conversation),
-// // eslint-disable-next-line react-hooks/exhaustive-deps
-// [
-// xmtpAddress,
-// conversation,
-// conversation?.lastUpdateAt,
-// messageAttachmentsLength,
-// ]
-// );
-
-// const DEFAULT_INPUT_HEIGHT = 58;
-// const chatInputHeight = useSharedValue(0);
-// const chatInputDisplayedHeight = useDerivedValue(() => {
-// return frameTextInputFocused
-// ? 0
-// : chatInputHeight.value + DEFAULT_INPUT_HEIGHT;
-// });
-
-// const insets = useSafeAreaInsets();
-
-// const { height: keyboardHeight } = useKeyboardAnimation();
-// const tertiary = tertiaryBackgroundColor(colorScheme);
-
-// const showChatInput = !!(
-// conversation &&
-// !isBlockedPeer &&
-// (!conversation.isGroup ||
-// conversation.groupMembers.includes(getCleanAddress(xmtpAddress)))
-// );
-
-// const textInputStyle = useAnimatedStyle(
-// () => ({
-// position: "absolute",
-// width: "100%",
-// backgroundColor: tertiary,
-// height: "auto",
-// zIndex: 1,
-// transform: [
-// { translateY: -Math.max(insets.bottom, keyboardHeight.value) },
-// ] as any,
-// }),
-// [keyboardHeight, insets.bottom]
-// );
-
-// useEffect(() => {
-// const unsubscribe = useMessageReactionsStore.subscribe((state) => {
-// if (state.rolledUpReactions) {
-// Keyboard.dismiss();
-// }
-// });
-// return unsubscribe;
-// }, [rolledUpReactions]);
-
-// const chatContentStyle = useAnimatedStyle(
-// () => ({
-// ...styles.chatContent,
-// paddingBottom: showChatInput
-// ? chatInputDisplayedHeight.value +
-// Math.max(insets.bottom, keyboardHeight.value)
-// : insets.bottom,
-// }),
-// [showChatInput, keyboardHeight, chatInputDisplayedHeight, insets.bottom]
-// );
-
-// const ListFooterComponent = useMemo(() => {
-// const recommendationData = getProfileData(recommendationsData, peerSocials);
-// if (!recommendationData || !conversation?.peerAddress) return null;
-// return (
-//
-//
-//
-// );
-// }, [
-// conversation?.peerAddress,
-// peerSocials,
-// recommendationsData,
-// styles.inChatRecommendations,
-// ]);
-
-// const { messageFramesMap, frames: framesStore } = useFramesStore(
-// useSelect(["messageFramesMap", "frames"])
-// );
-
-// const showPlaceholder = useIsShowingPlaceholder({
-// messages: listArray,
-// isBlockedPeer,
-// conversation,
-// });
-
-// const renderItem = useRenderItem({
-// xmtpAddress,
-// conversation,
-// messageFramesMap,
-// colorScheme,
-// });
-
-// const messageListRef = useRef<
-// FlatList | FlashList | undefined
-// >();
-
-// const scrollToMessage = useCallback(
-// (data: { messageId?: string; index?: number; animated?: boolean }) => {
-// let index = data.index;
-
-// if (index === undefined && data.messageId) {
-// index = listArray.findIndex((m) => m.id === data.messageId);
-// }
-
-// if (index !== undefined) {
-// messageListRef.current?.scrollToIndex({
-// index,
-// viewPosition: 0.5,
-// animated: !!data.animated,
-// });
-// }
-// },
-// [listArray]
-// );
-
-// useEffect(() => {
-// converseEventEmitter.on("scrollChatToMessage", scrollToMessage);
-
-// return () => {
-// converseEventEmitter.off("scrollChatToMessage", scrollToMessage);
-// };
-// }, [scrollToMessage]);
-
-// const handleOnLayout = useCallback(() => {
-// setTimeout(() => {
-// onReadyToFocus();
-// }, 50);
-// }, [onReadyToFocus]);
-
-// return (
-//
-//
-//
-// {conversation && listArray.length > 0 && !isBlockedPeer && (
-// {
-// if (r) {
-// messageListRef.current = r;
-// }
-// }}
-// keyboardDismissMode="interactive"
-// automaticallyAdjustContentInsets={false}
-// contentInsetAdjustmentBehavior="never"
-// // Causes a glitch on Android, no sure we need it for now
-// // maintainVisibleContentPosition={{
-// // minIndexForVisible: 0,
-// // autoscrollToTopThreshold: 100,
-// // }}
-// estimatedListSize={Dimensions.get("screen")}
-// inverted
-// keyExtractor={keyExtractor}
-// getItemType={getItemType}
-// keyboardShouldPersistTaps="handled"
-// estimatedItemSize={80}
-// // Size glitch on Android
-// showsVerticalScrollIndicator={Platform.OS === "ios"}
-// pointerEvents="auto"
-// ListFooterComponent={ListFooterComponent}
-// />
-// )}
-// {conversation?.isGroup ? : }
-//
-// {showChatInput && (
-// <>
-//
-//
-//
-//
-// >
-// )}
-//
-//
-//
-//
-// );
-// }
-
-// // Lightweight chat preview component used for longpress on chat
-// export function ChatPreview() {
-// const conversation = useConversationContext("conversation");
-// const AnimatedListView = useAnimatedListView(conversation);
-// const isBlockedPeer = useConversationContext("isBlockedPeer");
-// const onReadyToFocus = useConversationContext("onReadyToFocus");
-
-// const xmtpAddress = useCurrentAccount() as string;
-// const peerSocials = usePeerSocials();
-
-// const colorScheme = useColorScheme();
-// const styles = useStyles();
-// const messageAttachmentsLength = useChatStore(
-// useShallow((s) => Object.keys(s.messageAttachments).length)
-// );
-
-// const listArray = useMemo(
-// // Get only the last 20 messages for performance in preview
-// () => getListArray(xmtpAddress, conversation, 20),
-// // eslint-disable-next-line react-hooks/exhaustive-deps
-// [
-// xmtpAddress,
-// conversation,
-// conversation?.lastUpdateAt,
-// messageAttachmentsLength,
-// ]
-// );
-
-// const { frames: framesStore, messageFramesMap } = useFramesStore(
-// useSelect(["frames", "messageFramesMap"])
-// );
-
-// const showPlaceholder = useIsShowingPlaceholder({
-// messages: listArray,
-// isBlockedPeer,
-// conversation,
-// });
-
-// const renderItem = useRenderItem({
-// xmtpAddress,
-// conversation,
-// messageFramesMap,
-// colorScheme,
-// });
-
-// const keyExtractor = useCallback((item: MessageToDisplay) => item.id, []);
-
-// const messageListRef = useRef<
-// FlatList | FlashList | undefined
-// >();
-
-// const handleOnLayout = useCallback(() => {
-// setTimeout(() => {
-// onReadyToFocus();
-// }, 50);
-// }, [onReadyToFocus]);
-
-// return (
-//
-//
-// {conversation && listArray.length > 0 && !isBlockedPeer && (
-// {
-// if (r) {
-// messageListRef.current = r;
-// }
-// }}
-// keyboardDismissMode="interactive"
-// automaticallyAdjustContentInsets={false}
-// contentInsetAdjustmentBehavior="never"
-// estimatedListSize={Dimensions.get("screen")}
-// inverted
-// keyExtractor={keyExtractor}
-// getItemType={getItemType}
-// keyboardShouldPersistTaps="handled"
-// estimatedItemSize={80}
-// showsVerticalScrollIndicator={false}
-// pointerEvents="none"
-// />
-// )}
-// {showPlaceholder && !conversation?.isGroup && (
-//
-// )}
-//
-//
-// );
-// }
-
-// const useStyles = () => {
-// const colorScheme = useColorScheme();
-// const { theme } = useAppTheme();
-
-// return useMemo(
-// () =>
-// StyleSheet.create({
-// chatContainer: {
-// flex: 1,
-// justifyContent: "flex-end",
-// backgroundColor: theme.colors.background.surface,
-// },
-// chatContent: {
-// backgroundColor: theme.colors.background.surface,
-// flex: 1,
-// },
-// chatPreviewContent: {
-// backgroundColor: theme.colors.background.surface,
-// flex: 1,
-// paddingBottom: 0,
-// },
-// chat: {
-// backgroundColor: theme.colors.background.surface,
-// },
-// inputBottomFiller: {
-// position: "absolute",
-// width: "100%",
-// bottom: 0,
-// backgroundColor: theme.colors.background.surface,
-// zIndex: 0,
-// },
-// inChatRecommendations: {
-// borderBottomWidth: 0.5,
-// borderBottomColor: itemSeparatorColor(colorScheme),
-// marginHorizontal: 20,
-// marginBottom: 10,
-// },
-// }),
-// [colorScheme, theme]
-// );
-// };
diff --git a/components/Chat/ChatDumb.tsx b/components/Chat/ChatDumb.tsx
deleted file mode 100644
index c96644195..000000000
--- a/components/Chat/ChatDumb.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-import { FlashList, ListRenderItem } from "@shopify/flash-list";
-import {
- backgroundColor,
- itemSeparatorColor,
- tertiaryBackgroundColor,
-} from "@styles/colors";
-import React, { useCallback, useEffect, useMemo, useRef } from "react";
-import {
- FlatList,
- Platform,
- StyleSheet,
- View,
- useColorScheme,
-} from "react-native";
-import Animated, {
- useAnimatedStyle,
- useDerivedValue,
- useSharedValue,
-} from "react-native-reanimated";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { RemoteAttachmentContent } from "@xmtp/react-native-sdk";
-import { ReanimatedFlashList } from "../../utils/animations";
-import { useKeyboardAnimation } from "../../utils/animations/keyboardAnimation";
-import { converseEventEmitter } from "../../utils/events";
-
-type ChatDumbProps = {
- onReadyToFocus: () => void;
- frameTextInputFocused: boolean;
- items: T[];
- renderItem: ListRenderItem;
- keyExtractor: (item: T) => string;
- showChatInput: boolean;
- ListFooterComponent: React.JSX.Element | null;
- showPlaceholder: boolean;
- key?: string;
- displayList: boolean;
- refreshing: boolean;
- getItemType: (
- item: T,
- index: number,
- extraData?: any
- ) => string | number | undefined;
- placeholderComponent: React.JSX.Element | null;
- extraData?: any;
- itemToId: (id: T) => string;
- onSend: (payload: {
- text?: string;
- referencedMessageId?: string;
- attachment?: RemoteAttachmentContent;
- }) => Promise;
-};
-
-const useStyles = () => {
- const colorScheme = useColorScheme();
- return useMemo(
- () =>
- StyleSheet.create({
- chatContainer: {
- flex: 1,
- justifyContent: "flex-end",
- backgroundColor: backgroundColor(colorScheme),
- },
- chatContent: {
- backgroundColor: backgroundColor(colorScheme),
- flex: 1,
- },
- chatPreviewContent: {
- backgroundColor: backgroundColor(colorScheme),
- flex: 1,
- paddingBottom: 0,
- },
- chat: {
- backgroundColor: backgroundColor(colorScheme),
- },
- inputBottomFiller: {
- position: "absolute",
- width: "100%",
- bottom: 0,
- backgroundColor: backgroundColor(colorScheme),
- zIndex: 0,
- },
- inChatRecommendations: {
- borderBottomWidth: 0.5,
- borderBottomColor: itemSeparatorColor(colorScheme),
- marginHorizontal: 20,
- marginBottom: 10,
- },
- }),
- [colorScheme]
- );
-};
-
-export function ChatDumb({
- onReadyToFocus,
- frameTextInputFocused,
- items,
- renderItem,
- keyExtractor,
- showChatInput,
- showPlaceholder,
- key,
- displayList,
- refreshing,
- ListFooterComponent,
- getItemType,
- placeholderComponent,
- extraData,
- itemToId,
- onSend,
-}: ChatDumbProps) {
- const colorScheme = useColorScheme();
- const styles = useStyles();
-
- const hideInputIfFrameFocused = Platform.OS !== "web";
-
- const DEFAULT_INPUT_HEIGHT = 58;
- const chatInputHeight = useSharedValue(0);
- const chatInputDisplayedHeight = useDerivedValue(() => {
- return frameTextInputFocused && hideInputIfFrameFocused
- ? 0
- : chatInputHeight.value + DEFAULT_INPUT_HEIGHT;
- });
-
- const insets = useSafeAreaInsets();
-
- const { height: keyboardHeight } = useKeyboardAnimation();
- const tertiary = tertiaryBackgroundColor(colorScheme);
-
- const textInputStyle = useAnimatedStyle(
- () => ({
- position: "absolute",
- width: "100%",
- backgroundColor: tertiary,
- height: "auto",
- zIndex: 1,
- transform: [
- { translateY: -Math.max(insets.bottom, keyboardHeight.value) },
- ] as any,
- }),
- [keyboardHeight, colorScheme, insets.bottom]
- );
-
- const chatContentStyle = useAnimatedStyle(
- () => ({
- ...styles.chatContent,
- paddingBottom: showChatInput
- ? chatInputDisplayedHeight.value +
- Math.max(insets.bottom, keyboardHeight.value)
- : insets.bottom,
- }),
- [showChatInput, keyboardHeight, chatInputDisplayedHeight, insets.bottom]
- );
-
- const messageListRef = useRef | FlashList | undefined>();
-
- const scrollToMessage = useCallback(
- (data: { messageId?: string; index?: number; animated?: boolean }) => {
- let index = data.index;
- if (index === undefined && data.messageId) {
- index = items.findIndex((m) => itemToId(m) === data.messageId);
- }
- if (index !== undefined) {
- messageListRef.current?.scrollToIndex({
- index,
- viewPosition: 0.5,
- animated: !!data.animated,
- });
- }
- },
- [itemToId, items]
- );
-
- useEffect(() => {
- converseEventEmitter.on("scrollChatToMessage", scrollToMessage);
- return () => {
- converseEventEmitter.off("scrollChatToMessage", scrollToMessage);
- };
- }, [scrollToMessage]);
-
- const handleOnLayout = useCallback(() => {
- setTimeout(() => {
- onReadyToFocus();
- }, 50);
- }, [onReadyToFocus]);
-
- return (
-
-
- {displayList && (
-
- )}
- {showPlaceholder && placeholderComponent}
-
- {/* {showChatInput && (
- <>
-
-
-
-
- >
- )} */}
-
- );
-}
diff --git a/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx b/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx
deleted file mode 100644
index 0849e2416..000000000
--- a/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-// import { Button } from "@design-system/Button/Button";
-// import { translate } from "@i18n";
-// import { actionSheetColors, textPrimaryColor } from "@styles/colors";
-// import { isV3Topic } from "@utils/groupUtils/groupId";
-// import {
-// Keyboard,
-// Platform,
-// StyleSheet,
-// Text,
-// TouchableWithoutFeedback,
-// useColorScheme,
-// View,
-// } from "react-native";
-
-// import {
-// currentAccount,
-// useProfilesStore,
-// useRecommendationsStore,
-// useSettingsStore,
-// } from "../../../data/store/accountsStore";
-// import { useConversationContext } from "../../../utils/conversation";
-// import { sendMessage } from "../../../utils/message";
-// import { getProfile, getProfileData } from "../../../utils/profile";
-// import { conversationName } from "../../../utils/str";
-// import ActivityIndicator from "../../ActivityIndicator/ActivityIndicator";
-// import { Recommendation } from "../../Recommendations/Recommendation";
-// import { showActionSheetWithOptions } from "../../StateHandlers/ActionSheetStateHandler";
-// import { consentToAddressesOnProtocolByAccount } from "@utils/xmtpRN/contacts";
-// import { DmWithCodecsType } from "@utils/xmtpRN/client";
-// import { useInboxProfileSocials } from "@hooks/useInboxProfileSocials";
-
-// type Props = {
-// messagesCount: number;
-// dm: DmWithCodecsType | undefined | null;
-// };
-
-// export function DmChatPlaceholder({ messagesCount, dm }: Props) {
-// const topic = useConversationContext("topic");
-// const onReadyToFocus = useConversationContext("onReadyToFocus");
-// const colorScheme = useColorScheme();
-// const styles = useStyles();
-// const { peerAddress } = useInboxProfileSocials(dm?.);
-// const peerSocials = useProfilesStore((s) =>
-// dm?.peerAddress
-// ? getProfile(conversation.peerAddress, s.profiles)?.socials
-// : undefined
-// );
-// const profileData = getProfileData(recommendationData, peerSocials);
-// return (
-// {
-// Keyboard.dismiss();
-// }}
-// >
-// {
-// if (conversation && !isBlockedPeer && messagesCount === 0) {
-// onReadyToFocus();
-// }
-// }}
-// style={styles.chatPlaceholder}
-// >
-// {!conversation && (
-//
-// {!topic && }
-//
-// {topic
-// ? isV3Topic(topic)
-// ? translate("group_not_found")
-// : translate("conversation_not_found")
-// : translate("opening_conversation")}
-//
-//
-// )}
-// {conversation && isBlockedPeer && (
-//
-// This user is blocked
-//
-// )}
-// {conversation && !isBlockedPeer && messagesCount === 0 && (
-//
-// {profileData && !conversation.isGroup ? (
-//
-// ) : (
-//
-// This is the beginning of your{"\n"}conversation with{" "}
-// {conversation ? conversationName(conversation) : ""}
-//
-// )}
-
-//
-// )}
-//
-//
-// );
-// }
-
-// const useStyles = () => {
-// const colorScheme = useColorScheme();
-// return StyleSheet.create({
-// chatPlaceholder: {
-// flex: 1,
-// justifyContent: "center",
-// },
-// chatPlaceholderContent: {
-// paddingVertical: 20,
-// flex: 1,
-// },
-// chatPlaceholderText: {
-// textAlign: "center",
-// fontSize: Platform.OS === "android" ? 16 : 17,
-// color: textPrimaryColor(colorScheme),
-// paddingHorizontal: 30,
-// },
-// cta: {
-// alignSelf: "center",
-// marginTop: 20,
-// },
-// });
-// };
diff --git a/components/Chat/Message/Message.tsx b/components/Chat/Message/Message.tsx
deleted file mode 100644
index f93b4aca3..000000000
--- a/components/Chat/Message/Message.tsx
+++ /dev/null
@@ -1,639 +0,0 @@
-// import { useFramesStore } from "@data/store/framesStore";
-// import {
-// inversePrimaryColor,
-// messageInnerBubbleColor,
-// myMessageInnerBubbleColor,
-// textPrimaryColor,
-// textSecondaryColor,
-// } from "@styles/colors";
-// import { useAppTheme } from "@theme/useAppTheme";
-// import { isFrameMessage } from "@utils/frames";
-// import * as Haptics from "expo-haptics";
-// import React, { ReactNode, useCallback, useMemo, useRef } from "react";
-// import {
-// Animated as RNAnimated,
-// ColorSchemeName,
-// Linking,
-// Platform,
-// StyleSheet,
-// Text,
-// TouchableOpacity,
-// View,
-// useColorScheme,
-// DimensionValue,
-// } from "react-native";
-// import { Swipeable } from "react-native-gesture-handler";
-// import Animated, {
-// useSharedValue,
-// useAnimatedStyle,
-// withTiming,
-// } from "react-native-reanimated";
-// import { useShallow } from "zustand/react/shallow";
-
-import { XmtpMessage } from "@data/store/chatStore";
-
-// import ChatMessageActions from "./MessageActions";
-// import { ChatMessageReactions } from "./MessageReactions/MessageReactions";
-// import { MessageSender } from "./MessageSender";
-// import { MessageSenderAvatar } from "./MessageSenderAvatar";
-// import MessageStatus from "./MessageStatus";
-// import { TextMessage } from "./TextMessage";
-// import {
-// currentAccount,
-// useChatStore,
-// } from "../../../data/store/accountsStore";
-// import { isAttachmentMessage } from "@utils/attachment/isAttachmentMessage";
-// import { getLocalizedTime, getRelativeDate } from "../../../utils/date";
-// import { converseEventEmitter } from "../../../utils/events";
-// import {
-// getUrlToRender,
-// isAllEmojisAndMaxThree,
-// } from "../../../utils/messageContent";
-// import { LimitedMap } from "../../../utils/objects";
-// import { getMessageReactions } from "../../../utils/reactions";
-// import { getReadableProfile } from "../../../utils/str";
-// import {
-// getMessageContentType,
-// isContentType,
-// } from "../../../utils/xmtpRN/contentTypes";
-// import ActionButton from "../ActionButton";
-// import AttachmentMessagePreview from "../Attachment/AttachmentMessagePreview";
-// import { ChatGroupUpdatedMessage } from "../ChatGroupUpdatedMessage";
-// import { FramesPreviews } from "../Frame/FramesPreviews";
-// import ChatInputReplyBubble from "../Input/InputReplyBubble";
-// // import TransactionPreview from "../Transaction/TransactionPreview";
-// import { GroupUpdatedContent } from "@xmtp/react-native-sdk";
-
-/**
- * @deprecated Use DecodedMessageWithCodecsType instead
- */
-export type MessageToDisplay = XmtpMessage & {
- hasPreviousMessageInSeries: boolean;
- hasNextMessageInSeries: boolean;
- dateChange: boolean;
- fromMe: boolean;
- isLatestSettledFromMe: boolean;
- isLatestSettledFromPeer: boolean;
- isLoadingAttachment: boolean | undefined;
- nextMessageIsLoadingAttachment: boolean | undefined;
-};
-
-// type Props = {
-// account: string;
-// message: MessageToDisplay;
-// colorScheme: ColorSchemeName;
-// isGroup: boolean;
-// hasFrames: boolean;
-// };
-
-// // On iOS, the native context menu view handles the long press, but could potentially trigger the onPress event
-// // So we have to set a noop on long press on iOS so it doens't also trigger the onPress event
-// const platformTouchableLongPressDelay = Platform.select({
-// ios: 100,
-// default: undefined,
-// });
-
-// const noop = () => {};
-
-// const platformTouchableOnLongPress = Platform.select({
-// ios: noop,
-// default: undefined,
-// });
-
-// const ChatMessage = ({
-// account,
-// message,
-// colorScheme,
-// isGroup,
-// hasFrames,
-// }: Props) => {
-// const styles = useStyles();
-
-// const messageDate = useMemo(
-// () => getRelativeDate(message.sent),
-// [message.sent]
-// );
-// const messageTime = useMemo(
-// () => getLocalizedTime(message.sent),
-// [message.sent]
-// );
-// // The content is completely a frame so a larger full width frame will be shown
-// const isFrame = useFramesStore(
-// useShallow((s) =>
-// isFrameMessage(
-// isContentType("text", message.contentType),
-// message.content,
-// s.frames
-// )
-// )
-// );
-
-// // Reanimated shared values for time and date-time animations
-// const timeHeight = useSharedValue(0);
-// const timeTranslateY = useSharedValue(20);
-// const timeOpacity = useSharedValue(0);
-// const timeAnimatedStyle = useAnimatedStyle(() => ({
-// height: timeHeight.value,
-// overflow: "hidden",
-// width: "100%",
-// transform: [{ translateY: timeTranslateY.value }],
-// opacity: timeOpacity.value,
-// }));
-
-// const dateTimeDisplay = useSharedValue<"none" | "flex">("none");
-// const dateTimeAnimatedStyle = useAnimatedStyle(() => ({
-// display: dateTimeDisplay.value,
-// }));
-
-// // Handle showTime animation
-// const showTime = useRef(false);
-// const showDateTime = useRef(false);
-// const animateTime = useCallback(() => {
-// if (isAttachmentMessage()) {
-// return;
-// }
-// // For messages with date change
-// if (message.dateChange) {
-// showDateTime.current = !showDateTime.current;
-// dateTimeDisplay.value = showDateTime.current ? "flex" : "none";
-// return;
-// }
-// // For all other messages
-// showTime.current = !showTime.current;
-// const animationConfig = { duration: 300 };
-// if (showTime.current) {
-// timeHeight.value = withTiming(34, animationConfig);
-// timeTranslateY.value = withTiming(0, animationConfig);
-// timeOpacity.value = withTiming(1, animationConfig);
-// } else {
-// timeOpacity.value = withTiming(0, animationConfig);
-// timeHeight.value = withTiming(0, animationConfig, () => {
-// timeTranslateY.value = withTiming(20, animationConfig);
-// });
-// }
-// }, [
-// timeHeight,
-// timeTranslateY,
-// timeOpacity,
-// dateTimeDisplay,
-// message.dateChange,
-// ]);
-
-// let messageContent: ReactNode;
-// const contentType = getMessageContentType(message.contentType);
-
-// const handleUrlPress = useCallback((url: string) => {
-// const cleanedUrl = url.toLowerCase().trim();
-
-// const uri = cleanedUrl.startsWith("http")
-// ? cleanedUrl
-// : `https://${cleanedUrl}`;
-
-// Linking.openURL(uri);
-// }, []);
-
-// // maybe using useChatStore inside ChatMessage
-// // leads to bad perf? Let's be cautious
-// const replyingToMessage = useChatStore((s) =>
-// message.referencedMessageId
-// ? s.conversations[message.topic]?.messages.get(
-// message.referencedMessageId
-// )
-// : undefined
-// );
-
-// const hideBackground =
-// isAttachmentMessage(message.contentType) ||
-// (isContentType("text", message.contentType) &&
-// !replyingToMessage &&
-// isAllEmojisAndMaxThree(message.content));
-
-// switch (contentType) {
-// case "attachment":
-// case "remoteAttachment":
-// messageContent = ;
-// break;
-// // TODO: Update later
-// // case "transactionReference":
-// // case "coinbasePayment":
-// // messageContent = ;
-// // break;
-// case "groupUpdated":
-// messageContent = (
-//
-// );
-// break;
-// default: {
-// messageContent =
-// // Don't show URL as part of message bubble if this is a frame
-// !isFrame && (
-//
-// );
-// break;
-// }
-// }
-
-// const isAttachment = isAttachmentMessage(message.contentType);
-// const isGroupUpdated = isContentType("groupUpdated", message.contentType);
-
-// const reactions = useMemo(() => getMessageReactions(message), [message]);
-// const hasReactions = Object.keys(reactions).length > 0;
-// const isChatMessage = !isGroupUpdated;
-// const shouldShowOutsideContentRow = isChatMessage && hasReactions;
-
-// let messageMaxWidth: DimensionValue;
-
-// if (isAttachment) {
-// messageMaxWidth = "60%";
-// } else {
-// if (isFrame) {
-// messageMaxWidth = "100%";
-// } else messageMaxWidth = "85%";
-// }
-
-// const showStatus =
-// message.fromMe &&
-// (!message.hasNextMessageInSeries ||
-// (message.hasNextMessageInSeries &&
-// message.nextMessageIsLoadingAttachment));
-
-// const replyingToProfileName = useMemo(() => {
-// if (!replyingToMessage?.senderAddress) return "";
-// if (replyingToMessage.senderAddress === currentAccount()) return "You";
-// return getReadableProfile(
-// currentAccount(),
-// replyingToMessage.senderAddress
-// );
-// }, [replyingToMessage?.senderAddress]);
-
-// const swipeableRef = useRef(null);
-
-// const renderLeftActions = useCallback(
-// (
-// progressAnimatedValue: RNAnimated.AnimatedInterpolation
-// ) => {
-// return (
-//
-//
-//
-// );
-// },
-// []
-// );
-
-// return (
-//
-// {message.dateChange && (
-//
-// {messageDate}
-//
-// {` – ${messageTime}`}
-//
-//
-// )}
-// {!message.dateChange && showTime && (
-//
-// {messageTime}
-//
-// )}
-// {isGroupUpdated && messageContent}
-// {isChatMessage && (
-// {
-// const translation = swipeableRef.current?.state.rowTranslation;
-// if (translation && (translation as any)._value > 70) {
-// Haptics.notificationAsync(
-// Haptics.NotificationFeedbackType.Success
-// );
-// converseEventEmitter.emit("triggerReplyToMessage", message);
-// }
-// }}
-// ref={swipeableRef}
-// >
-//
-// {!message.fromMe && (
-//
-// )}
-//
-// {isGroup &&
-// !message.fromMe &&
-// !message.hasPreviousMessageInSeries &&
-// isChatMessage && (
-//
-// )}
-//
-//
-// {isContentType("text", message.contentType) && (
-//
-// )}
-// {replyingToMessage ? (
-//
-// {
-// converseEventEmitter.emit("scrollChatToMessage", {
-// messageId: replyingToMessage.id,
-// animated: false,
-// });
-// setTimeout(() => {
-// converseEventEmitter.emit(
-// "highlightMessage",
-// replyingToMessage.id
-// );
-// }, 350);
-// }}
-// >
-//
-// {replyingToProfileName}
-//
-//
-//
-//
-//
-// {messageContent}
-//
-//
-//
-// ) : (
-//
-//
-// {messageContent}
-//
-//
-// )}
-//
-// {shouldShowOutsideContentRow ? (
-//
-// {isFrame && (
-// handleUrlPress(message.content)}
-// delayLongPress={platformTouchableLongPressDelay}
-// onLongPress={platformTouchableOnLongPress}
-// >
-//
-// {getUrlToRender(message.content)}
-//
-//
-// )}
-//
-//
-//
-// {isFrame && message.fromMe && !hasReactions && (
-//
-// )}
-//
-// ) : (
-// message.fromMe &&
-// !hasReactions &&
-// )}
-//
-//
-//
-//
-// )}
-//
-// );
-// };
-
-// // We use a cache for chat messages so that it doesn't rerender too often.
-// // Indeed, since we use an inverted FlashList for chat, when a new message
-// // arrives it is pushed at the BEGINNING of the array, and FlashList internals
-// // rerenders a bunch of messages which can have an impact on performance.
-// // With this LimitedMap we keep 50 rendered messages in RAM for better perf.
-
-// type RenderedChatMessage = {
-// renderedMessage: JSX.Element;
-// message: MessageToDisplay;
-// colorScheme: ColorSchemeName;
-// isGroup: boolean;
-// hasFrames: boolean;
-// };
-
-// const renderedMessages = new LimitedMap(50);
-// const keysChangesToRerender: (keyof MessageToDisplay)[] = [
-// "id",
-// "sent",
-// "status",
-// "lastUpdateAt",
-// "dateChange",
-// "hasNextMessageInSeries",
-// "hasPreviousMessageInSeries",
-// "isLatestSettledFromMe",
-// "isLatestSettledFromPeer",
-// "isLoadingAttachment",
-// "nextMessageIsLoadingAttachment",
-// "reactions",
-// ];
-
-// export default function CachedChatMessage({
-// account,
-// message,
-// colorScheme,
-// isGroup,
-// hasFrames = false,
-// }: Props) {
-// const alreadyRenderedMessage = renderedMessages.get(
-// `${account}-${message.id}`
-// );
-// const shouldRerender =
-// !alreadyRenderedMessage ||
-// alreadyRenderedMessage.colorScheme !== colorScheme ||
-// keysChangesToRerender.some(
-// (k) => message[k] !== alreadyRenderedMessage.message[k]
-// );
-// if (shouldRerender) {
-// const renderedMessage = ChatMessage({
-// account,
-// message,
-// colorScheme,
-// isGroup,
-// hasFrames,
-// });
-// renderedMessages.set(`${account}-${message.id}`, {
-// message,
-// renderedMessage,
-// colorScheme,
-// isGroup,
-// hasFrames,
-// });
-// return renderedMessage;
-// } else {
-// return alreadyRenderedMessage.renderedMessage;
-// }
-// }
-
-// const useStyles = () => {
-// const { theme } = useAppTheme();
-
-// const colorScheme = useColorScheme();
-// return StyleSheet.create({
-// messageContainer: {
-// flexDirection: "row",
-// width: "100%",
-// alignItems: "flex-end",
-// },
-// innerBubble: {
-// backgroundColor: messageInnerBubbleColor(colorScheme),
-// borderRadius: 14,
-// paddingHorizontal: 12,
-// paddingVertical: 10,
-// marginTop: 10,
-// marginHorizontal: 10,
-// },
-// innerBubbleMe: {
-// backgroundColor: myMessageInnerBubbleColor(colorScheme),
-// },
-// messageRow: {
-// flexDirection: "row",
-// flexWrap: "wrap",
-// },
-// messageSwipeable: {
-// width: "100%",
-// flexDirection: "row",
-// paddingLeft: 12,
-// paddingRight: 15,
-// overflow: "visible",
-// },
-// messageSwipeableChildren: {
-// width: "100%",
-// flexDirection: "row",
-// flexWrap: "wrap",
-// },
-// linkToFrame: {
-// fontSize: 12,
-// padding: 6,
-// color: textSecondaryColor(colorScheme),
-// flexGrow: 1,
-// },
-// dateTimeContainer: {
-// width: "100%",
-// flexDirection: "row",
-// justifyContent: "center",
-// alignItems: "center",
-// },
-// dateTime: {
-// textAlign: "center",
-// fontSize: 12,
-// color: textSecondaryColor(colorScheme),
-// marginTop: 12,
-// marginBottom: 8,
-// fontWeight: "bold",
-// height: 20,
-// },
-// replyToUsername: {
-// fontSize: 12,
-// marginBottom: 4,
-// color: textSecondaryColor(colorScheme),
-// paddingVertical: 0,
-// paddingHorizontal: 0,
-// },
-// messageText: {
-// color: textPrimaryColor(colorScheme),
-// fontSize: 17,
-// },
-// messageTextMe: {
-// color: inversePrimaryColor(colorScheme),
-// },
-// messageTextReply: {
-// color: textPrimaryColor(colorScheme),
-// },
-// messageTextReplyMe: {
-// color: inversePrimaryColor(colorScheme),
-// },
-// outsideContentRow: {
-// marginTop: theme.spacing["4xs"],
-// marginBottom: theme.spacing.xxxs,
-// flexDirection: "row",
-// justifyContent: "flex-start",
-// columnGap: 8,
-// width: "100%",
-// },
-// reactionsContainer: {
-// marginHorizontal: 8,
-// marginBottom: 8,
-// },
-// outsideReactionsContainer: {
-// flex: 1,
-// },
-// });
-// };
diff --git a/components/Chat/Message/MessageActions.tsx b/components/Chat/Message/MessageActions.tsx
deleted file mode 100644
index 71e01adac..000000000
--- a/components/Chat/Message/MessageActions.tsx
+++ /dev/null
@@ -1,499 +0,0 @@
-/**
- *
- * TODO: DELETE when finished refactoring messages/conversation
- *
- */
-
-// import { MessageContextMenu } from "@components/Chat/Message/MessageContextMenu";
-// import { TableViewItemType } from "@components/TableView/TableView";
-// import { TableViewPicto } from "@components/TableView/TableViewImage";
-// import { useSelect } from "@data/store/storeHelpers";
-// import { translate } from "@i18n/index";
-// import Clipboard from "@react-native-clipboard/clipboard";
-// import {
-// messageBubbleColor,
-// messageHighlightedBubbleColor,
-// myMessageBubbleColor,
-// myMessageHighlightedBubbleColor,
-// } from "@styles/colors";
-// import { useConversationContext } from "@utils/conversation";
-// import { navigate } from "@utils/navigation";
-// import * as Haptics from "expo-haptics";
-// import React, {
-// useCallback,
-// useEffect,
-// useMemo,
-// useRef,
-// useState,
-// } from "react";
-// import {
-// Keyboard,
-// Platform,
-// StyleSheet,
-// useColorScheme,
-// View,
-// } from "react-native";
-// import { Gesture, GestureDetector } from "react-native-gesture-handler";
-// import Animated, {
-// AnimatedStyle,
-// Easing,
-// ReduceMotion,
-// runOnJS,
-// useAnimatedStyle,
-// useSharedValue,
-// withTiming,
-// withSpring,
-// measure,
-// useAnimatedRef,
-// withDelay,
-// } from "react-native-reanimated";
-
-// import { MessageToDisplay } from "./Message";
-// import { MessageReactionsList } from "./MessageReactionsList";
-// import MessageTail from "./MessageTail";
-// import { useCurrentAccount } from "../../../data/store/accountsStore";
-// import { useAppStore } from "../../../data/store/appStore";
-// import { useFramesStore } from "../../../data/store/framesStore";
-// import { ReanimatedTouchableOpacity } from "../../../utils/animations";
-// import { converseEventEmitter } from "../../../utils/events";
-// import {
-// MessageReaction,
-// addReactionToMessage,
-// } from "../../../utils/reactions";
-// import { UUID_REGEX } from "../../../utils/regex";
-// import { isTransactionMessage } from "../../../utils/transaction";
-// import { isAttachmentMessage } from "@utils/attachment/isAttachmentMessage";
-
-// type Props = {
-// children: React.ReactNode;
-// message: MessageToDisplay;
-// reactions: {
-// [senderAddress: string]: MessageReaction[];
-// };
-// hideBackground: boolean;
-// isFrame: boolean;
-// };
-
-// enum ContextMenuActions {
-// REPLY = "Reply",
-// COPY_MESSAGE = "Copy",
-// SHARE_FRAME = "Share",
-// }
-
-// export default function ChatMessageActions({
-// children,
-// message,
-// reactions,
-// hideBackground = false,
-// isFrame,
-// }: Props) {
-// const isAttachment = isAttachmentMessage(message.contentType);
-// const isTransaction = isTransactionMessage(message.contentType);
-// const colorScheme = useColorScheme();
-// const userAddress = useCurrentAccount() as string;
-// const styles = useStyles();
-// const { setContextMenuShown } = useAppStore(
-// useSelect(["setContextMenuShown"])
-// );
-// const inputRef = useConversationContext("inputRef");
-// const opacity = useSharedValue(1);
-// const scale = useSharedValue(1);
-// const translateY = useSharedValue(0);
-// const itemRectY = useSharedValue(0);
-// const itemRectX = useSharedValue(0);
-// const itemRectHeight = useSharedValue(0);
-// const itemRectWidth = useSharedValue(0);
-// const containerRef = useAnimatedRef();
-// const [isActive, setIsActive] = useState(false);
-// const keyboardWasOpen = useRef(false);
-
-// const scaleBack = useCallback(() => {
-// "worklet";
-// scale.value = withTiming(1, {
-// duration: 150 / 2,
-// });
-// }, [scale]);
-
-// const activateAnimation = useCallback(() => {
-// "worklet";
-// const measured = measure(containerRef);
-// if (!measured) return;
-
-// itemRectY.value = measured.pageY;
-// itemRectX.value = measured.pageX;
-// itemRectHeight.value = measured.height;
-// itemRectWidth.value = measured.width;
-// opacity.value = withDelay(100, withTiming(0));
-// }, [
-// containerRef,
-// itemRectY,
-// itemRectX,
-// itemRectHeight,
-// itemRectWidth,
-// opacity,
-// ]);
-
-// const dismissKeyboard = useCallback(() => {
-// const isVisible = Keyboard.isVisible();
-// keyboardWasOpen.current = isVisible;
-// if (isVisible) {
-// Keyboard.dismiss();
-// }
-// }, []);
-
-// const onLongHoldCompletion = useCallback(
-// (isFinished?: boolean) => {
-// "worklet";
-// if (isFinished) {
-// activateAnimation();
-// runOnJS(dismissKeyboard)();
-// runOnJS(setIsActive)(true);
-// runOnJS(setContextMenuShown)(message.id);
-// }
-// },
-// [activateAnimation, message.id, setContextMenuShown, dismissKeyboard]
-// );
-
-// const scaleHold = useCallback(() => {
-// "worklet";
-// scale.value = withTiming(1.02, { duration: 210 }, onLongHoldCompletion);
-// }, [scale, onLongHoldCompletion]);
-
-// const canAddReaction =
-// message.status !== "sending" && message.status !== "error";
-
-// const tapGesture = useMemo(() => {
-// return Gesture.Tap()
-// .onStart(() => {
-// if (isAttachment) {
-// // Transfering attachment opening intent to component
-// converseEventEmitter.emit(
-// `openAttachmentForMessage-${message.id}` as const
-// );
-// }
-// if (isTransaction) {
-// // Transfering event to component
-// converseEventEmitter.emit(`showActionSheetForTxRef-${message.id}`);
-// }
-// })
-// .runOnJS(true);
-// }, [isAttachment, isTransaction, message]);
-
-// const doubleTapGesture = useMemo(
-// () =>
-// Gesture.Tap()
-// .numberOfTaps(2)
-// .onStart(() => {
-// if (isAttachment || !canAddReaction) return;
-// addReactionToMessage(userAddress, message, "❤️");
-// })
-// .runOnJS(true),
-// [canAddReaction, isAttachment, userAddress, message]
-// );
-
-// const longPressGesture = useMemo(() => {
-// return Gesture.LongPress()
-// .onStart(() => {
-// Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
-// scaleHold();
-// })
-// .onEnd(() => {
-// scaleBack();
-// })
-// .runOnJS(true);
-// }, [scaleBack, scaleHold]);
-
-// const composed = useMemo(() => {
-// return Gesture.Simultaneous(tapGesture, doubleTapGesture, longPressGesture);
-// }, [tapGesture, doubleTapGesture, longPressGesture]);
-
-// const initialBubbleBackgroundColor = message.fromMe
-// ? myMessageBubbleColor(colorScheme)
-// : messageBubbleColor(colorScheme);
-
-// const bubbleBackgroundColor = useSharedValue(initialBubbleBackgroundColor);
-
-// // reinit color on recycling
-// useEffect(() => {
-// if (bubbleBackgroundColor.value !== initialBubbleBackgroundColor) {
-// bubbleBackgroundColor.value = initialBubbleBackgroundColor;
-// }
-// }, [bubbleBackgroundColor, initialBubbleBackgroundColor]);
-
-// const animatedBackgroundStyle = useAnimatedStyle(() => {
-// return {
-// backgroundColor: bubbleBackgroundColor.value,
-// };
-// }, [bubbleBackgroundColor, message.id]);
-// const iosAnimatedTailStyle = useAnimatedStyle(
-// () => ({
-// color: bubbleBackgroundColor.value,
-// }),
-// [bubbleBackgroundColor]
-// ) as AnimatedStyle;
-// const [highlightingMessage, setHighlightingMessage] = useState(false);
-
-// const highlightMessage = useCallback(
-// (messageId: string) => {
-// if (messageId === message.id) {
-// setHighlightingMessage(true);
-// bubbleBackgroundColor.value = withTiming(
-// message.fromMe
-// ? myMessageHighlightedBubbleColor(colorScheme)
-// : messageHighlightedBubbleColor(colorScheme),
-// {
-// duration: 300,
-// easing: Easing.inOut(Easing.quad),
-// reduceMotion: ReduceMotion.System,
-// }
-// );
-// setTimeout(() => {
-// bubbleBackgroundColor.value = withTiming(
-// initialBubbleBackgroundColor,
-// {
-// duration: 300,
-// easing: Easing.inOut(Easing.quad),
-// reduceMotion: ReduceMotion.System,
-// },
-// () => {
-// runOnJS(setHighlightingMessage)(false);
-// }
-// );
-// }, 800);
-// }
-// },
-// [
-// bubbleBackgroundColor,
-// colorScheme,
-// initialBubbleBackgroundColor,
-// message.fromMe,
-// message.id,
-// ]
-// );
-
-// useEffect(() => {
-// converseEventEmitter.on(`highlightMessage`, highlightMessage);
-// return () => {
-// converseEventEmitter.off("highlightMessage", highlightMessage);
-// };
-// }, [highlightMessage]);
-
-// // Entrance animation for new messages. For sent messages,
-// // we filter on UUIDs to avoid repeating the animation
-// // when the message is received from the stream.
-// const shouldAnimateIn =
-// isAttachmentMessage(message.contentType) ||
-// message.isLatestSettledFromPeer ||
-// ((message.status === "sending" || message.status === "prepared") &&
-// UUID_REGEX.test(message.id));
-// const isAnimatingIn = useRef(false);
-
-// const triggerReplyToMessage = useCallback(() => {
-// converseEventEmitter.emit("triggerReplyToMessage", message.id);
-// }, [message]);
-
-// const frameURL = useMemo(() => {
-// if (isFrame) {
-// // TODO: Implement frames
-// const frames: any[] = [];
-// return frames[0]?.url;
-// }
-// return null;
-// }, [isFrame]);
-
-// const animateInStyle = useAnimatedStyle(() => {
-// return {
-// opacity: opacity.value,
-// transform: [{ scale: scale.value }, { translateY: translateY.value }],
-// };
-// });
-
-// const onContextCloseAnimation = useCallback(() => {
-// "worklet";
-// opacity.value = 1;
-// runOnJS(setIsActive)(false);
-// if (keyboardWasOpen.current && inputRef.current) {
-// inputRef.current.focus();
-// }
-// }, [inputRef, opacity]);
-
-// const onContextClose = useCallback(() => {
-// onContextCloseAnimation();
-// }, [onContextCloseAnimation]);
-
-// const animateIn = useCallback(() => {
-// "worklet";
-// opacity.value = 0;
-// scale.value = 0.7;
-// translateY.value = 20;
-
-// const timingConfig = {
-// duration: 250,
-// easing: Easing.inOut(Easing.quad),
-// };
-// const springConfig = {
-// damping: 10,
-// stiffness: 200,
-// mass: 0.2,
-// overshootClamping: false,
-// restSpeedThreshold: 0.001,
-// restDisplacementThreshold: 0.001,
-// };
-
-// opacity.value = withTiming(1, timingConfig);
-// scale.value = withSpring(1, springConfig);
-// translateY.value = withSpring(0, springConfig);
-// }, [opacity, scale, translateY]);
-
-// useEffect(() => {
-// if (shouldAnimateIn && !isAnimatingIn.current) {
-// isAnimatingIn.current = true;
-// animateIn();
-// }
-// }, [shouldAnimateIn, animateIn]);
-
-// // We use a mix of Gesture Detector AND TouchableOpacity
-// // because GestureDetector is better for dual tap but if
-// // we add the gesture detector for long press the long press
-// // in the parsed text stops working (https://github.com/software-mansion/react-native-gesture-handler/issues/867)
-
-// const StyledMessage = useMemo(() => {
-// return () => (
-//
-//
-// {children}
-//
-// {!message.hasNextMessageInSeries &&
-// !frameURL &&
-// !isAttachment &&
-// !isTransaction &&
-// !hideBackground &&
-// Platform.OS === "ios" && (
-//
-// )}
-//
-// );
-// }, [
-// styles.messageContainer,
-// styles.messageBubble,
-// styles.messageBubbleMe,
-// message.fromMe,
-// message.hasNextMessageInSeries,
-// message.hasPreviousMessageInSeries,
-// hideBackground,
-// initialBubbleBackgroundColor,
-// highlightingMessage,
-// animatedBackgroundStyle,
-// children,
-// frameURL,
-// isAttachment,
-// isTransaction,
-// iosAnimatedTailStyle,
-// colorScheme,
-// ]);
-
-// return (
-// <>
-//
-//
-//
-//
-//
-//
-//
-
-//
-// }
-// isActive={isActive}
-// onClose={onContextClose}
-// itemRectY={itemRectY}
-// itemRectX={itemRectX}
-// itemRectHeight={itemRectHeight}
-// itemRectWidth={itemRectWidth}
-// fromMe={message.fromMe}
-// >
-//
-//
-// >
-// );
-// }
-
-// const useStyles = () => {
-// return StyleSheet.create({
-// animateInWrapper: {
-// alignSelf: "flex-start",
-// flexDirection: "row",
-// borderRadius: 18,
-// },
-// messageBubble: {
-// flexShrink: 1,
-// flexGrow: 0,
-// minHeight: 32,
-// borderRadius: 18,
-// },
-// messageBubbleMe: {
-// marginLeft: "auto",
-// },
-// messageContainer: {
-// flexDirection: "row",
-// },
-// wrapper: {
-// width: "100%",
-// overflow: "visible",
-// },
-// });
-// };
diff --git a/components/Chat/Message/MessageStatus.tsx b/components/Chat/Message/MessageStatus.tsx
deleted file mode 100644
index aaab75d98..000000000
--- a/components/Chat/Message/MessageStatus.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { textSecondaryColor } from "@styles/colors";
-import React, { useEffect, useRef, useState } from "react";
-import { StyleSheet, useColorScheme, View } from "react-native";
-import Animated, {
- useSharedValue,
- useAnimatedStyle,
- withTiming,
- Easing,
-} from "react-native-reanimated";
-
-import { MessageToDisplay } from "./Message";
-
-type Props = {
- message: MessageToDisplay;
-};
-
-const statusMapping: {
- [key: string]: string | undefined;
-} = {
- sent: "Sent",
- delivered: "Sent",
- error: "Failed",
- sending: "Sending",
- prepared: "Sending",
- seen: "Read",
-};
-
-export default function MessageStatus({ message }: Props) {
- const styles = useStyles();
- const prevStatusRef = useRef(message.status);
- const isSentOrDelivered =
- message.status === "sent" || message.status === "delivered";
- const isLatestSettledFromMe = message.isLatestSettledFromMe;
-
- const [renderText, setRenderText] = useState(false);
- const opacity = useSharedValue(message.isLatestSettledFromMe ? 1 : 0);
- const height = useSharedValue(message.isLatestSettledFromMe ? 22 : 0);
- const scale = useSharedValue(message.isLatestSettledFromMe ? 1 : 0);
-
- const timingConfig = {
- duration: 200,
- easing: Easing.inOut(Easing.quad),
- };
-
- const animatedStyle = useAnimatedStyle(() => ({
- opacity: opacity.value,
- height: height.value,
- transform: [{ scale: scale.value }],
- }));
-
- useEffect(
- () => {
- const prevStatus = prevStatusRef.current;
- prevStatusRef.current = message.status;
-
- setTimeout(() => {
- requestAnimationFrame(() => {
- if (
- isSentOrDelivered &&
- (prevStatus === "sending" || prevStatus === "prepared")
- ) {
- opacity.value = withTiming(1, timingConfig);
- height.value = withTiming(22, timingConfig);
- scale.value = withTiming(1, timingConfig);
- setRenderText(true);
- } else if (isSentOrDelivered && !isLatestSettledFromMe) {
- opacity.value = withTiming(0, timingConfig);
- height.value = withTiming(0, timingConfig);
- scale.value = withTiming(0, timingConfig);
- setTimeout(() => setRenderText(false), timingConfig.duration);
- } else if (isLatestSettledFromMe) {
- opacity.value = 1;
- height.value = 22;
- scale.value = 1;
- setRenderText(true);
- }
- });
- }, 100);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [isLatestSettledFromMe, isSentOrDelivered]
- );
-
- return (
- message.fromMe &&
- message.status !== "sending" &&
- message.status !== "prepared" && (
-
-
-
- {renderText && statusMapping[message.status]}
-
-
-
- )
- );
-}
-
-const useStyles = () => {
- const colorScheme = useColorScheme();
- return StyleSheet.create({
- container: {
- overflow: "hidden",
- },
- contentContainer: {
- paddingTop: 5,
- },
- statusText: {
- fontSize: 12,
- color: textSecondaryColor(colorScheme),
- },
- });
-};
diff --git a/components/Chat/Message/V3Message.tsx b/components/Chat/Message/V3Message.tsx
deleted file mode 100644
index 5c26e04cd..000000000
--- a/components/Chat/Message/V3Message.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { MessageStaticAttachment } from "@/components/Chat/Message/message-content-types/message-static-attachment";
-import { MessageContextStoreProvider } from "@/components/Chat/Message/stores/message-store";
-import { VStack } from "@/design-system/VStack";
-import { InboxId, MessageId } from "@xmtp/react-native-sdk";
-import { memo } from "react";
-import { getCurrentConversationMessages } from "../../../features/conversation/conversation-service";
-import { hasNextMessageInSeries } from "../../../features/conversations/utils/hasNextMessageInSeries";
-import { hasPreviousMessageInSeries } from "../../../features/conversations/utils/hasPreviousMessageInSeries";
-import { messageIsFromCurrentUserV3 } from "../../../features/conversations/utils/messageIsFromCurrentUser";
-import { messageShouldShowDateChange } from "../../../features/conversations/utils/messageShouldShowDateChange";
-import { ChatGroupUpdatedMessage } from "../ChatGroupUpdatedMessage";
-import { MessageRemoteAttachment } from "./message-content-types/message-remote-attachment";
-import { MessageReply } from "./message-content-types/message-reply";
-import { MessageSimpleText } from "./message-content-types/message-simple-text";
-import {
- convertNanosecondsToMilliseconds,
- isGroupUpdatedMessage,
- isRemoteAttachmentMessage,
- isReplyMessage,
- isStaticAttachmentMessage,
- isTextMessage,
-} from "./message-utils";
-
-type V3MessageProps = {
- messageId: MessageId;
- previousMessageId: MessageId;
- nextMessageId: MessageId;
-};
-
-export const V3Message = memo(
- ({ messageId, previousMessageId, nextMessageId }: V3MessageProps) => {
- const messages = getCurrentConversationMessages()!;
-
- const message = messages.byId[messageId];
- const previousMessage = messages.byId[previousMessageId];
- const nextMessage = messages.byId[nextMessageId];
-
- const _hasPreviousMessageInSeries =
- !!previousMessage &&
- hasPreviousMessageInSeries({
- currentMessage: message,
- previousMessage,
- });
-
- const _hasNextMessageInSeries = Boolean(
- !!nextMessage &&
- message &&
- hasNextMessageInSeries({
- currentMessage: message,
- nextMessage,
- })
- );
-
- const showDateChange = messageShouldShowDateChange({
- message,
- previousMessage,
- });
-
- const fromMe = messageIsFromCurrentUserV3({
- message,
- });
-
- // const isLatestSettledFromMe = isLatestSettledFromCurrentUser({
- // message,
- // currentAccount,
- // });
-
- // const isLatestSettledFromPeer =
- // !!nextMessage &&
- // isLatestMessageSettledFromPeer({
- // message,
- // currentAccount,
- // nextMessage,
- // });
-
- if (!message) {
- console.log("no message found", messageId);
- return null;
- }
-
- return (
-
-
- {isTextMessage(message) && }
- {isGroupUpdatedMessage(message) && (
-
- )}
- {isReplyMessage(message) && }
- {isRemoteAttachmentMessage(message) && (
-
- )}
- {isStaticAttachmentMessage(message) && (
-
- )}
-
-
- );
- }
-);
diff --git a/components/Chat/Message/components/message-layout.tsx b/components/Chat/Message/components/message-layout.tsx
deleted file mode 100644
index f2cd245ad..000000000
--- a/components/Chat/Message/components/message-layout.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { MessageReactions } from "@/components/Chat/Message/MessageReactions/MessageReactions";
-import { V3MessageSenderAvatar } from "@/components/Chat/Message/MessageSenderAvatar";
-import { MessageContainer } from "@/components/Chat/Message/components/message-container";
-import { MessageContentContainer } from "@/components/Chat/Message/components/message-content-container";
-import { MessageRepliable } from "@/components/Chat/Message/components/message-repliable";
-import { MessageSpaceBetweenMessages } from "@/components/Chat/Message/components/message-space-between-messages";
-import { MessageDateChange } from "@/components/Chat/Message/message-date-change";
-import {
- IMessageGesturesOnLongPressArgs,
- MessageGestures,
-} from "@/components/Chat/Message/message-gestures";
-import { MessageTimestamp } from "@/components/Chat/Message/message-timestamp";
-import {
- useMessageContextStore,
- useMessageContextStoreContext,
-} from "@/components/Chat/Message/stores/message-store";
-import { useSelect } from "@/data/store/storeHelpers";
-import { VStack } from "@/design-system/VStack";
-import {
- setCurrentConversationReplyToMessageId,
- setMessageContextMenuData,
-} from "@/features/conversation/conversation-service";
-import { useAppTheme } from "@/theme/useAppTheme";
-import { ReactNode, useCallback } from "react";
-
-type IMessageLayoutProps = {
- children: ReactNode;
-};
-
-export function MessageLayout({ children }: IMessageLayoutProps) {
- const { theme } = useAppTheme();
-
- const { senderAddress, fromMe, messageId } = useMessageContextStoreContext(
- useSelect(["senderAddress", "fromMe", "messageId"])
- );
-
- const messageStore = useMessageContextStore();
-
- const handleReply = useCallback(() => {
- setCurrentConversationReplyToMessageId(messageId);
- }, [messageId]);
-
- const handleTap = useCallback(() => {
- const isShowingTime = !messageStore.getState().isShowingTime;
- messageStore.setState({
- isShowingTime,
- });
- }, [messageStore]);
-
- const handleLongPress = useCallback(
- (e: IMessageGesturesOnLongPressArgs) => {
- const messageId = messageStore.getState().messageId;
- setMessageContextMenuData({
- messageId,
- itemRectX: e.pageX,
- itemRectY: e.pageY,
- itemRectHeight: e.height,
- itemRectWidth: e.width,
- messageComponent: children,
- });
- },
- [messageStore, children]
- );
-
- return (
- <>
-
-
-
-
-
- {!fromMe && (
- <>
-
-
- >
- )}
-
-
- {children}
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/components/Chat/Message/components/message-repliable.tsx b/components/Chat/Message/components/message-repliable.tsx
deleted file mode 100644
index 12fda940d..000000000
--- a/components/Chat/Message/components/message-repliable.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Icon } from "@design-system/Icon/Icon";
-import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
-import { Haptics } from "@utils/haptics";
-import { memo, useRef } from "react";
-import { Animated, ViewStyle } from "react-native";
-import { Swipeable } from "react-native-gesture-handler";
-
-// TODO: Switch to import ReanimatedSwipeable from "react-native-gesture-handler/ReanimatedSwipeable"; once we upgrade to Expo SDK 52
-
-// TODO: Once we have Expo SDK 52, let's redo to be more performant and use SharedValue to trigger the onReply etc...
-
-// TODO: When we'll use ReanimatedSwipeable, we'll be able to listen to progress and trigger haptic once the treshold to reply is reached
-
-type IProps = {
- children: React.ReactNode;
- onReply: () => void;
-};
-
-export const MessageRepliable = memo(function MessageRepliable({
- children,
- onReply,
-}: IProps) {
- const { themed, theme } = useAppTheme();
-
- const swipeableRef = useRef(null);
- const dragOffsetFromLeftEdge = theme.spacing.xs;
- const xTresholdToReply = theme.spacing["3xl"];
-
- return (
- {
- return (
- // TODO: Switch to AnimatedVStack once we upgrade to Expo SDK 52
-
-
-
- );
- }}
- onSwipeableWillClose={() => {
- const translation = swipeableRef.current?.state.rowTranslation;
- const translationValue = (translation as any)._value;
- const v = translationValue - dragOffsetFromLeftEdge;
- if (translation && v > xTresholdToReply) {
- Haptics.successNotificationAsync();
- onReply();
- }
- }}
- ref={swipeableRef}
- >
- {children}
-
- );
-});
-
-const $container: ThemedStyle = ({ colors, spacing }) => ({
- width: "100%",
- flexDirection: "row",
- overflow: "visible",
-});
-
-const $childrenContainer: ThemedStyle = ({ colors, spacing }) => ({
- width: "100%",
- flexDirection: "row",
-});
diff --git a/components/Chat/Message/message-content-types/message-simple-text.tsx b/components/Chat/Message/message-content-types/message-simple-text.tsx
deleted file mode 100644
index 5f4ade762..000000000
--- a/components/Chat/Message/message-content-types/message-simple-text.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import {
- BubbleContainer,
- BubbleContentContainer,
-} from "@/components/Chat/Message/components/message-bubble";
-import { MessageLayout } from "@/components/Chat/Message/components/message-layout";
-import { MessageText } from "@/components/Chat/Message/components/message-text";
-import { useMessageContextStoreContext } from "@/components/Chat/Message/stores/message-store";
-import { useSelect } from "@/data/store/storeHelpers";
-import { DecodedMessage, TextCodec } from "@xmtp/react-native-sdk";
-import { memo } from "react";
-
-export const MessageSimpleText = memo(function MessageSimpleText(props: {
- message: DecodedMessage;
-}) {
- const { message } = props;
-
- const textContent = message.content();
-
- const { hasNextMessageInSeries, fromMe } = useMessageContextStoreContext(
- useSelect(["hasNextMessageInSeries", "fromMe"])
- );
-
- return (
-
-
-
- {textContent}
-
-
-
- );
-});
-
-/**
- * Tried different approaches to implement native context menu, but it's not
- * working as expected. Still missing some pieces from libraries to achieve what we want
- */
-
-// import { MessageContextMenu } from "@/components/Chat/Message/MessageContextMenu";
-// import ContextMenu from "react-native-context-menu-view";
-// import * as ContextMenu from "zeego/context-menu";
-
-{
- /* {
- console.log("onMenuWillShow");
- }}
- menuConfig={{
- menuTitle: "Message Options",
- menuItems: [
- {
- actionKey: "copy",
- actionTitle: "Copy",
- icon: {
- type: "IMAGE_SYSTEM",
- imageValue: {
- systemName: "doc.on.doc",
- },
- },
- },
- {
- actionKey: "delete",
- actionTitle: "Delete",
- menuAttributes: ["destructive"],
- icon: {
- type: "IMAGE_SYSTEM",
- imageValue: {
- systemName: "trash",
- },
- },
- },
- ],
- }}
- auxiliaryPreviewConfig={{
- transitionEntranceDelay: "RECOMMENDED",
- anchorPosition: "top",
- // alignmentHorizontal: "previewTrailing",
- verticalAnchorPosition: "top",
- // height: 600,
- // preferredHeight: {
- // mode: "percentRelativeToWindow",
- // percent: 50,
- // },
- }}
- previewConfig={{ previewType: "CUSTOM" }}
- // renderPreview={() => (
- //
- // {textContent}
- //
- // )}
- isAuxiliaryPreviewEnabled={true}
- renderPreview={() => (
- //
-
- {textContent}
-
- )}
- renderAuxiliaryPreview={() => (
-
- 😅
- 🤣
- 😂
- 🤩
- 🤗
- 🤔
-
- )}
- > */
-}
diff --git a/components/Chat/Message/message-context-menu/message-context-menu.tsx b/components/Chat/Message/message-context-menu/message-context-menu.tsx
deleted file mode 100644
index d6c9a7075..000000000
--- a/components/Chat/Message/message-context-menu/message-context-menu.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { MessageContextMenuBackdrop } from "@/components/Chat/Message/message-context-menu/message-context-menu-backdrop";
-import { MessageContextMenuEmojiPicker } from "@/components/Chat/Message/message-context-menu/message-context-menu-emoji-picker/message-context-menu-emoji-picker";
-import { openMessageContextMenuEmojiPicker } from "@/components/Chat/Message/message-context-menu/message-context-menu-emoji-picker/message-context-menu-emoji-picker-utils";
-import { MessageContextMenuItems } from "@/components/Chat/Message/message-context-menu/message-context-menu-items";
-import { MessageContextMenuReactors } from "@/components/Chat/Message/message-context-menu/message-context-menu-reactors";
-import {
- getMessageById,
- useConversationMessageReactions,
- useCurrentAccountInboxId,
-} from "@/components/Chat/Message/message-utils";
-import { AnimatedVStack, VStack } from "@/design-system/VStack";
-import { useConversationContext } from "@/features/conversation/conversation-context";
-import {
- resetMessageContextMenuData,
- useMessageContextMenuData,
-} from "@/features/conversation/conversation-service";
-import { messageIsFromCurrentUserV3 } from "@/features/conversations/utils/messageIsFromCurrentUser";
-import { calculateMenuHeight } from "@design-system/ContextMenu/ContextMenu.utils";
-import { Portal } from "@gorhom/portal";
-import { memo, useCallback } from "react";
-import { StyleSheet } from "react-native";
-import { MessageContextMenuAboveMessageReactions } from "./message-context-menu-above-message-reactions";
-import { MessageContextMenuContainer } from "./message-context-menu-container";
-import { getMessageContextMenuItems } from "./message-context-menu-utils";
-
-export const MESSAGE_CONTEXT_MENU_SPACE_BETWEEN_ABOVE_MESSAGE_REACTIONS_AND_MESSAGE = 16;
-
-export const MessageContextMenu = memo(function MessageContextMenu() {
- const messageContextMenuData = useMessageContextMenuData();
-
- if (!messageContextMenuData) {
- return null;
- }
-
- return ;
-});
-
-const Content = memo(function Content() {
- const {
- messageId,
- itemRectX,
- itemRectY,
- itemRectHeight,
- itemRectWidth,
- messageComponent,
- } = useMessageContextMenuData()!;
-
- const { data: currentUserInboxId } = useCurrentAccountInboxId();
- const message = getMessageById(messageId)!;
- const fromMe = messageIsFromCurrentUserV3({ message });
- const menuItems = getMessageContextMenuItems({
- messageId: messageId,
- });
- const menuHeight = calculateMenuHeight(menuItems.length);
- const { bySender } = useConversationMessageReactions(messageId!);
-
- const reactOnMessage = useConversationContext("reactOnMessage");
- const removeReactionFromMessage = useConversationContext(
- "removeReactionFromMessage"
- );
-
- const handlePressBackdrop = useCallback(() => {
- resetMessageContextMenuData();
- }, []);
-
- const handleSelectReaction = useCallback(
- (emoji: string) => {
- const currentUserAlreadyReacted = bySender?.[currentUserInboxId!]?.find(
- (reaction) => reaction.content === emoji
- );
- if (currentUserAlreadyReacted) {
- removeReactionFromMessage({
- messageId: messageId,
- emoji,
- });
- } else {
- reactOnMessage({ messageId: messageId, emoji });
- }
- resetMessageContextMenuData();
- },
- [
- reactOnMessage,
- messageId,
- currentUserInboxId,
- bySender,
- removeReactionFromMessage,
- ]
- );
-
- const handleChooseMoreEmojis = useCallback(() => {
- openMessageContextMenuEmojiPicker();
- }, []);
-
- const hasReactions = Boolean(bySender && Object.keys(bySender).length > 0);
-
- return (
- <>
-
-
-
- {!!bySender && }
-
-
-
- {/* Replace with rowGap when we refactored menu items */}
-
-
- {messageComponent}
-
- {/* Put back once we refactor the menu items */}
- {/* */}
-
-
-
-
-
-
-
- >
- );
-});
diff --git a/components/Chat/Message/message-date-change.tsx b/components/Chat/Message/message-date-change.tsx
deleted file mode 100644
index 1d0808ed5..000000000
--- a/components/Chat/Message/message-date-change.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
- useMessageContextStore,
- useMessageContextStoreContext,
-} from "@/components/Chat/Message/stores/message-store";
-import { AnimatedHStack } from "@design-system/HStack";
-import { AnimatedText, Text } from "@design-system/Text";
-import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
-import { useAppTheme } from "@theme/useAppTheme";
-import { getLocalizedTime, getRelativeDate } from "@utils/date";
-import { memo, useEffect } from "react";
-import {
- useAnimatedStyle,
- useSharedValue,
- withSpring,
-} from "react-native-reanimated";
-
-export const MessageDateChange = memo(function MessageDateChange() {
- const { theme } = useAppTheme();
-
- const [sentAt, showDateChange] = useMessageContextStoreContext((s) => [
- s.sentAt,
- s.showDateChange,
- ]);
-
- const showTimeAV = useSharedValue(0);
-
- const messageStore = useMessageContextStore();
-
- useEffect(() => {
- const unsubscribe = messageStore.subscribe(
- (state) => state.isShowingTime,
- (isShowingTime) => {
- showTimeAV.value = isShowingTime ? 1 : 0;
- }
- );
- return () => unsubscribe();
- }, [messageStore, showTimeAV]);
-
- const messageTime = sentAt ? getLocalizedTime(sentAt) : "";
-
- const timeInlineAnimatedStyle = useAnimatedStyle(() => {
- return {
- display: showTimeAV.value ? "flex" : "none",
- opacity: withSpring(showTimeAV.value ? 1 : 0, {
- damping: SICK_DAMPING,
- stiffness: SICK_STIFFNESS,
- }),
- };
- });
-
- if (!showDateChange) {
- return null;
- }
-
- const messageDate = getRelativeDate(sentAt);
-
- return (
-
-
- {messageDate}
-
-
- {messageTime}
-
-
- );
-});
diff --git a/components/Chat/Message/message-timestamp.tsx b/components/Chat/Message/message-timestamp.tsx
deleted file mode 100644
index b83e0c6db..000000000
--- a/components/Chat/Message/message-timestamp.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- useMessageContextStore,
- useMessageContextStoreContext,
-} from "@/components/Chat/Message/stores/message-store";
-import { Text } from "@design-system/Text";
-import { getTextStyle } from "@design-system/Text/Text.utils";
-import { AnimatedVStack } from "@design-system/VStack";
-import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
-import { useAppTheme } from "@theme/useAppTheme";
-import { getLocalizedTime } from "@utils/date";
-import { flattenStyles } from "@utils/styles";
-import { memo, useEffect } from "react";
-import {
- interpolate,
- useAnimatedStyle,
- useDerivedValue,
- useSharedValue,
- withSpring,
-} from "react-native-reanimated";
-
-export const MessageTimestamp = memo(function MessageTimestamp() {
- const { theme, themed } = useAppTheme();
-
- const [sentAt, showDateChange] = useMessageContextStoreContext((s) => [
- s.sentAt,
- s.showDateChange,
- ]);
-
- // const { showTimeAV } = useMessageContext();
-
- const showTimeAV = useSharedValue(0);
-
- const messageStore = useMessageContextStore();
-
- useEffect(() => {
- const unsubscribe = messageStore.subscribe(
- (state) => state.isShowingTime,
- (isShowingTime) => {
- showTimeAV.value = isShowingTime ? 1 : 0;
- }
- );
- return () => unsubscribe();
- }, [messageStore, showTimeAV]);
-
- const showTimeProgressAV = useDerivedValue(() => {
- return withSpring(showTimeAV.value ? 1 : 0, {
- damping: SICK_DAMPING,
- stiffness: SICK_STIFFNESS,
- });
- });
-
- const messageTime = sentAt ? getLocalizedTime(sentAt) : "";
-
- const textHeight = flattenStyles(
- getTextStyle(themed, { preset: "smaller" })
- ).lineHeight;
-
- const timeAnimatedStyle = useAnimatedStyle(() => {
- return {
- height: interpolate(
- showTimeProgressAV.value,
- [0, 1],
- [0, textHeight || 14]
- ),
- opacity: interpolate(showTimeProgressAV.value, [0, 1], [0, 1]),
- marginVertical: interpolate(
- showTimeProgressAV.value,
- [0, 1],
- [0, theme.spacing.sm]
- ),
- transform: [
- { scale: showTimeProgressAV.value },
- {
- translateY: interpolate(
- showTimeProgressAV.value,
- [0, 1],
- [theme.spacing.xl, 0]
- ),
- },
- ],
- };
- }, [textHeight]);
-
- // Because we'll show the time in the MessageDateChange component instead
- if (showDateChange) {
- return null;
- }
-
- return (
-
-
- {messageTime}
-
-
- );
-});
diff --git a/components/Chat/Message/stores/message-store.tsx b/components/Chat/Message/stores/message-store.tsx
deleted file mode 100644
index 3d9df4af8..000000000
--- a/components/Chat/Message/stores/message-store.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { InboxId, MessageId } from "@xmtp/react-native-sdk";
-import { createContext, memo, useContext, useEffect, useRef } from "react";
-import { createStore, useStore } from "zustand";
-import { subscribeWithSelector } from "zustand/middleware";
-
-type IMessageContextStoreProps = {
- messageId: MessageId;
- hasNextMessageInSeries: boolean;
- hasPreviousMessageInSeries: boolean;
- fromMe: boolean;
- sentAt: number;
- showDateChange: boolean;
- senderAddress: InboxId;
-};
-
-type IMessageContextStoreState = IMessageContextStoreProps & {
- isHighlighted: boolean;
- isShowingTime: boolean;
-};
-
-type IMessageContextStoreProviderProps =
- React.PropsWithChildren;
-
-type IMessageContextStore = ReturnType;
-
-export const MessageContextStoreProvider = memo(
- ({ children, ...props }: IMessageContextStoreProviderProps) => {
- const storeRef = useRef();
- if (!storeRef.current) {
- storeRef.current = createMessageContextStore(props);
- }
-
- useEffect(() => {
- // TODO: Check if the props have made something change in the store?
- storeRef.current?.setState(props);
- }, [props]);
-
- return (
-
- {children}
-
- );
- }
-);
-
-const createMessageContextStore = (initProps: IMessageContextStoreProps) => {
- return createStore()(
- subscribeWithSelector((set) => ({
- ...initProps,
- isHighlighted: false,
- isShowingTime: false,
- }))
- );
-};
-
-const MessageContextStoreContext = createContext(
- null
-);
-
-export function useMessageContextStoreContext(
- selector: (state: IMessageContextStoreState) => T
-): T {
- const store = useContext(MessageContextStoreContext);
- if (!store)
- throw new Error("Missing MessageContextStore.Provider in the tree");
- return useStore(store, selector);
-}
-
-export function useMessageContextStore() {
- const store = useContext(MessageContextStoreContext);
- if (!store) throw new Error();
- return store;
-}
diff --git a/components/Chat/Transaction/TransactionPreview.tsx b/components/Chat/Transaction/TransactionPreview.tsx
index 99148d4a8..9e1dad5f3 100644
--- a/components/Chat/Transaction/TransactionPreview.tsx
+++ b/components/Chat/Transaction/TransactionPreview.tsx
@@ -1,345 +1,345 @@
-import Clipboard from "@react-native-clipboard/clipboard";
-import {
- actionSheetColors,
- messageInnerBubbleColor,
- myMessageInnerBubbleColor,
- textPrimaryColor,
- textSecondaryColor,
-} from "@styles/colors";
-import * as Linking from "expo-linking";
-import { useCallback, useEffect } from "react";
-import {
- ColorSchemeName,
- Platform,
- StyleSheet,
- Text,
- useColorScheme,
- View,
-} from "react-native";
+// import Clipboard from "@react-native-clipboard/clipboard";
+// import {
+// actionSheetColors,
+// messageInnerBubbleColor,
+// myMessageInnerBubbleColor,
+// textPrimaryColor,
+// textSecondaryColor,
+// } from "@styles/colors";
+// import * as Linking from "expo-linking";
+// import { useCallback, useEffect } from "react";
+// import {
+// ColorSchemeName,
+// Platform,
+// StyleSheet,
+// Text,
+// useColorScheme,
+// View,
+// } from "react-native";
-import Checkmark from "../../../assets/checkmark.svg";
-import Clock from "../../../assets/clock.svg";
-import Exclamationmark from "../../../assets/exclamationmark.triangle.svg";
-// import { useConversationContext } from "../../../utils/conversation";
-// import { converseEventEmitter } from "../../../utils/events";
-// import { shortAddress } from "@utils/strings/shortAddress";
-// import { useTransactionForMessage } from "../../../utils/transaction";
-// import { showActionSheetWithOptions } from "../../StateHandlers/ActionSheetStateHandler";
-import { MessageToDisplay } from "../Message/Message";
-// import MessageTimestamp from "../Message/MessageTimestamp";
+// import Checkmark from "../../../assets/checkmark.svg";
+// import Clock from "../../../assets/clock.svg";
+// import Exclamationmark from "../../../assets/exclamationmark.triangle.svg";
+// // import { useConversationContext } from "../../../utils/conversation";
+// // import { converseEventEmitter } from "../../../utils/events";
+// // import { shortAddress } from "@utils/strings/shortAddress";
+// // import { useTransactionForMessage } from "../../../utils/transaction";
+// // import { showActionSheetWithOptions } from "../../StateHandlers/ActionSheetStateHandler";
+// import { MessageToDisplay } from "../../../features/conversation/Message";
+// // import MessageTimestamp from "../Message/MessageTimestamp";
-type Props = {
- message: MessageToDisplay;
-};
-
-const TransactionView = ({
- fromMe,
- children,
-}: {
- fromMe: boolean;
- children: React.ReactNode;
-}) => {
- const styles = useStyles();
- return (
- <>
-
- {children}
-
- >
- );
-};
+// type Props = {
+// message: MessageToDisplay;
+// };
-// const TransactionStatusView = ({
+// const TransactionView = ({
// fromMe,
-// transactionDisplay,
-// status,
-// colorScheme,
-// error,
-// showingAmount,
+// children,
// }: {
// fromMe: boolean;
-// transactionDisplay?: string;
-// status?: "PENDING" | "FAILURE" | "SUCCESS";
-// colorScheme: ColorSchemeName;
-// error?: string;
-// showingAmount: boolean;
+// children: React.ReactNode;
// }) => {
// const styles = useStyles();
-// const StatusIcon =
-// status === "FAILURE" || error
-// ? Exclamationmark
-// : status === "SUCCESS"
-// ? Checkmark
-// : Clock;
-// const statusText = error
-// ? error
-// : status === "PENDING"
-// ? "Pending"
-// : status === "FAILURE"
-// ? "Failed"
-// : status === "SUCCESS"
-// ? "Success"
-// : "Loading";
-
// return (
// <>
-//
-//
-// {transactionDisplay && (
-//
-// {transactionDisplay}
-//
-// )}
-//
-//
-// {statusText}
-//
-//
+//
+// {children}
//
// >
// );
// };
-// export default function TransactionPreview({ message }: Props) {
-// const colorScheme = useColorScheme();
-// const styles = useStyles();
-// const { transaction, transactionDisplay, amountToDisplay } =
-// useTransactionForMessage(message, conversation?.peerAddress);
+// // const TransactionStatusView = ({
+// // fromMe,
+// // transactionDisplay,
+// // status,
+// // colorScheme,
+// // error,
+// // showingAmount,
+// // }: {
+// // fromMe: boolean;
+// // transactionDisplay?: string;
+// // status?: "PENDING" | "FAILURE" | "SUCCESS";
+// // colorScheme: ColorSchemeName;
+// // error?: string;
+// // showingAmount: boolean;
+// // }) => {
+// // const styles = useStyles();
+// // const StatusIcon =
+// // status === "FAILURE" || error
+// // ? Exclamationmark
+// // : status === "SUCCESS"
+// // ? Checkmark
+// // : Clock;
+// // const statusText = error
+// // ? error
+// // : status === "PENDING"
+// // ? "Pending"
+// // : status === "FAILURE"
+// // ? "Failed"
+// // : status === "SUCCESS"
+// // ? "Success"
+// // : "Loading";
-// const showTransactionActionSheet = useCallback(() => {
-// const methods: { [key: string]: () => void } = {};
-// if (transaction.error) {
-// methods["Copy message content"] = () =>
-// Clipboard.setString(message.content);
-// } else {
-// if (transaction.blockExplorerURL) {
-// methods["See in block explorer"] = () =>
-// Linking.openURL(transaction.blockExplorerURL!);
-// }
-// }
-// methods["Cancel"] = () => {};
-// const options = Object.keys(methods);
-// showActionSheetWithOptions(
-// {
-// options,
-// cancelButtonIndex: options.indexOf("Cancel"),
-// ...actionSheetColors(colorScheme),
-// },
-// (selectedIndex?: number) => {
-// if (selectedIndex === undefined) return;
-// const selectedOption = options[selectedIndex];
-// const method = methods[selectedOption];
-// if (method) {
-// method();
-// }
-// }
-// );
-// }, [
-// colorScheme,
-// message.content,
-// transaction.blockExplorerURL,
-// transaction.error,
-// ]);
+// // return (
+// // <>
+// //
+// //
+// // {transactionDisplay && (
+// //
+// // {transactionDisplay}
+// //
+// // )}
+// //
+// //
+// // {statusText}
+// //
+// //
+// //
+// // >
+// // );
+// // };
-// useEffect(() => {
-// const eventHandler = `showActionSheetForTxRef-${message.id}` as const;
-// converseEventEmitter.on(eventHandler, showTransactionActionSheet);
-// return () => {
-// converseEventEmitter.off(eventHandler, showTransactionActionSheet);
-// };
-// }, [message.id, showTransactionActionSheet]);
+// // export default function TransactionPreview({ message }: Props) {
+// // const colorScheme = useColorScheme();
+// // const styles = useStyles();
+// // const { transaction, transactionDisplay, amountToDisplay } =
+// // useTransactionForMessage(message, conversation?.peerAddress);
-// const metadataView = (
-//
-// );
+// // const showTransactionActionSheet = useCallback(() => {
+// // const methods: { [key: string]: () => void } = {};
+// // if (transaction.error) {
+// // methods["Copy message content"] = () =>
+// // Clipboard.setString(message.content);
+// // } else {
+// // if (transaction.blockExplorerURL) {
+// // methods["See in block explorer"] = () =>
+// // Linking.openURL(transaction.blockExplorerURL!);
+// // }
+// // }
+// // methods["Cancel"] = () => {};
+// // const options = Object.keys(methods);
+// // showActionSheetWithOptions(
+// // {
+// // options,
+// // cancelButtonIndex: options.indexOf("Cancel"),
+// // ...actionSheetColors(colorScheme),
+// // },
+// // (selectedIndex?: number) => {
+// // if (selectedIndex === undefined) return;
+// // const selectedOption = options[selectedIndex];
+// // const method = methods[selectedOption];
+// // if (method) {
+// // method();
+// // }
+// // }
+// // );
+// // }, [
+// // colorScheme,
+// // message.content,
+// // transaction.blockExplorerURL,
+// // transaction.error,
+// // ]);
+
+// // useEffect(() => {
+// // const eventHandler = `showActionSheetForTxRef-${message.id}` as const;
+// // converseEventEmitter.on(eventHandler, showTransactionActionSheet);
+// // return () => {
+// // converseEventEmitter.off(eventHandler, showTransactionActionSheet);
+// // };
+// // }, [message.id, showTransactionActionSheet]);
+
+// // const metadataView = (
+// //
+// // );
-// // Converse sponsored transaction
-// if (transaction.sponsored || transaction.status !== "PENDING") {
-// return (
-// <>
-//
-// {amountToDisplay && (
-//
-// {amountToDisplay}
-//
-// )}
-//
-//
-// {metadataView}
-// >
-// );
-// } else {
-// return (
-// <>
-//
-//
-// Transaction
-//
-//
-// Blockchain: {transaction.chainName}
-//
-//
-// Transaction hash: {shortAddress(transaction.reference)}
-//
-//
-//
-// Status:
-//
-//
-//
-// Pending
-//
-//
-//
-// {metadataView}
-// >
-// );
-// }
-// }
+// // // Converse sponsored transaction
+// // if (transaction.sponsored || transaction.status !== "PENDING") {
+// // return (
+// // <>
+// //
+// // {amountToDisplay && (
+// //
+// // {amountToDisplay}
+// //
+// // )}
+// //
+// //
+// // {metadataView}
+// // >
+// // );
+// // } else {
+// // return (
+// // <>
+// //
+// //
+// // Transaction
+// //
+// //
+// // Blockchain: {transaction.chainName}
+// //
+// //
+// // Transaction hash: {shortAddress(transaction.reference)}
+// //
+// //
+// //
+// // Status:
+// //
+// //
+// //
+// // Pending
+// //
+// //
+// //
+// // {metadataView}
+// // >
+// // );
+// // }
+// // }
-const useStyles = () => {
- const colorScheme = useColorScheme();
- return StyleSheet.create({
- transactionDetailsContainer: {
- flexDirection: "column",
- width: "100%",
- },
- statusContainer: {
- flexDirection: "row",
- alignItems: "center",
- },
- centeredStatusContainer: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- minWidth: 100,
- },
- transactionDetails: {
- fontSize: Platform.OS === "android" ? 14 : 16,
- color: textSecondaryColor(colorScheme),
- },
- text: {
- paddingHorizontal: 8,
- paddingVertical: Platform.OS === "android" ? 2 : 3,
- fontSize: 17,
- color: textPrimaryColor(colorScheme),
- },
- textMe: {
- color: "white",
- },
- amount: {
- fontSize: Platform.OS === "android" ? 28 : 34,
- fontWeight: Platform.OS === "android" ? "normal" : "bold",
- paddingVertical: 3,
- textAlign: "center",
- },
- bold: {
- fontWeight: "bold",
- },
- small: {
- fontSize: 15,
- },
- innerBubble: {
- backgroundColor: messageInnerBubbleColor(colorScheme),
- borderRadius: 14,
- width: "100%",
- paddingHorizontal: 2,
- paddingVertical: 6,
- marginBottom: 5,
- },
- innerBubbleMe: {
- backgroundColor: myMessageInnerBubbleColor(colorScheme),
- },
- metadataContainer: {
- position: "absolute",
- bottom: 6,
- right: 12,
- backgroundColor: "rgba(24, 24, 24, 0.5)",
- borderRadius: 18,
- paddingLeft: 1,
- paddingRight: 2,
- zIndex: 2,
- ...Platform.select({
- default: {
- paddingBottom: 1,
- paddingTop: 1,
- },
- android: { paddingBottom: 3, paddingTop: 2 },
- }),
- },
- statusIcon: {
- marginRight: -4,
- marginLeft: 8,
- },
- statusIconInline: {
- marginHorizontal: -4,
- },
- });
-};
+// const useStyles = () => {
+// const colorScheme = useColorScheme();
+// return StyleSheet.create({
+// transactionDetailsContainer: {
+// flexDirection: "column",
+// width: "100%",
+// },
+// statusContainer: {
+// flexDirection: "row",
+// alignItems: "center",
+// },
+// centeredStatusContainer: {
+// flexDirection: "row",
+// alignItems: "center",
+// justifyContent: "center",
+// minWidth: 100,
+// },
+// transactionDetails: {
+// fontSize: Platform.OS === "android" ? 14 : 16,
+// color: textSecondaryColor(colorScheme),
+// },
+// text: {
+// paddingHorizontal: 8,
+// paddingVertical: Platform.OS === "android" ? 2 : 3,
+// fontSize: 17,
+// color: textPrimaryColor(colorScheme),
+// },
+// textMe: {
+// color: "white",
+// },
+// amount: {
+// fontSize: Platform.OS === "android" ? 28 : 34,
+// fontWeight: Platform.OS === "android" ? "normal" : "bold",
+// paddingVertical: 3,
+// textAlign: "center",
+// },
+// bold: {
+// fontWeight: "bold",
+// },
+// small: {
+// fontSize: 15,
+// },
+// innerBubble: {
+// backgroundColor: messageInnerBubbleColor(colorScheme),
+// borderRadius: 14,
+// width: "100%",
+// paddingHorizontal: 2,
+// paddingVertical: 6,
+// marginBottom: 5,
+// },
+// innerBubbleMe: {
+// backgroundColor: myMessageInnerBubbleColor(colorScheme),
+// },
+// metadataContainer: {
+// position: "absolute",
+// bottom: 6,
+// right: 12,
+// backgroundColor: "rgba(24, 24, 24, 0.5)",
+// borderRadius: 18,
+// paddingLeft: 1,
+// paddingRight: 2,
+// zIndex: 2,
+// ...Platform.select({
+// default: {
+// paddingBottom: 1,
+// paddingTop: 1,
+// },
+// android: { paddingBottom: 3, paddingTop: 2 },
+// }),
+// },
+// statusIcon: {
+// marginRight: -4,
+// marginLeft: 8,
+// },
+// statusIconInline: {
+// marginHorizontal: -4,
+// },
+// });
+// };
diff --git a/components/Connecting.tsx b/components/Connecting.tsx
index a127cb4a2..c474102f4 100644
--- a/components/Connecting.tsx
+++ b/components/Connecting.tsx
@@ -1,11 +1,11 @@
import { sentryTrackMessage } from "@utils/sentry";
import { useEffect, useRef, useState } from "react";
-import ActivityIndicator from "./ActivityIndicator/ActivityIndicator";
import { useDebugEnabled } from "./DebugButton";
-import { useChatStore } from "../data/store/accountsStore";
+import { useChatStore, useCurrentAccount } from "../data/store/accountsStore";
import { useAppStore } from "../data/store/appStore";
import { useSelect } from "../data/store/storeHelpers";
+import { useV3ConversationListQuery } from "@/queries/useV3ConversationListQuery";
export const useShouldShowConnecting = () => {
const isInternetReachable = useAppStore((s) => s.isInternetReachable);
@@ -66,9 +66,9 @@ export const useShouldShowConnecting = () => {
};
export const useShouldShowConnectingOrSyncing = () => {
- const { initialLoadDoneOnce } = useChatStore(
- useSelect(["initialLoadDoneOnce"])
- );
+ const currentAccount = useCurrentAccount();
+ const { isLoading } = useV3ConversationListQuery(currentAccount!);
+ const initialLoadDoneOnce = !isLoading;
const shouldShowConnecting = useShouldShowConnecting();
const conditionTrueTime = useRef(0);
@@ -101,7 +101,3 @@ export const useShouldShowConnectingOrSyncing = () => {
return shouldShowConnecting.shouldShow || !initialLoadDoneOnce;
};
-
-export default function Connecting() {
- return ;
-}
diff --git a/components/Conversation/V3Conversation.tsx b/components/Conversation/V3Conversation.tsx
deleted file mode 100644
index 76d0f4363..000000000
--- a/components/Conversation/V3Conversation.tsx
+++ /dev/null
@@ -1,433 +0,0 @@
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { useConversationMessages } from "@queries/useConversationMessages";
-import {
- ConversationTopic,
- ConversationVersion,
- MessageId,
-} from "@xmtp/react-native-sdk";
-import { memo, useCallback, useEffect, useRef } from "react";
-import { FlatListProps, Platform } from "react-native";
-// import { DmChatPlaceholder } from "@components/Chat/ChatPlaceholder/ChatPlaceholder";
-import { DmConsentPopup } from "@/components/Chat/ConsentPopup/dm-consent-popup";
-import { GroupConsentPopup } from "@/components/Chat/ConsentPopup/group-consent-popup";
-import { MessageReactionsDrawer } from "@/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer";
-import { MessageContextMenu } from "@/components/Chat/Message/message-context-menu/message-context-menu";
-import { ExternalWalletPicker } from "@/features/ExternalWalletPicker/ExternalWalletPicker";
-import { ExternalWalletPickerContextProvider } from "@/features/ExternalWalletPicker/ExternalWalletPicker.context";
-import { useConversationIsUnread } from "@/features/conversation-list/hooks/useMessageIsUnread";
-import { useToggleReadStatus } from "@/features/conversation-list/hooks/useToggleReadStatus";
-import { useDmPeerInboxId } from "@/queries/useDmPeerInbox";
-import { V3Message } from "@components/Chat/Message/V3Message";
-import { Screen } from "@components/Screen/ScreenComp/Screen";
-import { Button } from "@design-system/Button/Button";
-import { Center } from "@design-system/Center";
-import { Text } from "@design-system/Text";
-import { AnimatedVStack, VStack } from "@design-system/VStack";
-import {
- Composer,
- IComposerSendArgs,
-} from "@features/conversation/composer/composer";
-import {
- ConversationContextProvider,
- useConversationContext,
-} from "@features/conversation/conversation-context";
-import {
- ConversationGroupContextProvider,
- useConversationGroupContext,
-} from "@features/conversation/conversation-group-context";
-import {
- initializeCurrentConversation,
- useConversationCurrentTopic,
-} from "@features/conversation/conversation-service";
-import { DmConversationTitle } from "@features/conversations/components/DmConversationTitle";
-import { GroupConversationTitle } from "@features/conversations/components/GroupConversationTitle";
-import { NewConversationTitle } from "@features/conversations/components/NewConversationTitle";
-import { translate } from "@i18n/translate";
-import { useRouter } from "@navigation/useNavigation";
-import { useAppTheme } from "@theme/useAppTheme";
-import Animated, {
- AnimatedProps,
- useAnimatedKeyboard,
- useAnimatedStyle,
-} from "react-native-reanimated";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-
-const keyExtractor = (item: string) => item;
-
-type V3ConversationProps = {
- topic: ConversationTopic | undefined;
- peerAddress?: string;
- textPrefill?: string;
-};
-
-export const V3Conversation = ({
- topic,
- peerAddress,
- textPrefill,
-}: V3ConversationProps) => {
- // TODO: Handle when topic is not defined
- const messageToPrefill = textPrefill ?? "";
- initializeCurrentConversation({
- topic,
- peerAddress,
- inputValue: messageToPrefill,
- });
-
- return (
-
-
-
-
-
- );
-};
-
-const Content = memo(function Content() {
- const { theme } = useAppTheme();
- const isNewConversation = useConversationContext("isNewConversation");
- const conversationVersion = useConversationContext("conversationVersion");
-
- return (
-
-
- {isNewConversation ? (
-
- ) : conversationVersion === ConversationVersion.DM ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
- );
-});
-
-const ComposerWrapper = memo(function ComposerWrapper() {
- const sendMessage = useConversationContext("sendMessage");
-
- const handleSend = useCallback(
- async (args: IComposerSendArgs) => {
- sendMessage(args);
- },
- [sendMessage]
- );
-
- return ;
-});
-
-const NewConversationContent = memo(function NewConversationContent() {
- useNewConversationHeader();
-
- return ;
-});
-
-const DmContent = memo(function DmContent() {
- const currentAccount = useCurrentAccount()!;
- const topic = useConversationCurrentTopic()!;
- const conversationNotFound = useConversationContext("conversationNotFound");
- const isAllowedConversation = useConversationContext("isAllowedConversation");
- // const peerAddress = useConversationContext("peerAddress")!;
- const conversationId = useConversationContext("conversationId")!;
- const isLoadingConversationConsent = useConversationContext(
- "isLoadingConversationConsent"
- );
-
- const {
- data: messages,
- isLoading: messagesLoading,
- isRefetching: isRefetchingMessages,
- refetch: refetchMessages,
- } = useConversationMessages(currentAccount, topic!);
-
- const { data: peerInboxId } = useDmPeerInboxId(currentAccount, topic!);
-
- const isUnread = useConversationIsUnread({
- topic,
- lastMessage: messages?.ids?.length
- ? messages.byId[messages.ids[0]]
- : undefined,
- timestamp: messages?.ids?.length
- ? (messages.byId[messages.ids[0]]?.sentNs ?? 0)
- : 0,
- });
- const toggleReadStatus = useToggleReadStatus({
- topic,
- isUnread,
- currentAccount,
- });
- useMarkAsReadOnEnter({
- messagesLoading,
- isUnread,
- toggleReadStatus,
- });
-
- useDmHeader();
-
- if (conversationNotFound) {
- // TODO: Add DM placeholder
- return null;
- }
-
- if (messages?.ids.length === 0 && !messagesLoading) {
- // TODO: Add DM placeholder
- return null;
- }
-
- return (
-
- ) : undefined
- }
- />
- );
-});
-
-const GroupContent = memo(function GroupContent() {
- const currentAccount = useCurrentAccount()!;
- const topic = useConversationCurrentTopic()!;
- const conversationNotFound = useConversationContext("conversationNotFound");
- const isAllowedConversation = useConversationContext("isAllowedConversation");
- const isLoadingConversationConsent = useConversationContext(
- "isLoadingConversationConsent"
- );
-
- const {
- data: messages,
- isLoading: messagesLoading,
- isRefetching: isRefetchingMessages,
- refetch,
- } = useConversationMessages(currentAccount, topic!);
-
- const isUnread = useConversationIsUnread({
- topic,
- lastMessage: messages?.byId[messages?.ids[0]], // Get latest message
- timestamp: messages?.byId[messages?.ids[0]]?.sentNs ?? 0,
- });
- const toggleReadStatus = useToggleReadStatus({
- topic,
- isUnread,
- currentAccount,
- });
- useMarkAsReadOnEnter({
- messagesLoading,
- isUnread,
- toggleReadStatus,
- });
-
- useGroupHeader();
-
- if (conversationNotFound) {
- return ;
- }
-
- if (messages?.ids.length === 0 && !messagesLoading) {
- return ;
- }
-
- return (
-
- ) : undefined
- }
- />
- );
-});
-
-export const MessagesList = memo(function MessagesList(
- props: Omit>, "renderItem" | "data"> & {
- messageIds: MessageId[];
- }
-) {
- const { messageIds, ...rest } = props;
-
- return (
- // @ts-ignore
- {
- return (
-
- );
- }}
- keyboardDismissMode="interactive"
- automaticallyAdjustContentInsets={false}
- contentInsetAdjustmentBehavior="never"
- keyExtractor={keyExtractor}
- keyboardShouldPersistTaps="handled"
- // estimatedItemSize={34} // TODO
- showsVerticalScrollIndicator={Platform.OS === "ios"} // Size glitch on Android
- pointerEvents="auto"
- /**
- * Causes a glitch on Android, no sure we need it for now
- */
- // maintainVisibleContentPosition={{
- // minIndexForVisible: 0,
- // autoscrollToTopThreshold: 100,
- // }}
- // estimatedListSize={Dimensions.get("screen")}
- {...rest}
- />
- );
-});
-
-export const KeyboardFiller = memo(function KeyboardFiller() {
- const { height: keyboardHeightAV } = useAnimatedKeyboard();
- const insets = useSafeAreaInsets();
-
- const as = useAnimatedStyle(() => ({
- height: Math.max(keyboardHeightAV.value - insets.bottom, 0),
- }));
-
- return ;
-});
-
-const useMarkAsReadOnEnter = ({
- messagesLoading,
- isUnread,
- toggleReadStatus,
-}: {
- messagesLoading: boolean;
- isUnread: boolean;
- toggleReadStatus: () => void;
-}) => {
- const hasMarkedAsRead = useRef(false);
-
- useEffect(() => {
- if (isUnread && !messagesLoading && !hasMarkedAsRead.current) {
- toggleReadStatus();
- hasMarkedAsRead.current = true;
- }
- }, [isUnread, messagesLoading, toggleReadStatus]);
-};
-
-function useNewConversationHeader() {
- const navigation = useRouter();
-
- const peerAddress = useConversationContext("peerAddress");
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: () => ,
- });
- }, [peerAddress, navigation]);
-}
-
-function useDmHeader() {
- const navigation = useRouter();
-
- const topic = useConversationCurrentTopic();
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: () => ,
- });
- }, [topic, navigation]);
-}
-
-function useGroupHeader() {
- const navigation = useRouter();
-
- const topic = useConversationCurrentTopic();
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: () => ,
- });
- }, [topic, navigation]);
-}
-
-const GroupConversationMissing = memo(() => {
- const topic = useConversationCurrentTopic();
-
- return (
-
-
- {topic
- ? translate("group_not_found")
- : translate("opening_conversation")}
-
-
- );
-});
-
-const GroupConversationEmpty = memo(() => {
- const { theme } = useAppTheme();
-
- const groupName = useConversationGroupContext("groupName");
- const sendMessage = useConversationContext("sendMessage");
-
- const handleSend = useCallback(() => {
- sendMessage({
- content: {
- text: "👋",
- },
- });
- }, [sendMessage]);
-
- return (
-
-
- {translate("group_placeholder.placeholder_text", {
- groupName,
- })}
-
-
-
-
- );
-});
diff --git a/components/Conversation/V3ConversationHeader.tsx b/components/Conversation/V3ConversationHeader.tsx
deleted file mode 100644
index 6d6a3bdd3..000000000
--- a/components/Conversation/V3ConversationHeader.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export const V3ConversationHeader = () => {};
diff --git a/components/Chat/ChatNullState.tsx b/components/ConversationList/ChatNullState.tsx
similarity index 98%
rename from components/Chat/ChatNullState.tsx
rename to components/ConversationList/ChatNullState.tsx
index b256177dc..7c9893fc1 100644
--- a/components/Chat/ChatNullState.tsx
+++ b/components/ConversationList/ChatNullState.tsx
@@ -25,7 +25,7 @@ import {
getPreferredUsername,
getProfile,
} from "../../utils/profile";
-import NewConversationButton from "../ConversationList/NewConversationButton";
+import NewConversationButton from "./NewConversationButton";
type ChatNullStateProps = {
currentAccount: string;
diff --git a/components/GroupAvatar.tsx b/components/GroupAvatar.tsx
index e54d288b3..5c7b3f567 100644
--- a/components/GroupAvatar.tsx
+++ b/components/GroupAvatar.tsx
@@ -42,9 +42,6 @@ type GroupAvatarProps = {
topic?: ConversationTopic;
pendingGroupMembers?: { address: string; uri?: string; name?: string }[];
excludeSelf?: boolean;
- // Converstion List should not make requests across the bridge
- // Use data from the initial sync, and as the query gets updated
- onConversationListScreen?: boolean;
};
type GroupAvatarDumbProps = {
@@ -194,23 +191,10 @@ const GroupAvatar: React.FC = ({
topic,
pendingGroupMembers,
excludeSelf = true,
- onConversationListScreen = false,
}) => {
const colorScheme = useColorScheme();
const styles = getStyles(colorScheme);
- const { members: groupMembers } = useGroupMembers(
- topic!,
- onConversationListScreen
- ? {
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchInterval: false,
- staleTime: Infinity,
- enabled: !!topic && !pendingGroupMembers,
- }
- : undefined
- );
+ const { members: groupMembers } = useGroupMembers(topic!);
const profiles = useProfilesStore((s) => s.profiles);
const account = useCurrentAccount();
diff --git a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx
index 532ec7cc9..c8e9368b9 100644
--- a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx
+++ b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx
@@ -69,6 +69,7 @@ export function useConnectViaWalletStoreContext(
export function useConnectViaWalletStore() {
const store = useContext(ConnectViaWalletStoreContext);
- if (!store) throw new Error();
+ if (!store)
+ throw new Error(`Missing ConnectViaWalletStore.Provider in the tree`);
return store;
}
diff --git a/components/Onboarding/init-xmtp-client.ts b/components/Onboarding/init-xmtp-client.ts
index ec41fbb51..03373450c 100644
--- a/components/Onboarding/init-xmtp-client.ts
+++ b/components/Onboarding/init-xmtp-client.ts
@@ -2,6 +2,7 @@ import { Signer } from "ethers";
import { Alert } from "react-native";
// import { refreshProfileForAddress } from "../../data/helpers/profiles/profilesUpdate";
+import { prefetchInboxIdQuery } from "@/queries/use-inbox-id-query";
import {
getSettingsStore,
getWalletStore,
@@ -9,7 +10,6 @@ import {
} from "../../data/store/accountsStore";
import { translate } from "../../i18n";
import { awaitableAlert } from "../../utils/alert";
-import { saveXmtpKey } from "../../utils/keychain/helpers";
import logger from "../../utils/logger";
import { logoutAccount, waitForLogoutTasksDone } from "../../utils/logout";
import { sentryTrackMessage } from "../../utils/sentry";
@@ -118,6 +118,8 @@ async function finalizeAccountSetup(args: IConnectWithAddressKeyArgs) {
getWalletStore(address).getState().setPrivateKeyPath(args.pkPath);
}
+ await prefetchInboxIdQuery({ account: address });
+
getXmtpClient(address);
logger.debug("Account setup finalized");
diff --git a/components/PinnedConversations/PinnedMessagePreview.tsx b/components/PinnedConversations/PinnedMessagePreview.tsx
index ab05d526d..b7eeb3b2a 100644
--- a/components/PinnedConversations/PinnedMessagePreview.tsx
+++ b/components/PinnedConversations/PinnedMessagePreview.tsx
@@ -4,7 +4,7 @@ import { ThemedStyle, useAppTheme } from "@/theme/useAppTheme";
import { DecodedMessageWithCodecsType } from "@/utils/xmtpRN/client";
import { useMemo } from "react";
import { ViewStyle } from "react-native";
-import { isTextMessage } from "../Chat/Message/message-utils";
+import { isTextMessage } from "../../features/conversation/conversation-message/conversation-message.utils";
export type PinnedMessagePreviewProps = {
message: DecodedMessageWithCodecsType;
diff --git a/components/PinnedConversations/PinnedV3DMConversation.tsx b/components/PinnedConversations/PinnedV3DMConversation.tsx
index 446ec9844..dd19354a0 100644
--- a/components/PinnedConversations/PinnedV3DMConversation.tsx
+++ b/components/PinnedConversations/PinnedV3DMConversation.tsx
@@ -18,7 +18,7 @@ import { useConversationIsUnread } from "@/features/conversation-list/hooks/useM
import { useHandleDeleteDm } from "@/features/conversation-list/hooks/useHandleDeleteDm";
import { useAppTheme } from "@/theme/useAppTheme";
import { ContextMenuIcon, ContextMenuItem } from "../ContextMenuItems";
-import { isTextMessage } from "../Chat/Message/message-utils";
+import { isTextMessage } from "../../features/conversation/conversation-message/conversation-message.utils";
import { VStack } from "@/design-system/VStack";
import { PinnedMessagePreview } from "./PinnedMessagePreview";
@@ -55,7 +55,7 @@ export const PinnedV3DMConversation = ({
const isUnread = useConversationIsUnread({
topic,
lastMessage: conversation.lastMessage,
- timestamp,
+ timestampNs: timestamp,
});
const toggleReadStatus = useToggleReadStatus({
diff --git a/components/PinnedConversations/PinnedV3GroupConversation.tsx b/components/PinnedConversations/PinnedV3GroupConversation.tsx
index 22171b80b..d7a7b5bb3 100644
--- a/components/PinnedConversations/PinnedV3GroupConversation.tsx
+++ b/components/PinnedConversations/PinnedV3GroupConversation.tsx
@@ -17,7 +17,7 @@ import { useToggleReadStatus } from "@/features/conversation-list/hooks/useToggl
import { useConversationIsUnread } from "@/features/conversation-list/hooks/useMessageIsUnread";
import { useAppTheme } from "@/theme/useAppTheme";
import { ContextMenuIcon, ContextMenuItem } from "../ContextMenuItems";
-import { isTextMessage } from "../Chat/Message/message-utils";
+import { isTextMessage } from "../../features/conversation/conversation-message/conversation-message.utils";
import { VStack } from "@/design-system/VStack";
import { PinnedMessagePreview } from "./PinnedMessagePreview";
@@ -52,7 +52,7 @@ export const PinnedV3GroupConversation = ({
const isUnread = useConversationIsUnread({
topic,
lastMessage: group.lastMessage,
- timestamp,
+ timestampNs: timestamp,
});
const toggleReadStatus = useToggleReadStatus({
diff --git a/components/Recommendations/Recommendations.tsx b/components/Recommendations/Recommendations.tsx
index 009ec819d..eded0d24a 100644
--- a/components/Recommendations/Recommendations.tsx
+++ b/components/Recommendations/Recommendations.tsx
@@ -80,7 +80,7 @@ export default function Recommendations({
navigation.popToTop();
setTimeout(() => {
navigation.navigate("Conversation", {
- mainConversationWithPeer: config.contactAddress,
+ peer: config.contactAddress,
});
}, 300);
}, [navigation]);
diff --git a/components/Screen/ScreenComp/Screen.props.tsx b/components/Screen/ScreenComp/Screen.props.tsx
index fefdb634c..280f033f1 100644
--- a/components/Screen/ScreenComp/Screen.props.tsx
+++ b/components/Screen/ScreenComp/Screen.props.tsx
@@ -1,4 +1,3 @@
-import { StatusBarProps } from "expo-status-bar";
import { ScrollViewProps, StyleProp, ViewStyle } from "react-native";
import { ExtendedEdge } from "./Screen.helpers";
@@ -24,14 +23,6 @@ type BaseScreenProps = {
* Background color
*/
backgroundColor?: string;
- /**
- * Status bar setting. Defaults to dark.
- */
- statusBarStyle?: "light" | "dark";
- /**
- * Pass any additional props directly to the StatusBar component.
- */
- StatusBarProps?: StatusBarProps;
/**
* Pass any additional props directly to the KeyboardAvoidingView component.
*/
diff --git a/components/Screen/ScreenComp/Screen.tsx b/components/Screen/ScreenComp/Screen.tsx
index 10ceb5181..72a37759d 100644
--- a/components/Screen/ScreenComp/Screen.tsx
+++ b/components/Screen/ScreenComp/Screen.tsx
@@ -1,5 +1,4 @@
import { useScrollToTop } from "@react-navigation/native";
-import { StatusBar } from "expo-status-bar";
import React, { useRef } from "react";
import { ScrollView, View, ViewStyle } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
@@ -81,23 +80,18 @@ function ScreenWithScrolling(props: IScreenProps) {
/**
* Represents a screen component that provides a consistent layout and behavior for different screen presets.
* The `Screen` component can be used with different presets such as "fixed", "scroll", or "auto".
- * It handles safe area insets, status bar settings, keyboard avoiding behavior, and scrollability based on the preset.
+ * It handles safe area insets, keyboard avoiding behavior, and scrollability based on the preset.
* @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/Screen/}
*/
export function Screen(props: IScreenProps) {
const { theme } = useAppTheme();
- const {
- backgroundColor = theme.colors.background.surface,
- safeAreaEdges,
- StatusBarProps,
- statusBarStyle = "dark",
- } = props;
+ const { backgroundColor = theme.colors.background.surface, safeAreaEdges } =
+ props;
const $containerInsets = useSafeAreaInsetsStyle(safeAreaEdges);
return (
-
{isNonScrolling(props.preset) ? (
) : (
diff --git a/components/Screen/ScreenHeaderModalCloseButton.tsx b/components/Screen/ScreenHeaderModalCloseButton.tsx
index 6485dab69..a6fd03a2d 100644
--- a/components/Screen/ScreenHeaderModalCloseButton.tsx
+++ b/components/Screen/ScreenHeaderModalCloseButton.tsx
@@ -4,6 +4,7 @@ import { Platform } from "react-native";
import { ScreenHeaderButton } from "./ScreenHeaderButton/ScreenHeaderButton";
import { IScreenHeaderButtonProps } from "./ScreenHeaderButton/ScreenHeaderButton.props";
import { Optional } from "../../types/general";
+import { translate } from "@/i18n";
export type IScreenHeaderModalCloseButtonProps = Optional<
IScreenHeaderButtonProps,
@@ -21,8 +22,8 @@ export const ScreenHeaderModalCloseButton = memo(
title={
title ??
Platform.select({
- android: "Back",
- default: "Close",
+ android: translate("back"),
+ default: translate("close"),
})
}
{...rest}
diff --git a/components/Screen/ScreenHeaderModalCloseButton/ScreenHeaderModalCloseButton.tsx b/components/Screen/ScreenHeaderModalCloseButton/ScreenHeaderModalCloseButton.tsx
index 467382aaf..3242a9937 100644
--- a/components/Screen/ScreenHeaderModalCloseButton/ScreenHeaderModalCloseButton.tsx
+++ b/components/Screen/ScreenHeaderModalCloseButton/ScreenHeaderModalCloseButton.tsx
@@ -4,6 +4,7 @@ import { Platform } from "react-native";
import { Optional } from "../../../types/general";
import { ScreenHeaderButton } from "../ScreenHeaderButton/ScreenHeaderButton";
import { IScreenHeaderButtonProps } from "../ScreenHeaderButton/ScreenHeaderButton.props";
+import { translate } from "@/i18n";
export type IScreenHeaderModalCloseButtonProps = Optional<
IScreenHeaderButtonProps,
@@ -21,8 +22,8 @@ export const ScreenHeaderModalCloseButton = memo(
title={
title ??
Platform.select({
- android: "Back",
- default: "Close",
+ android: translate("back"),
+ default: translate("close"),
})
}
{...rest}
diff --git a/components/StateHandlers/HydrationStateHandler.tsx b/components/StateHandlers/HydrationStateHandler.tsx
index 420a9dbbb..b884d9f8f 100644
--- a/components/StateHandlers/HydrationStateHandler.tsx
+++ b/components/StateHandlers/HydrationStateHandler.tsx
@@ -1,8 +1,10 @@
+import { prefetchInboxIdQuery } from "@/queries/use-inbox-id-query";
+import { fetchPersistedConversationListQuery } from "@/queries/useV3ConversationListQuery";
import logger from "@utils/logger";
import { useEffect } from "react";
-import { getAccountsList } from "../../data/store/accountsStore";
-import { useAppStore } from "../../data/store/appStore";
-import { getXmtpClient } from "../../utils/xmtpRN/sync";
+import { getAccountsList } from "@data/store/accountsStore";
+import { useAppStore } from "@data/store/appStore";
+import { getXmtpClient } from "@utils/xmtpRN/sync";
import { getInstalledWallets } from "../Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets";
export default function HydrationStateHandler() {
@@ -18,7 +20,41 @@ export default function HydrationStateHandler() {
// note(lustig) I don't think this does anything?
getInstalledWallets(false);
}
- accounts.map((a) => getXmtpClient(a));
+
+ // Fetching persisted conversation lists for all accounts
+ // We may want to fetch only the selected account's conversation list
+ // in the future, but this is simple for now, and want to get feedback to really confirm
+ logger.debug("[Hydration] Fetching persisted conversation list");
+ await Promise.allSettled(
+ accounts.map(async (account) => {
+ const accountStartTime = new Date().getTime();
+ logger.debug(
+ `[Hydration] Fetching persisted conversation list for ${account}`
+ );
+
+ const results = await Promise.allSettled([
+ getXmtpClient(account),
+ fetchPersistedConversationListQuery(account),
+ prefetchInboxIdQuery({ account }),
+ ]);
+
+ const errors = results.filter(
+ (result) => result.status === "rejected"
+ );
+ if (errors.length > 0) {
+ logger.warn(`[Hydration] error for ${account}:`, errors);
+ }
+
+ const accountEndTime = new Date().getTime();
+ logger.debug(
+ `[Hydration] Done fetching persisted conversation list for ${account} in ${
+ (accountEndTime - accountStartTime) / 1000
+ } seconds`
+ );
+ })
+ );
+
+ logger.debug("[Hydration] Done fetching persisted conversation list");
useAppStore.getState().setHydrationDone(true);
logger.debug(
diff --git a/components/V3DMListItem.tsx b/components/V3DMListItem.tsx
index 0fc4f6ac9..571d5b6aa 100644
--- a/components/V3DMListItem.tsx
+++ b/components/V3DMListItem.tsx
@@ -69,7 +69,7 @@ export const V3DMListItem = ({ conversation }: V3DMListItemProps) => {
const isUnread = useConversationIsUnread({
topic,
lastMessage: conversation.lastMessage,
- timestamp,
+ timestampNs: timestamp,
});
const { leftActionIcon } = useDisplayInfo({
diff --git a/components/V3GroupConversationListItem.tsx b/components/V3GroupConversationListItem.tsx
index 210fd6292..c02da6d03 100644
--- a/components/V3GroupConversationListItem.tsx
+++ b/components/V3GroupConversationListItem.tsx
@@ -69,7 +69,7 @@ const useData = ({ group }: UseDataProps) => {
const isUnread = useConversationIsUnread({
topic,
lastMessage: group.lastMessage,
- timestamp,
+ timestampNs: timestamp,
});
const { memberData } = useGroupConversationListAvatarInfo(
diff --git a/data/helpers/conversations/spamScore.ts b/data/helpers/conversations/spamScore.ts
index 05f381cc6..c409eef43 100644
--- a/data/helpers/conversations/spamScore.ts
+++ b/data/helpers/conversations/spamScore.ts
@@ -1,5 +1,8 @@
import { URL_REGEX } from "@utils/regex";
-import { IConvosContentType, isContentType } from "@utils/xmtpRN/contentTypes";
+import {
+ IConvosContentType,
+ isContentType,
+} from "@/utils/xmtpRN/content-types/content-types";
type V3SpameScoreParams = {
message: string;
diff --git a/data/store/chatStore.ts b/data/store/chatStore.ts
index db7e827b9..f00fc90c5 100644
--- a/data/store/chatStore.ts
+++ b/data/store/chatStore.ts
@@ -1,9 +1,8 @@
import logger from "@utils/logger";
+import { RemoteAttachmentContent } from "@xmtp/react-native-sdk";
import isDeepEqual from "fast-deep-equal";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
-
-import { RemoteAttachmentContent } from "@xmtp/react-native-sdk";
import { Nullable } from "../../types/general";
import { zustandMMKVStorage } from "../../utils/mmkv";
@@ -102,13 +101,6 @@ export type TopicsData = {
[topic: string]: TopicData | undefined;
};
-export type MessageAttachmentStatus =
- | "picked"
- | "uploading"
- | "uploaded"
- | "error"
- | "sending";
-
export type MessageAttachmentPreview = {
mediaURI: string;
mimeType: Nullable;
@@ -146,7 +138,6 @@ export type ChatStoreType = {
};
pinnedConversationTopics: string[];
openedConversationTopic: string | null;
- // setOpenedConversationTopic: (topic: string | null) => void;
lastUpdateAt: number;
lastSyncedAt: number;
lastSyncedTopics: string[];
@@ -186,23 +177,11 @@ export type ChatStoreType = {
// period: number
// ) => void;
- messageAttachments: Record;
-
- setMessageAttachment: (
- messageId: string,
- attachment: MessageAttachmentTodo
- ) => void;
-
groupInviteLinks: {
[topic: string]: string;
};
setGroupInviteLink: (topic: string, inviteLink: string) => void;
deleteGroupInviteLink: (topic: string) => void;
-
- reactingToMessage: { topic: string; messageId: string } | null;
- setReactingToMessage: (
- r: { topic: string; messageId: string } | null
- ) => void;
};
const now = () => new Date().getTime();
@@ -219,34 +198,6 @@ export const initChatStore = (account: string) => {
topicsData: {},
topicsDataFetchedOnce: false,
openedConversationTopic: "",
- // setOpenedConversationTopic: (topic) =>
- // set((state) => {
- // const newState = { ...state, openedConversationTopic: topic };
- // if (topic && newState.conversations[topic]) {
- // const conversation = newState.conversations[topic];
- // const lastMessageId =
- // conversation.messagesIds.length > 0
- // ? conversation.messagesIds[
- // conversation.messagesIds.length - 1
- // ]
- // : undefined;
- // if (lastMessageId) {
- // const lastMessage = conversation.messages.get(lastMessageId);
- // if (lastMessage) {
- // const newData = {
- // status: "read",
- // readUntil: lastMessage.sent,
- // timestamp: now(),
- // } as TopicData;
- // newState.topicsData[topic] = newData;
- // saveTopicsData(account, {
- // [topic]: newData,
- // });
- // }
- // }
- // }
- // return newState;
- // }),
conversationsMapping: {},
conversationsSortedOnce: false,
lastUpdateAt: 0,
@@ -369,14 +320,6 @@ export const initChatStore = (account: string) => {
// });
// return { conversations: newConversations };
// }),
- messageAttachments: {},
- setMessageAttachment(messageId, attachment) {
- set((state) => {
- const newMessageAttachments = { ...state.messageAttachments };
- newMessageAttachments[messageId] = attachment;
- return { messageAttachments: newMessageAttachments };
- });
- },
groupInviteLinks: {},
setGroupInviteLink(topic, inviteLink) {
set((state) => {
@@ -392,10 +335,6 @@ export const initChatStore = (account: string) => {
return { groupInviteLinks: newGroupInvites };
});
},
- reactingToMessage: null,
- setReactingToMessage: (
- r: { topic: string; messageId: string } | null
- ) => set(() => ({ reactingToMessage: r })),
}) as ChatStoreType,
{
name: `store-${account}-chat`, // Account-based storage so each account can have its own chat data
diff --git a/design-system/staggered-animation.tsx b/design-system/staggered-animation.tsx
new file mode 100644
index 000000000..08e84969f
--- /dev/null
+++ b/design-system/staggered-animation.tsx
@@ -0,0 +1,52 @@
+import { useAppTheme } from "@/theme/useAppTheme";
+import { EntryOrExitLayoutType } from "@/utils/react-native-reanimated";
+import { PropsWithChildren } from "react";
+import { ViewStyle } from "react-native";
+import { AnimatedVStack } from "./VStack";
+
+type IStaggeredAnimationProps = PropsWithChildren<{
+ index: number;
+ totalItems: number;
+ delayBetweenItems?: number;
+ baseDelay?: number;
+ isReverse?: boolean;
+ getEnteringAnimation?: (args: { delay: number }) => EntryOrExitLayoutType;
+ getExitingAnimation?: (args: { delay: number }) => EntryOrExitLayoutType;
+ style?: ViewStyle;
+}>;
+
+export function StaggeredAnimation(args: IStaggeredAnimationProps) {
+ const { theme } = useAppTheme();
+
+ const {
+ children,
+ index,
+ totalItems,
+ delayBetweenItems = 60,
+ baseDelay = 100,
+ style,
+ isReverse = false,
+ } = args;
+
+ const delay = isReverse
+ ? (totalItems - index) * delayBetweenItems + baseDelay
+ : index * delayBetweenItems + baseDelay;
+
+ const {
+ getEnteringAnimation = (args: { delay: number }) =>
+ theme.animation.reanimatedFadeInScaleIn({
+ delay,
+ }),
+ getExitingAnimation,
+ } = args;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/design-system/touchable-without-feedback.tsx b/design-system/touchable-without-feedback.tsx
new file mode 100644
index 000000000..f8fbc3058
--- /dev/null
+++ b/design-system/touchable-without-feedback.tsx
@@ -0,0 +1,11 @@
+import { memo } from "react";
+import {
+ TouchableWithoutFeedback as RNTouchableWithoutFeedback,
+ TouchableWithoutFeedbackProps,
+} from "react-native";
+
+export const TouchableWithoutFeedback = memo(function TouchableWithoutFeedback(
+ props: TouchableWithoutFeedbackProps
+) {
+ return ;
+});
diff --git a/features/conversation-list/hooks/useMessageIsUnread.ts b/features/conversation-list/hooks/useMessageIsUnread.ts
index 11b337a56..ea058fad8 100644
--- a/features/conversation-list/hooks/useMessageIsUnread.ts
+++ b/features/conversation-list/hooks/useMessageIsUnread.ts
@@ -4,12 +4,12 @@ import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
import { useChatStore, useCurrentAccount } from "@data/store/accountsStore";
import { useSelect } from "@data/store/storeHelpers";
import { normalizeTimestamp } from "@/utils/date";
-import { getCurrentUserAccountInboxId } from "@/components/Chat/Message/message-utils";
+import { getCurrentUserAccountInboxId } from "@/hooks/use-current-account-inbox-id";
type UseConversationIsUnreadProps = {
topic: string;
lastMessage: DecodedMessageWithCodecsType | undefined;
- timestamp: number;
+ timestampNs: number;
};
const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"];
@@ -17,7 +17,7 @@ const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"];
export const useConversationIsUnread = ({
topic,
lastMessage,
- timestamp: timestampNs,
+ timestampNs,
}: UseConversationIsUnreadProps) => {
const { topicsData } = useChatStore(useSelect(chatStoreSelectKeys));
const currentInboxId = getCurrentUserAccountInboxId();
diff --git a/features/conversation-list/hooks/useMessageText.ts b/features/conversation-list/hooks/useMessageText.ts
index 77e3151ff..8a1d7ec80 100644
--- a/features/conversation-list/hooks/useMessageText.ts
+++ b/features/conversation-list/hooks/useMessageText.ts
@@ -1,10 +1,10 @@
import {
isReplyMessage,
isStaticAttachmentMessage,
-} from "@/components/Chat/Message/message-utils";
+} from "@/features/conversation/conversation-message/conversation-message.utils";
import logger from "@utils/logger";
import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
-import { getMessageContentType } from "@utils/xmtpRN/contentTypes";
+import { getMessageContentType } from "@/utils/xmtpRN/content-types/content-types";
import { DecodedMessage, ReplyCodec } from "@xmtp/react-native-sdk";
import { useMemo } from "react";
diff --git a/features/conversation-list/hooks/useToggleReadStatus.ts b/features/conversation-list/hooks/useToggleReadStatus.ts
index f59e3119d..c74619f75 100644
--- a/features/conversation-list/hooks/useToggleReadStatus.ts
+++ b/features/conversation-list/hooks/useToggleReadStatus.ts
@@ -1,10 +1,11 @@
import { useChatStore } from "@/data/store/accountsStore";
import { useSelect } from "@/data/store/storeHelpers";
import { saveTopicsData } from "@utils/api";
+import { ConversationTopic } from "@xmtp/react-native-sdk";
import { useCallback } from "react";
type UseToggleReadStatusProps = {
- topic: string;
+ topic: ConversationTopic;
isUnread: boolean;
currentAccount: string;
};
diff --git a/features/conversation-list/useGroupConversationListAvatarInfo.ts b/features/conversation-list/useGroupConversationListAvatarInfo.ts
index 30d981037..23a6f015a 100644
--- a/features/conversation-list/useGroupConversationListAvatarInfo.ts
+++ b/features/conversation-list/useGroupConversationListAvatarInfo.ts
@@ -5,7 +5,7 @@ import {
getPreferredInboxName,
} from "@utils/profile";
import { GroupWithCodecsType } from "@utils/xmtpRN/client";
-import { InboxId, Member } from "@xmtp/react-native-sdk";
+import type { InboxId, Member } from "@xmtp/react-native-sdk";
import { useEffect, useMemo, useState } from "react";
export const useGroupConversationListAvatarInfo = (
diff --git a/features/conversation-list/useV3ConversationItems.ts b/features/conversation-list/useV3ConversationItems.ts
index af0f56144..ec4592b18 100644
--- a/features/conversation-list/useV3ConversationItems.ts
+++ b/features/conversation-list/useV3ConversationItems.ts
@@ -1,3 +1,4 @@
+import { isConversationAllowed } from "@/features/conversation/utils/is-conversation-allowed";
import { useChatStore, useCurrentAccount } from "@data/store/accountsStore";
import { useSelect } from "@data/store/storeHelpers";
import { useV3ConversationListQuery } from "@queries/useV3ConversationListQuery";
@@ -28,7 +29,7 @@ export const useV3ConversationItems = () => {
);
return conversations?.filter((conversation) => {
- const isAllowed = conversation.state === "allowed";
+ const isAllowed = isConversationAllowed(conversation);
const isNotPinned = !pinnedTopics.has(conversation.topic);
const isNotDeleted = !deletedTopics.has(conversation.topic);
diff --git a/features/conversation-requests-list/useRequestItems.tsx b/features/conversation-requests-list/useRequestItems.tsx
index 9e73a6ca3..ac8dde9b6 100644
--- a/features/conversation-requests-list/useRequestItems.tsx
+++ b/features/conversation-requests-list/useRequestItems.tsx
@@ -1,10 +1,13 @@
import { useV3RequestItems } from "./useV3RequestItems";
export const useRequestItems = () => {
- const { likelyNotSpam, likelySpam } = useV3RequestItems();
+ const { likelyNotSpam, likelySpam, isRefetching, refetch } =
+ useV3RequestItems();
return {
likelyNotSpam,
likelySpam,
+ isRefetching,
+ refetch,
};
};
diff --git a/features/conversation-requests-list/useV3RequestItems.tsx b/features/conversation-requests-list/useV3RequestItems.tsx
index c7e062b4b..ee32e02c3 100644
--- a/features/conversation-requests-list/useV3RequestItems.tsx
+++ b/features/conversation-requests-list/useV3RequestItems.tsx
@@ -1,5 +1,5 @@
import logger from "@/utils/logger";
-import { getMessageContentType } from "@/utils/xmtpRN/contentTypes";
+import { getMessageContentType } from "@/utils/xmtpRN/content-types/content-types";
import { getV3SpamScore } from "@data/helpers/conversations/spamScore";
import { useCurrentAccount } from "@data/store/accountsStore";
import { useV3ConversationListQuery } from "@queries/useV3ConversationListQuery";
diff --git a/features/conversation/composer/composer.tsx b/features/conversation/composer/composer.tsx
deleted file mode 100644
index 014245d05..000000000
--- a/features/conversation/composer/composer.tsx
+++ /dev/null
@@ -1,611 +0,0 @@
-import { RemoteAttachmentImage } from "@/components/Chat/Attachment/remote-attachment-image";
-import {
- isCoinbasePaymentMessage,
- isGroupUpdatedMessage,
- isReactionMessage,
- isReadReceiptMessage,
- isRemoteAttachmentMessage,
- isReplyMessage,
- isStaticAttachmentMessage,
- isTransactionReferenceMessage,
- useCurrentAccountInboxId,
-} from "@/components/Chat/Message/message-utils";
-import { SendAttachmentPreview } from "@/features/conversation/composer/send-attachment-preview";
-import { HStack } from "@design-system/HStack";
-import { Icon } from "@design-system/Icon/Icon";
-import { IconButton } from "@design-system/IconButton/IconButton";
-import { Text } from "@design-system/Text";
-import { textSizeStyles } from "@design-system/Text/Text.styles";
-import { AnimatedVStack, VStack } from "@design-system/VStack";
-import { getConversationMessages } from "@queries/useConversationMessages";
-import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
-import { useAppTheme } from "@theme/useAppTheme";
-import { Haptics } from "@utils/haptics";
-import { sentryTrackError } from "@utils/sentry";
-import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
-import {
- DecodedMessage,
- InboxId,
- MessageId,
- RemoteAttachmentCodec,
- RemoteAttachmentContent,
- ReplyCodec,
- StaticAttachmentCodec,
-} from "@xmtp/react-native-sdk";
-import React, { memo, useCallback, useEffect, useRef } from "react";
-import { Platform, TextInput as RNTextInput } from "react-native";
-import {
- useAnimatedStyle,
- useSharedValue,
- withSpring,
-} from "react-native-reanimated";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useCurrentAccount } from "../../../data/store/accountsStore";
-import { getReadableProfile } from "../../../utils/str";
-import { useMessageText } from "../../conversation-list/hooks/useMessageText";
-import { useCurrentConversationPersistedStoreState } from "../conversation-persisted-stores";
-import {
- getComposerMediaPreview,
- getCurrentConversationInputValue,
- getCurrentConversationReplyToMessageId,
- getUploadedRemoteAttachment,
- listenToComposerInputValueChange,
- resetComposerMediaPreview,
- resetUploadedRemoteAttachment,
- saveAttachmentLocally,
- setComposerMediaPreview,
- setComposerMediaPreviewStatus,
- setCurrentConversationInputValue,
- setCurrentConversationReplyToMessageId,
- useConversationComposerMediaPreview,
- useConversationCurrentTopic,
- useCurrentConversationInputValue,
- useReplyToMessageId,
- waitUntilMediaPreviewIsUploaded,
-} from "../conversation-service";
-import { AddAttachmentButton } from "./add-attachment-button";
-import { ISendMessageParams } from "@/features/conversation/conversation-context";
-import { usePreferredInboxName } from "@/hooks/usePreferredInboxName";
-
-export type IComposerSendArgs = ISendMessageParams;
-
-type IComposerProps = {
- onSend: (args: IComposerSendArgs) => Promise;
-};
-
-export function Composer(props: IComposerProps) {
- const { onSend } = props;
-
- const { theme } = useAppTheme();
-
- const send = useCallback(async () => {
- const mediaPreview = getComposerMediaPreview();
-
- const replyingToMessageId = getCurrentConversationReplyToMessageId();
-
- if (mediaPreview) {
- if (mediaPreview?.status === "uploading") {
- await waitUntilMediaPreviewIsUploaded();
- }
-
- setComposerMediaPreviewStatus("sending");
-
- try {
- await saveAttachmentLocally();
- } catch (error) {
- sentryTrackError(error);
- }
-
- const uploadedRemoteAttachment = getUploadedRemoteAttachment()!;
-
- await onSend({
- content: {
- remoteAttachment: uploadedRemoteAttachment,
- },
- ...(replyingToMessageId && {
- referencedMessageId: replyingToMessageId,
- }),
- });
-
- resetUploadedRemoteAttachment();
- resetComposerMediaPreview();
- }
-
- const inputValue = getCurrentConversationInputValue();
-
- if (inputValue.length > 0) {
- await onSend({
- content: {
- text: inputValue,
- },
- ...(replyingToMessageId && {
- referencedMessageId: replyingToMessageId,
- }),
- });
- }
-
- // Reset stuff
- setCurrentConversationInputValue("");
- setCurrentConversationReplyToMessageId(null);
-
- // TODO: Fix with function in context
- // converseEventEmitter.emit("scrollChatToMessage", {
- // index: 0,
- // });
- }, [onSend]);
-
- const insets = useSafeAreaInsets();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-const SendButton = memo(function SendButton(props: { onPress: () => void }) {
- const { onPress } = props;
-
- const { theme } = useAppTheme();
-
- const mediaPreview = useConversationComposerMediaPreview();
-
- const composerInputValue = useCurrentConversationInputValue();
-
- const canSend =
- composerInputValue.length > 0 || mediaPreview?.status === "uploaded";
-
- return (
-
-
-
- );
-});
-
-const ReplyPreview = memo(function ReplyPreview() {
- const { theme } = useAppTheme();
-
- const replyingToMessageId = useReplyToMessageId();
- const currentAccount = useCurrentAccount()!;
- const { data: currentAccountInboxId } = useCurrentAccountInboxId();
- const topic = useConversationCurrentTopic();
-
- const replyMessage = replyingToMessageId
- ? getConversationMessages(currentAccount, topic!)?.byId[replyingToMessageId]
- : undefined;
-
- const inboxName = usePreferredInboxName(
- replyMessage?.senderAddress as InboxId
- );
-
- const replyingTo = replyMessage
- ? replyMessage.senderAddress === currentAccountInboxId
- ? `Replying to you`
- : inboxName
- ? `Replying to ${inboxName}`
- : "Replying"
- : "";
-
- const contentHeightAV = useSharedValue(0);
-
- const containerAS = useAnimatedStyle(() => {
- return {
- height: withSpring(
- replyingToMessageId && contentHeightAV.value !== 0
- ? contentHeightAV.value
- : 0,
- { damping: SICK_DAMPING, stiffness: SICK_STIFFNESS }
- ),
- };
- }, [replyingToMessageId]);
-
- useEffect(() => {
- if (replyingToMessageId) {
- Haptics.softImpactAsync();
- }
- }, [replyingToMessageId]);
-
- const handleDismiss = useCallback(() => {
- setCurrentConversationReplyToMessageId(null);
- }, []);
-
- return (
-
- {!!replyMessage && (
- {
- contentHeightAV.value = event.nativeEvent.layout.height;
- }}
- style={{
- borderTopWidth: theme.borderWidth.xs,
- borderTopColor: theme.colors.border.subtle,
- paddingLeft: theme.spacing.sm,
- paddingRight: theme.spacing.sm,
- paddingTop: theme.spacing.sm,
- paddingBottom: theme.spacing.xxxs,
- backgroundColor: theme.colors.background.surface,
- minHeight: replyMessage
- ? 56 // Value from Figma. Not the best but we need minHeight for this to work. If the content end up being bigger it will adjust automatically
- : 0,
- }}
- >
-
-
-
-
-
- {replyingTo}
-
-
- {!!replyMessage && (
-
- )}
-
-
-
-
-
- )}
-
- );
-});
-
-export const ReplyPreviewEndContent = memo(
- function ReplyPreviewEndContent(props: {
- replyMessage: DecodedMessageWithCodecsType;
- }) {
- const { replyMessage } = props;
-
- const { theme } = useAppTheme();
-
- if (isReplyMessage(replyMessage)) {
- const replyTyped = replyMessage as DecodedMessage;
-
- const content = replyTyped.content();
-
- if (typeof content === "string") {
- return null;
- }
-
- if (content.content.remoteAttachment) {
- return (
-
- );
- }
- }
-
- if (isRemoteAttachmentMessage(replyMessage)) {
- const messageTyped =
- replyMessage as DecodedMessage;
-
- const content = messageTyped.content();
-
- if (typeof content === "string") {
- return null;
- }
-
- return (
-
- );
- }
-
- return null;
- }
-);
-
-const ReplyPreviewMessageContent = memo(
- function ReplyPreviewMessageContent(props: {
- replyMessage: DecodedMessageWithCodecsType;
- }) {
- const { replyMessage } = props;
-
- const { theme } = useAppTheme();
-
- const messageText = useMessageText(replyMessage);
- const clearedMessage = messageText?.replace(/(\n)/gm, " ");
-
- if (isStaticAttachmentMessage(replyMessage)) {
- const messageTyped =
- replyMessage as DecodedMessage;
-
- const content = messageTyped.content();
-
- if (typeof content === "string") {
- return {content};
- }
-
- // TODO
- return Static attachment;
- }
-
- if (isTransactionReferenceMessage(replyMessage)) {
- return Transaction;
- }
-
- if (isReactionMessage(replyMessage)) {
- return Reaction;
- }
-
- if (isReadReceiptMessage(replyMessage)) {
- return Read Receipt;
- }
-
- if (isGroupUpdatedMessage(replyMessage)) {
- return Group updates;
- }
-
- if (isRemoteAttachmentMessage(replyMessage)) {
- return Remote Attachment;
- }
-
- if (isCoinbasePaymentMessage(replyMessage)) {
- return Coinbase Payment;
- }
-
- if (isReplyMessage(replyMessage)) {
- const messageTyped = replyMessage as DecodedMessage;
- const content = messageTyped.content();
-
- if (typeof content === "string") {
- return {content};
- }
-
- if (content.content.attachment) {
- return Reply with attachment;
- }
-
- if (content.content.text) {
- return {content.content.text};
- }
-
- if (content.content.remoteAttachment) {
- return Image;
- }
-
- return Reply;
- }
-
- return {clearedMessage};
- }
-);
-
-const AttachmentsPreview = memo(function AttachmentsPreview() {
- const { theme } = useAppTheme();
-
- const mediaPreview = useCurrentConversationPersistedStoreState(
- (state) => state.composerMediaPreview
- );
-
- const handleAttachmentClosed = useCallback(() => {
- setComposerMediaPreview(null);
- }, []);
-
- const isLandscape = !!(
- mediaPreview?.dimensions?.height &&
- mediaPreview?.dimensions?.width &&
- mediaPreview.dimensions.width > mediaPreview.dimensions.height
- );
-
- const maxHeight = isLandscape ? 90 : 120;
-
- const containerAS = useAnimatedStyle(() => {
- return {
- height: withSpring(mediaPreview?.mediaURI ? maxHeight : 0, {
- damping: SICK_DAMPING,
- stiffness: SICK_STIFFNESS,
- }),
- };
- }, [mediaPreview?.mediaURI, maxHeight]);
-
- return (
-
- {!!mediaPreview && (
-
- mediaPreview.dimensions.height
- )
- }
- />
-
- )}
-
- );
-});
-
-const ComposerTextInput = memo(function ComposerTextInput(props: {
- onSubmitEditing: () => Promise;
-}) {
- const { onSubmitEditing } = props;
-
- const inputRef = useRef(null);
-
- const { theme } = useAppTheme();
-
- const inputDefaultValue = getCurrentConversationInputValue();
-
- const handleChangeText = useCallback((text: string) => {
- setCurrentConversationInputValue(text);
- }, []);
-
- // If we clear the input (i.e after sending a message)
- // we need to clear the input value in the text input
- // Doing this since we are using a uncontrolled component
- useEffect(() => {
- listenToComposerInputValueChange((value, previousValue) => {
- if (previousValue && !value) {
- inputRef.current?.clear();
- }
- });
- }, []);
-
- const handleSubmitEditing = useCallback(() => {
- onSubmitEditing();
- }, [onSubmitEditing]);
-
- return (
- {
- // Maybe want a better check here, but web/tablet is not the focus right now
- if (Platform.OS !== "web") {
- return;
- }
-
- if (
- event.nativeEvent.key === "Enter" &&
- !event.altKey &&
- !event.metaKey &&
- !event.shiftKey
- ) {
- event.preventDefault();
- onSubmitEditing();
- }
- }}
- ref={inputRef}
- onSubmitEditing={handleSubmitEditing}
- onChangeText={handleChangeText}
- multiline
- defaultValue={inputDefaultValue}
- placeholder="Message"
- placeholderTextColor={theme.colors.text.tertiary}
- />
- );
-});
diff --git a/components/Chat/Attachment/attachment-container.tsx b/features/conversation/conversation-attachment/conversation-attachment-container.tsx
similarity index 100%
rename from components/Chat/Attachment/attachment-container.tsx
rename to features/conversation/conversation-attachment/conversation-attachment-container.tsx
diff --git a/components/Chat/Attachment/attachment-loading.tsx b/features/conversation/conversation-attachment/conversation-attachment-loading.tsx
similarity index 99%
rename from components/Chat/Attachment/attachment-loading.tsx
rename to features/conversation/conversation-attachment/conversation-attachment-loading.tsx
index f73d3581a..3902767f4 100644
--- a/components/Chat/Attachment/attachment-loading.tsx
+++ b/features/conversation/conversation-attachment/conversation-attachment-loading.tsx
@@ -4,6 +4,5 @@ import { memo } from "react";
export const AttachmentLoading = memo(function AttachmentLoading() {
const { theme } = useAppTheme();
-
return ;
});
diff --git a/components/Chat/Attachment/remote-attachment-image.tsx b/features/conversation/conversation-attachment/conversation-attachment-remote-image.tsx
similarity index 94%
rename from components/Chat/Attachment/remote-attachment-image.tsx
rename to features/conversation/conversation-attachment/conversation-attachment-remote-image.tsx
index a090288e8..785f82529 100644
--- a/components/Chat/Attachment/remote-attachment-image.tsx
+++ b/features/conversation/conversation-attachment/conversation-attachment-remote-image.tsx
@@ -1,4 +1,4 @@
-import { AttachmentLoading } from "@/components/Chat/Attachment/attachment-loading";
+import { AttachmentLoading } from "@/features/conversation/conversation-attachment/conversation-attachment-loading";
import { getCurrentAccount } from "@data/store/accountsStore";
import { Icon } from "@design-system/Icon/Icon";
import { Text } from "@design-system/Text";
@@ -18,15 +18,15 @@ import { Image } from "expo-image";
import prettyBytes from "pretty-bytes";
import { memo } from "react";
-type IRemoteAttachmentImageProps = {
+type IAttachmentRemoteImageProps = {
messageId: string;
remoteMessageContent: RemoteAttachmentContent;
fitAspectRatio?: boolean;
containerProps?: IVStackProps;
};
-export const RemoteAttachmentImage = memo(function RemoteAttachmentImage(
- props: IRemoteAttachmentImageProps
+export const AttachmentRemoteImage = memo(function AttachmentRemoteImage(
+ props: IAttachmentRemoteImageProps
) {
const { messageId, remoteMessageContent, fitAspectRatio, containerProps } =
props;
diff --git a/features/conversation/composer/add-attachment-button.tsx b/features/conversation/conversation-composer/conversation-composer-add-attachment-button.tsx
similarity index 56%
rename from features/conversation/composer/add-attachment-button.tsx
rename to features/conversation/conversation-composer/conversation-composer-add-attachment-button.tsx
index 8de0bb26f..1d07bee14 100644
--- a/features/conversation/composer/add-attachment-button.tsx
+++ b/features/conversation/conversation-composer/conversation-composer-add-attachment-button.tsx
@@ -1,3 +1,4 @@
+import { useConversationComposerStore } from "@/features/conversation/conversation-composer/conversation-composer.store-context";
import { getCurrentAccount } from "@data/store/accountsStore";
import { Icon } from "@design-system/Icon/Icon";
import { Pressable } from "@design-system/Pressable";
@@ -12,38 +13,89 @@ import {
} from "@utils/media";
import { sentryTrackError, sentryTrackMessage } from "@utils/sentry";
import { encryptRemoteAttachment } from "@utils/xmtpRN/attachments";
-import { RemoteAttachmentContent } from "@xmtp/react-native-sdk";
import * as ImagePicker from "expo-image-picker";
import { setStatusBarHidden } from "expo-status-bar";
import mime from "mime";
+import { useCallback } from "react";
import { Platform } from "react-native";
-import {
- setComposerMediaPreview,
- setComposerMediaPreviewStatus,
- setUploadedRemoteAttachment,
-} from "../conversation-service";
const DATA_MIMETYPE_REGEX = /data:(.*?);/;
-type AttachmentToSave = {
- filePath: string;
- fileName: string;
- mimeType: string | null;
- dimensions: {
- height: number;
- width: number;
- };
-};
-
-type SelectedAttachment = {
- uploadedAttachment?: RemoteAttachmentContent;
- attachmentToSave?: AttachmentToSave;
- uri?: string;
-};
-
export function AddAttachmentButton() {
const { theme } = useAppTheme();
+ const store = useConversationComposerStore();
+
+ const handleAttachmentSelected = useCallback(
+ async (asset: ImagePicker.ImagePickerAsset) => {
+ try {
+ store.getState().setComposerMediaPreview({
+ mediaURI: asset.uri,
+ status: "picked",
+ mimeType: undefined,
+ dimensions: {
+ height: asset.height,
+ width: asset.width,
+ },
+ });
+
+ store.getState().updateMediaPreviewStatus("uploading");
+
+ const resizedImage = await compressAndResizeImage(asset.uri);
+
+ let mimeType = mime.getType(resizedImage.uri);
+ if (!mimeType) {
+ const match = resizedImage.uri.match(DATA_MIMETYPE_REGEX);
+ if (match && match[1]) {
+ mimeType = match[1];
+ }
+ }
+
+ const currentAccount = getCurrentAccount()!;
+
+ const encryptedAttachment = await encryptRemoteAttachment(
+ currentAccount,
+ resizedImage.uri,
+ mimeType || undefined
+ );
+
+ try {
+ const uploadedAttachment = await uploadRemoteAttachment(
+ currentAccount,
+ encryptedAttachment
+ );
+
+ store.getState().updateMediaPreviewStatus("uploaded");
+
+ store.getState().setComposerUploadedAttachment(uploadedAttachment);
+ } catch (error) {
+ sentryTrackMessage("ATTACHMENT_UPLOAD_ERROR", { error });
+ }
+ } catch (error) {
+ sentryTrackError(error);
+ }
+ },
+ [store]
+ );
+
+ const pickMedia = useCallback(async () => {
+ if (Platform.OS === "ios") {
+ setStatusBarHidden(true, "fade");
+ }
+ const asset = await pickMediaFromLibrary();
+ if (Platform.OS === "ios") {
+ setStatusBarHidden(false, "fade");
+ }
+ if (!asset) return;
+ handleAttachmentSelected(asset);
+ }, [handleAttachmentSelected]);
+
+ const openCamera = useCallback(async () => {
+ const asset = await takePictureFromCamera();
+ if (!asset) return;
+ handleAttachmentSelected(asset);
+ }, [handleAttachmentSelected]);
+
return (
);
}
-
-async function pickMedia() {
- if (Platform.OS === "ios") {
- setStatusBarHidden(true, "fade");
- }
- const asset = await pickMediaFromLibrary();
- if (Platform.OS === "ios") {
- setStatusBarHidden(false, "fade");
- }
- if (!asset) return;
- handleAttachmentSelected(asset);
-}
-
-async function openCamera() {
- const asset = await takePictureFromCamera();
- if (!asset) return;
- handleAttachmentSelected(asset);
-}
-
-async function handleAttachmentSelected(asset: ImagePicker.ImagePickerAsset) {
- if (asset) {
- try {
- setComposerMediaPreview({
- mediaURI: asset.uri,
- status: "picked",
- mimeType: null,
- dimensions: {
- height: asset.height,
- width: asset.width,
- },
- });
-
- setComposerMediaPreviewStatus("uploading");
-
- const resizedImage = await compressAndResizeImage(asset.uri);
-
- let mimeType = mime.getType(resizedImage.uri);
- if (!mimeType) {
- const match = resizedImage.uri.match(DATA_MIMETYPE_REGEX);
- if (match && match[1]) {
- mimeType = match[1];
- }
- }
-
- const currentAccount = getCurrentAccount()!;
-
- const encryptedAttachment = await encryptRemoteAttachment(
- currentAccount,
- resizedImage.uri,
- mimeType || undefined
- );
-
- try {
- const uploadedAttachment = await uploadRemoteAttachment(
- currentAccount,
- encryptedAttachment
- );
-
- setUploadedRemoteAttachment(uploadedAttachment);
- setComposerMediaPreviewStatus("uploaded");
- } catch (error) {
- sentryTrackMessage("ATTACHMENT_UPLOAD_ERROR", { error });
- }
- } catch (error) {
- sentryTrackError(error);
- }
- }
-}
diff --git a/features/conversation/conversation-composer/conversation-composer-reply-preview.tsx b/features/conversation/conversation-composer/conversation-composer-reply-preview.tsx
new file mode 100644
index 000000000..6ecff3643
--- /dev/null
+++ b/features/conversation/conversation-composer/conversation-composer-reply-preview.tsx
@@ -0,0 +1,319 @@
+import { AttachmentRemoteImage } from "@/features/conversation/conversation-attachment/conversation-attachment-remote-image";
+import {
+ isCoinbasePaymentMessage,
+ isGroupUpdatedMessage,
+ isReactionMessage,
+ isReadReceiptMessage,
+ isRemoteAttachmentMessage,
+ isReplyMessage,
+ isStaticAttachmentMessage,
+ isTransactionReferenceMessage,
+ useConversationMessageById,
+} from "@/features/conversation/conversation-message/conversation-message.utils";
+import { useCurrentAccountInboxId } from "@/hooks/use-current-account-inbox-id";
+import { useCurrentConversationTopic } from "../conversation.store-context";
+import { usePreferredInboxName } from "@/hooks/usePreferredInboxName";
+import { HStack } from "@design-system/HStack";
+import { Icon } from "@design-system/Icon/Icon";
+import { IconButton } from "@design-system/IconButton/IconButton";
+import { Text } from "@design-system/Text";
+import { AnimatedVStack, VStack } from "@design-system/VStack";
+import { useMessageText } from "@features/conversation-list/hooks/useMessageText";
+import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
+import { useAppTheme } from "@theme/useAppTheme";
+import { Haptics } from "@utils/haptics";
+import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
+import {
+ DecodedMessage,
+ InboxId,
+ MessageId,
+ RemoteAttachmentCodec,
+ ReplyCodec,
+ StaticAttachmentCodec,
+} from "@xmtp/react-native-sdk";
+import { memo, useCallback, useEffect } from "react";
+import {
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from "react-native-reanimated";
+import {
+ useConversationComposerStore,
+ useConversationComposerStoreContext,
+} from "./conversation-composer.store-context";
+
+export const ReplyPreview = memo(function ReplyPreview() {
+ const replyingToMessageId = useConversationComposerStoreContext(
+ (state) => state.replyingToMessageId
+ );
+
+ if (!replyingToMessageId) {
+ return null;
+ }
+
+ return ;
+});
+
+const Content = memo(function Content(props: {
+ replyingToMessageId: MessageId;
+}) {
+ const { replyingToMessageId } = props;
+
+ const { theme } = useAppTheme();
+
+ const composerStore = useConversationComposerStore();
+
+ const { data: currentAccountInboxId } = useCurrentAccountInboxId();
+ const topic = useCurrentConversationTopic();
+
+ const { message: replyMessage } = useConversationMessageById({
+ messageId: replyingToMessageId,
+ topic,
+ });
+
+ const inboxName = usePreferredInboxName(
+ replyMessage?.senderAddress as InboxId
+ );
+
+ const replyingTo = replyMessage
+ ? replyMessage.senderAddress === currentAccountInboxId
+ ? `Replying to you`
+ : inboxName
+ ? `Replying to ${inboxName}`
+ : "Replying"
+ : "";
+
+ const contentHeightAV = useSharedValue(0);
+
+ const containerAS = useAnimatedStyle(() => {
+ return {
+ height: withSpring(
+ replyingToMessageId && contentHeightAV.value !== 0
+ ? contentHeightAV.value
+ : 0,
+ { damping: SICK_DAMPING, stiffness: SICK_STIFFNESS }
+ ),
+ };
+ }, [replyingToMessageId]);
+
+ useEffect(() => {
+ if (replyingToMessageId) {
+ Haptics.softImpactAsync();
+ }
+ }, [replyingToMessageId]);
+
+ const handleDismiss = useCallback(() => {
+ composerStore.getState().setReplyToMessageId(null);
+ }, [composerStore]);
+
+ return (
+
+ {!!replyMessage && (
+ {
+ contentHeightAV.value = event.nativeEvent.layout.height;
+ }}
+ style={{
+ borderTopWidth: theme.borderWidth.xs,
+ borderTopColor: theme.colors.border.subtle,
+ paddingLeft: theme.spacing.sm,
+ paddingRight: theme.spacing.sm,
+ paddingTop: theme.spacing.sm,
+ paddingBottom: theme.spacing.xxxs,
+ backgroundColor: theme.colors.background.surface,
+ minHeight: replyMessage
+ ? 56 // Value from Figma. Not the best but we need minHeight for this to work. If the content end up being bigger it will adjust automatically
+ : 0,
+ }}
+ >
+
+
+
+
+
+ {replyingTo}
+
+
+ {!!replyMessage && (
+
+ )}
+
+
+
+
+
+ )}
+
+ );
+});
+
+const ReplyPreviewEndContent = memo(function ReplyPreviewEndContent(props: {
+ replyMessage: DecodedMessageWithCodecsType;
+}) {
+ const { replyMessage } = props;
+
+ const { theme } = useAppTheme();
+
+ if (isReplyMessage(replyMessage)) {
+ const replyTyped = replyMessage as DecodedMessage;
+
+ const content = replyTyped.content();
+
+ if (typeof content === "string") {
+ return null;
+ }
+
+ if (content.content.remoteAttachment) {
+ return (
+
+ );
+ }
+ }
+
+ if (isRemoteAttachmentMessage(replyMessage)) {
+ const messageTyped = replyMessage as DecodedMessage;
+
+ const content = messageTyped.content();
+
+ if (typeof content === "string") {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+
+ return null;
+});
+
+const ReplyPreviewMessageContent = memo(
+ function ReplyPreviewMessageContent(props: {
+ replyMessage: DecodedMessageWithCodecsType;
+ }) {
+ const { replyMessage } = props;
+
+ const { theme } = useAppTheme();
+
+ const messageText = useMessageText(replyMessage);
+ const clearedMessage = messageText?.replace(/(\n)/gm, " ");
+
+ if (isStaticAttachmentMessage(replyMessage)) {
+ const messageTyped =
+ replyMessage as DecodedMessage;
+
+ const content = messageTyped.content();
+
+ if (typeof content === "string") {
+ return {content};
+ }
+
+ // TODO
+ return Static attachment;
+ }
+
+ if (isTransactionReferenceMessage(replyMessage)) {
+ return Transaction;
+ }
+
+ if (isReactionMessage(replyMessage)) {
+ return Reaction;
+ }
+
+ if (isReadReceiptMessage(replyMessage)) {
+ return Read Receipt;
+ }
+
+ if (isGroupUpdatedMessage(replyMessage)) {
+ return Group updates;
+ }
+
+ if (isRemoteAttachmentMessage(replyMessage)) {
+ return Remote Attachment;
+ }
+
+ if (isCoinbasePaymentMessage(replyMessage)) {
+ return Coinbase Payment;
+ }
+
+ if (isReplyMessage(replyMessage)) {
+ const messageTyped = replyMessage as DecodedMessage;
+ const content = messageTyped.content();
+
+ if (typeof content === "string") {
+ return {content};
+ }
+
+ if (content.content.attachment) {
+ return Reply with attachment;
+ }
+
+ if (content.content.text) {
+ return {content.content.text};
+ }
+
+ if (content.content.remoteAttachment) {
+ return Image;
+ }
+
+ return Reply;
+ }
+
+ return {clearedMessage};
+ }
+);
diff --git a/features/conversation/composer/send-attachment-preview.tsx b/features/conversation/conversation-composer/conversation-composer-send-attachment-preview.tsx
similarity index 100%
rename from features/conversation/composer/send-attachment-preview.tsx
rename to features/conversation/conversation-composer/conversation-composer-send-attachment-preview.tsx
diff --git a/features/conversation/conversation-composer/conversation-composer.store-context.tsx b/features/conversation/conversation-composer/conversation-composer.store-context.tsx
new file mode 100644
index 000000000..e0a86435f
--- /dev/null
+++ b/features/conversation/conversation-composer/conversation-composer.store-context.tsx
@@ -0,0 +1,142 @@
+import { LocalAttachment } from "@/utils/attachment/types";
+import { zustandMMKVStorage } from "@utils/mmkv";
+import { MessageId, RemoteAttachmentContent } from "@xmtp/react-native-sdk";
+import { createContext, memo, useContext, useRef } from "react";
+import { createStore, useStore } from "zustand";
+import {
+ createJSONStorage,
+ persist,
+ subscribeWithSelector,
+} from "zustand/middleware";
+
+export type IComposerMediaPreviewStatus =
+ | "picked"
+ | "uploading"
+ | "uploaded"
+ | "error"
+ | "sending";
+
+// TODO: Maybe move in attachments and make it more generic? (without that status)
+export type IComposerMediaPreview =
+ | (LocalAttachment & {
+ status: IComposerMediaPreviewStatus;
+ })
+ | null;
+
+type IConversationComposerStoreProps = {
+ storeName: string;
+ inputValue?: string;
+};
+
+type IConversationComposerState = IConversationComposerStoreProps & {
+ inputValue: string;
+ storeName: string;
+ replyingToMessageId: MessageId | null;
+ composerMediaPreview: IComposerMediaPreview;
+ composerUploadedAttachment: RemoteAttachmentContent | null;
+};
+
+type IConversationComposerActions = {
+ reset: () => void;
+ setInputValue: (value: string) => void;
+ setReplyToMessageId: (messageId: MessageId | null) => void;
+ setComposerMediaPreview: (mediaPreview: IComposerMediaPreview) => void;
+ setComposerUploadedAttachment: (
+ attachment: RemoteAttachmentContent | null
+ ) => void;
+ updateMediaPreviewStatus: (status: IComposerMediaPreviewStatus) => void;
+};
+
+type IConversationComposerStoreState = IConversationComposerState &
+ IConversationComposerActions;
+
+type IConversationComposerStoreProviderProps =
+ React.PropsWithChildren;
+
+type IConversationComposerStore = ReturnType<
+ typeof createConversationComposerStore
+>;
+
+export const ConversationComposerStoreProvider = memo(
+ ({ children, ...props }: IConversationComposerStoreProviderProps) => {
+ const storeRef = useRef();
+ if (!storeRef.current) {
+ storeRef.current = createConversationComposerStore(props);
+ }
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+const createConversationComposerStore = (
+ initProps: IConversationComposerStoreProps
+) => {
+ const DEFAULT_STATE: IConversationComposerState = {
+ storeName: initProps.storeName,
+ inputValue: initProps.inputValue ?? "",
+ composerMediaPreview: null,
+ composerUploadedAttachment: null,
+ replyingToMessageId: null,
+ };
+
+ return createStore()(
+ subscribeWithSelector(
+ persist(
+ (set) => ({
+ ...DEFAULT_STATE,
+ reset: () =>
+ set((state) => ({
+ ...state,
+ ...DEFAULT_STATE,
+ })),
+ setInputValue: (value) => set({ inputValue: value }),
+ setReplyToMessageId: (messageId) =>
+ set({ replyingToMessageId: messageId }),
+ setComposerMediaPreview: (mediaPreview) =>
+ set({ composerMediaPreview: mediaPreview }),
+ setComposerUploadedAttachment: (attachment) =>
+ set({ composerUploadedAttachment: attachment }),
+ updateMediaPreviewStatus: (status) =>
+ set((state) => ({
+ ...state,
+ composerMediaPreview: {
+ ...state.composerMediaPreview!,
+ status,
+ },
+ })),
+ }),
+ {
+ storage: createJSONStorage(() => zustandMMKVStorage),
+ name: initProps.storeName,
+ partialize: (state) => ({
+ inputValue: state.inputValue,
+ replyingToMessageId: state.replyingToMessageId,
+ composerMediaPreview: state.composerMediaPreview,
+ composerUploadedAttachment: state.composerUploadedAttachment,
+ }),
+ }
+ )
+ )
+ );
+};
+
+const ConversationComposerStoreContext =
+ createContext(null);
+
+export function useConversationComposerStoreContext(
+ selector: (state: IConversationComposerStoreState) => T
+): T {
+ const store = useContext(ConversationComposerStoreContext);
+ if (!store)
+ throw new Error("Missing ConversationComposerStore.Provider in the tree");
+ return useStore(store, selector);
+}
+
+export function useConversationComposerStore() {
+ const store = useContext(ConversationComposerStoreContext);
+ if (!store) throw new Error();
+ return store;
+}
diff --git a/features/conversation/conversation-composer/conversation-composer.tsx b/features/conversation/conversation-composer/conversation-composer.tsx
new file mode 100644
index 000000000..aa5259e34
--- /dev/null
+++ b/features/conversation/conversation-composer/conversation-composer.tsx
@@ -0,0 +1,341 @@
+import { SendAttachmentPreview } from "@/features/conversation/conversation-composer/conversation-composer-send-attachment-preview";
+import { ISendMessageParams } from "@/features/conversation/hooks/use-send-message";
+import { saveAttachmentLocally } from "@/utils/attachment/attachment.utils";
+import { HStack } from "@design-system/HStack";
+import { IconButton } from "@design-system/IconButton/IconButton";
+import { textSizeStyles } from "@design-system/Text/Text.styles";
+import { AnimatedVStack, VStack } from "@design-system/VStack";
+import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
+import { useAppTheme } from "@theme/useAppTheme";
+import { sentryTrackError } from "@utils/sentry";
+import React, { memo, useCallback, useEffect, useRef } from "react";
+import { Platform, TextInput as RNTextInput } from "react-native";
+import { useAnimatedStyle, withSpring } from "react-native-reanimated";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { AddAttachmentButton } from "./conversation-composer-add-attachment-button";
+import { ReplyPreview } from "./conversation-composer-reply-preview";
+import {
+ useConversationComposerStore,
+ useConversationComposerStoreContext,
+} from "./conversation-composer.store-context";
+
+type IComposerProps = {
+ onSend: (args: ISendMessageParams) => Promise;
+};
+
+export const Composer = memo(function Composer(props: IComposerProps) {
+ const { onSend } = props;
+
+ const { theme } = useAppTheme();
+ const store = useConversationComposerStore();
+
+ const send = useCallback(async () => {
+ const mediaPreview = store.getState().composerMediaPreview;
+ const replyingToMessageId = store.getState().replyingToMessageId;
+
+ function waitUntilMediaPreviewIsUploaded() {
+ return new Promise((resolve, reject) => {
+ const startTime = Date.now();
+ const checkStatus = () => {
+ if (!mediaPreview?.status) {
+ resolve(true);
+ return;
+ }
+ if (mediaPreview.status === "uploaded") {
+ resolve(true);
+ return;
+ }
+ if (Date.now() - startTime > 10000) {
+ reject(new Error("Media upload timeout after 10 seconds"));
+ return;
+ }
+ setTimeout(checkStatus, 200);
+ };
+ checkStatus();
+ });
+ }
+
+ if (mediaPreview) {
+ if (mediaPreview?.status === "uploading") {
+ await waitUntilMediaPreviewIsUploaded();
+ }
+
+ store.getState().updateMediaPreviewStatus("sending");
+
+ try {
+ const mediaPreview = store.getState().composerMediaPreview;
+ if (mediaPreview) {
+ await saveAttachmentLocally(mediaPreview);
+ }
+ } catch (error) {
+ sentryTrackError(error);
+ }
+
+ const uploadedRemoteAttachment =
+ store.getState().composerUploadedAttachment;
+
+ if (!uploadedRemoteAttachment) {
+ throw new Error("Something went wrong while uploading attachment");
+ }
+
+ store.getState().setComposerMediaPreview(null);
+ store.getState().setComposerUploadedAttachment(null);
+
+ await onSend({
+ content: {
+ remoteAttachment: uploadedRemoteAttachment,
+ },
+ ...(replyingToMessageId && {
+ referencedMessageId: replyingToMessageId,
+ }),
+ });
+ }
+
+ const inputValue = store.getState().inputValue;
+
+ store.getState().reset();
+
+ if (inputValue.length > 0) {
+ await onSend({
+ content: {
+ text: inputValue,
+ },
+ ...(replyingToMessageId && {
+ referencedMessageId: replyingToMessageId,
+ }),
+ });
+ }
+
+ // TODO: Fix with function in context
+ // converseEventEmitter.emit("scrollChatToMessage", {
+ // index: 0,
+ // });
+ }, [onSend, store]);
+
+ const insets = useSafeAreaInsets();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+const SendButton = memo(function SendButton(props: { onPress: () => void }) {
+ const { onPress } = props;
+
+ const { theme } = useAppTheme();
+
+ const mediaPreview = useConversationComposerStoreContext(
+ (state) => state.composerMediaPreview
+ );
+ const composerInputValue = useConversationComposerStoreContext(
+ (state) => state.inputValue
+ );
+
+ const canSend =
+ composerInputValue.length > 0 || mediaPreview?.status === "uploaded";
+
+ return (
+
+
+
+ );
+});
+
+const AttachmentsPreview = memo(function AttachmentsPreview() {
+ const { theme } = useAppTheme();
+
+ const mediaPreview = useConversationComposerStoreContext(
+ (state) => state.composerMediaPreview
+ );
+
+ const store = useConversationComposerStore();
+
+ const handleAttachmentClosed = useCallback(() => {
+ store.getState().setComposerMediaPreview(null);
+ }, [store]);
+
+ const isLandscape = !!(
+ mediaPreview?.dimensions?.height &&
+ mediaPreview?.dimensions?.width &&
+ mediaPreview.dimensions.width > mediaPreview.dimensions.height
+ );
+
+ const maxHeight = isLandscape ? 90 : 120;
+
+ const containerAS = useAnimatedStyle(() => {
+ return {
+ height: withSpring(mediaPreview?.mediaURI ? maxHeight : 0, {
+ damping: SICK_DAMPING,
+ stiffness: SICK_STIFFNESS,
+ }),
+ };
+ }, [mediaPreview?.mediaURI, maxHeight]);
+
+ return (
+
+ {!!mediaPreview && (
+
+ mediaPreview.dimensions.height
+ )
+ }
+ />
+
+ )}
+
+ );
+});
+
+const ComposerTextInput = memo(function ComposerTextInput(props: {
+ onSubmitEditing: () => Promise;
+}) {
+ const { onSubmitEditing } = props;
+
+ const inputRef = useRef(null);
+
+ const { theme } = useAppTheme();
+
+ const store = useConversationComposerStore();
+ const inputDefaultValue = store.getState().inputValue;
+
+ const handleChangeText = useCallback(
+ (text: string) => {
+ store.setState((state) => ({
+ ...state,
+ inputValue: text,
+ }));
+ },
+ [store]
+ );
+
+ // If we clear the input (i.e after sending a message)
+ // we need to clear the input value in the text input
+ // Doing this since we are using a uncontrolled component
+ useEffect(() => {
+ const unsubscribe = store.subscribe((state, prevState) => {
+ if (prevState.inputValue && !state.inputValue) {
+ inputRef.current?.clear();
+ }
+ });
+
+ return () => unsubscribe();
+ }, [store]);
+
+ const handleSubmitEditing = useCallback(() => {
+ onSubmitEditing();
+ }, [onSubmitEditing]);
+
+ return (
+ {
+ // Maybe want a better check here, but web/tablet is not the focus right now
+ if (Platform.OS !== "web") {
+ return;
+ }
+
+ if (
+ event.nativeEvent.key === "Enter" &&
+ !event.altKey &&
+ !event.metaKey &&
+ !event.shiftKey
+ ) {
+ event.preventDefault();
+ onSubmitEditing();
+ }
+ }}
+ ref={inputRef}
+ onSubmitEditing={handleSubmitEditing}
+ onChangeText={handleChangeText}
+ multiline
+ defaultValue={inputDefaultValue}
+ placeholder="Message"
+ placeholderTextColor={theme.colors.text.tertiary}
+ />
+ );
+});
diff --git a/components/Chat/ConsentPopup/dm-consent-popup.tsx b/features/conversation/conversation-consent-popup/conversation-consent-popup-dm.tsx
similarity index 85%
rename from components/Chat/ConsentPopup/dm-consent-popup.tsx
rename to features/conversation/conversation-consent-popup/conversation-consent-popup-dm.tsx
index 29fe132c7..61caf8696 100644
--- a/components/Chat/ConsentPopup/dm-consent-popup.tsx
+++ b/features/conversation/conversation-consent-popup/conversation-consent-popup-dm.tsx
@@ -1,8 +1,16 @@
import { showSnackbar } from "@/components/Snackbar/Snackbar.service";
import { showActionSheetWithOptions } from "@/components/StateHandlers/ActionSheetStateHandler";
-import { getCurrentAccount } from "@/data/store/accountsStore";
+import {
+ getCurrentAccount,
+ useCurrentAccount,
+} from "@/data/store/accountsStore";
+import {
+ useConversationCurrentConversationId,
+ useCurrentConversationTopic,
+} from "../conversation.store-context";
import { useRouter } from "@/navigation/useNavigation";
import { refetchConversationQuery } from "@/queries/useConversationQuery";
+import { useDmPeerInboxId } from "@/queries/useDmPeerInbox";
import { actionSheetColors } from "@/styles/colors";
import { captureError, captureErrorWithToast } from "@/utils/capture-error";
import { ensureError } from "@/utils/error";
@@ -12,11 +20,6 @@ import {
} from "@/utils/xmtpRN/contacts";
import { translate } from "@i18n";
import { useMutation } from "@tanstack/react-query";
-import {
- ConversationId,
- ConversationTopic,
- InboxId,
-} from "@xmtp/react-native-sdk";
import React, { useCallback } from "react";
import { useColorScheme } from "react-native";
import {
@@ -24,19 +27,15 @@ import {
ConsentPopupButtonsContainer,
ConsentPopupContainer,
ConsentPopupTitle,
-} from "./consent-popup.design-system";
+} from "./conversation-consent-popup.design-system";
-type ConsentPopupProps = {
- peerInboxId: InboxId;
- topic: ConversationTopic;
- conversationId: ConversationId;
-};
+export function DmConsentPopup() {
+ const topic = useCurrentConversationTopic();
+ const conversationId = useConversationCurrentConversationId();
+ const currentAccount = useCurrentAccount()!;
+
+ const { data: peerInboxId } = useDmPeerInboxId(currentAccount, topic);
-export function DmConsentPopup({
- peerInboxId,
- topic,
- conversationId,
-}: ConsentPopupProps) {
const navigation = useRouter();
const colorScheme = useColorScheme();
@@ -46,6 +45,9 @@ export function DmConsentPopup({
status: consentToInboxIdsOnProtocolByAccountStatus,
} = useMutation({
mutationFn: async (args: { consent: "allow" | "deny" }) => {
+ if (!peerInboxId) {
+ throw new Error("Peer inbox id not found");
+ }
const currentAccount = getCurrentAccount()!;
await Promise.all([
consentToGroupsOnProtocolByAccount({
diff --git a/components/Chat/ConsentPopup/group-consent-popup.tsx b/features/conversation/conversation-consent-popup/conversation-consent-popup-group.tsx
similarity index 89%
rename from components/Chat/ConsentPopup/group-consent-popup.tsx
rename to features/conversation/conversation-consent-popup/conversation-consent-popup-group.tsx
index a9541a318..8bb0007ca 100644
--- a/components/Chat/ConsentPopup/group-consent-popup.tsx
+++ b/features/conversation/conversation-consent-popup/conversation-consent-popup-group.tsx
@@ -3,20 +3,20 @@ import {
ConsentPopupButtonsContainer,
ConsentPopupContainer,
ConsentPopupTitle,
-} from "@/components/Chat/ConsentPopup/consent-popup.design-system";
+} from "@/features/conversation/conversation-consent-popup/conversation-consent-popup.design-system";
import { useCurrentAccount } from "@/data/store/accountsStore";
+import { useCurrentConversationTopic } from "../conversation.store-context";
import { useRouter } from "@/navigation/useNavigation";
import { getGroupNameQueryData } from "@/queries/useGroupNameQuery";
import { captureErrorWithToast } from "@/utils/capture-error";
import { useGroupConsent } from "@hooks/useGroupConsent";
import { translate } from "@i18n";
import { groupRemoveRestoreHandler } from "@utils/groupUtils/groupActionHandlers";
-import { ConversationTopic } from "@xmtp/react-native-sdk";
import React, { useCallback } from "react";
import { useColorScheme } from "react-native";
-export function GroupConsentPopup(props: { topic: ConversationTopic }) {
- const { topic } = props;
+export function GroupConsentPopup() {
+ const topic = useCurrentConversationTopic();
const navigation = useRouter();
diff --git a/components/Chat/ConsentPopup/consent-popup.design-system.tsx b/features/conversation/conversation-consent-popup/conversation-consent-popup.design-system.tsx
similarity index 96%
rename from components/Chat/ConsentPopup/consent-popup.design-system.tsx
rename to features/conversation/conversation-consent-popup/conversation-consent-popup.design-system.tsx
index 4e57c00b6..9ec435283 100644
--- a/components/Chat/ConsentPopup/consent-popup.design-system.tsx
+++ b/features/conversation/conversation-consent-popup/conversation-consent-popup.design-system.tsx
@@ -7,8 +7,7 @@ import React from "react";
export function ConsentPopupButton({ style, ...rest }: IButtonProps) {
const { theme } = useAppTheme();
-
- return ;
+ return ;
}
export function ConsentPopupTitle(props: ITextProps) {
diff --git a/features/conversation/conversation-context.tsx b/features/conversation/conversation-context.tsx
deleted file mode 100644
index baf85c300..000000000
--- a/features/conversation/conversation-context.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { captureErrorWithToast } from "@/utils/capture-error";
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { useConversationQuery } from "@queries/useConversationQuery";
-import { addConversationToConversationListQuery } from "@queries/useV3ConversationListQuery";
-import { navigate } from "@utils/navigation";
-import { createConversationByAccount } from "@utils/xmtpRN/conversations";
-import {
- ConversationId,
- ConversationVersion,
- MessageId,
- RemoteAttachmentContent,
-} from "@xmtp/react-native-sdk";
-import React, { useCallback, useEffect, useMemo } from "react";
-import { SharedValue, useSharedValue } from "react-native-reanimated";
-import { createContext, useContextSelector } from "use-context-selector";
-import {
- updateNewConversation,
- useConversationCurrentPeerAddress,
- useConversationCurrentTopic,
-} from "./conversation-service";
-
-export type ISendMessageParams = {
- content: {
- text?: string;
- remoteAttachment?: RemoteAttachmentContent;
- };
- referencedMessageId?: MessageId;
-} & (
- | { content: { text: string; remoteAttachment?: RemoteAttachmentContent } }
- | { content: { text?: string; remoteAttachment: RemoteAttachmentContent } }
-);
-
-export type IConversationContextType = {
- isAllowedConversation: boolean;
- isBlockedConversation: boolean;
- isLoadingConversationConsent: boolean;
- isNewConversation: boolean;
- conversationNotFound: boolean;
- conversationVersion: ConversationVersion | null;
- conversationId: ConversationId | null;
- peerAddress: string | null;
- composerHeightAV: SharedValue;
- sendMessage: (message: ISendMessageParams) => Promise;
- reactOnMessage: (args: {
- messageId: MessageId;
- emoji: string;
- }) => Promise;
- removeReactionFromMessage: (args: {
- messageId: MessageId;
- emoji: string;
- }) => Promise;
-};
-
-type IConversationContextProps = {
- children: React.ReactNode;
-};
-
-const ConversationContext = createContext(
- {} as IConversationContextType
-);
-
-export const ConversationContextProvider = (
- props: IConversationContextProps
-) => {
- const { children } = props;
-
- const topic = useConversationCurrentTopic();
- const peerAddress = useConversationCurrentPeerAddress();
- const currentAccount = useCurrentAccount()!;
-
- const { data: conversation, isLoading: isLoadingConversation } =
- useConversationQuery(currentAccount, topic!);
-
- const composerHeightAV = useSharedValue(0);
-
- useEffect(() => {
- const checkActive = async () => {
- if (!conversation) return;
- if (conversation.version === ConversationVersion.GROUP) {
- const isActive = conversation.isGroupActive;
- // If not active leave the screen
- if (!isActive) {
- navigate("Chats");
- }
- }
- };
- checkActive();
- }, [conversation]);
-
- const reactOnMessage = useCallback(
- async (args: { messageId: MessageId; emoji: string }) => {
- const { messageId, emoji } = args;
- try {
- if (!conversation) {
- return;
- }
- await conversation.send({
- reaction: {
- reference: messageId,
- content: emoji,
- schema: "unicode",
- action: "added",
- },
- });
- } catch (error) {
- captureErrorWithToast(error);
- }
- },
- [conversation]
- );
-
- const removeReactionFromMessage = useCallback(
- async (args: { messageId: MessageId; emoji: string }) => {
- const { messageId, emoji } = args;
- try {
- await conversation?.send({
- reaction: {
- reference: messageId,
- content: emoji,
- schema: "unicode",
- action: "removed",
- },
- });
- } catch (error) {
- captureErrorWithToast(error);
- }
- },
- [conversation]
- );
-
- const sendMessage = useCallback(
- async ({ referencedMessageId, content }: ISendMessageParams) => {
- const sendCallback = async (payload: any) => {
- if (!conversation && !peerAddress) {
- return;
- }
-
- if (!conversation && peerAddress) {
- const newConversation = await createConversationByAccount(
- currentAccount,
- peerAddress
- );
- updateNewConversation(newConversation.topic);
- await newConversation.send(payload);
- addConversationToConversationListQuery(
- currentAccount,
- newConversation
- );
- return;
- }
-
- await conversation?.send(payload);
- };
-
- if (referencedMessageId) {
- if (content.remoteAttachment) {
- await sendCallback({
- reply: {
- reference: referencedMessageId,
- content: { remoteAttachment: content.remoteAttachment },
- },
- });
- }
- if (content.text) {
- await sendCallback({
- reply: {
- reference: referencedMessageId,
- content: { text: content.text },
- },
- });
- }
- return;
- }
-
- if (content.remoteAttachment) {
- await sendCallback({
- remoteAttachment: content.remoteAttachment,
- });
- }
-
- if (content.text) {
- await sendCallback(content.text);
- }
- },
- [conversation, currentAccount, peerAddress]
- );
-
- const isAllowedConversation = useMemo(() => {
- if (!conversation) {
- return false;
- }
- return conversation.state === "allowed";
- }, [conversation]);
-
- return (
-
- {children}
-
- );
-};
-
-export const useConversationContext = <
- K extends keyof IConversationContextType,
->(
- key: K
-) => useContextSelector(ConversationContext, (s) => s[key]);
diff --git a/features/conversation/conversation-dm-context.tsx b/features/conversation/conversation-dm-context.tsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/features/conversations/components/DmConversationTitle.tsx b/features/conversation/conversation-dm-header-title.tsx
similarity index 71%
rename from features/conversations/components/DmConversationTitle.tsx
rename to features/conversation/conversation-dm-header-title.tsx
index 63c32ad8a..667f59553 100644
--- a/features/conversations/components/DmConversationTitle.tsx
+++ b/features/conversation/conversation-dm-header-title.tsx
@@ -1,46 +1,22 @@
-import { useCallback } from "react";
-import { useConversationTitleLongPress } from "../hooks/useConversationTitleLongPress";
-import { useRouter } from "@navigation/useNavigation";
-import { NativeStackNavigationProp } from "@react-navigation/native-stack";
-import { NavigationParamList } from "@screens/Navigation/Navigation";
-import { useDmPeerAddressQuery } from "@queries/useDmPeerAddressQuery";
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { ConversationTopic } from "@xmtp/react-native-sdk";
-import { usePreferredName } from "@hooks/usePreferredName";
+import { ConversationTitle } from "@/features/conversation/conversation-title";
+import { copyToClipboard } from "@/utils/clipboard";
import Avatar from "@components/Avatar";
+import { useCurrentAccount } from "@data/store/accountsStore";
import { usePreferredAvatarUri } from "@hooks/usePreferredAvatarUri";
+import { usePreferredName } from "@hooks/usePreferredName";
+import { useProfileSocials } from "@hooks/useProfileSocials";
+import { useRouter } from "@navigation/useNavigation";
+import { useDmPeerAddressQuery } from "@queries/useDmPeerAddressQuery";
import { AvatarSizes } from "@styles/sizes";
import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
+import { ConversationTopic } from "@xmtp/react-native-sdk";
+import { useCallback } from "react";
import { ImageStyle, Platform } from "react-native";
-import { useProfileSocials } from "@hooks/useProfileSocials";
-import { ConversationTitleDumb } from "@components/Conversation/ConversationTitleDumb";
type DmConversationTitleProps = {
topic: ConversationTopic;
};
-type UseUserInteractionProps = {
- peerAddress?: string;
- navigation: NativeStackNavigationProp;
- topic: ConversationTopic;
-};
-
-const useUserInteraction = ({
- navigation,
- peerAddress,
- topic,
-}: UseUserInteractionProps) => {
- const onPress = useCallback(() => {
- if (peerAddress) {
- navigation.push("Profile", { address: peerAddress });
- }
- }, [navigation, peerAddress]);
-
- const onLongPress = useConversationTitleLongPress(topic);
-
- return { onPress, onLongPress };
-};
-
export const DmConversationTitle = ({ topic }: DmConversationTitleProps) => {
const account = useCurrentAccount()!;
@@ -50,11 +26,15 @@ export const DmConversationTitle = ({ topic }: DmConversationTitleProps) => {
const { data: peerAddress } = useDmPeerAddressQuery(account, topic);
- const { onPress, onLongPress } = useUserInteraction({
- peerAddress,
- navigation,
- topic,
- });
+ const onPress = useCallback(() => {
+ if (peerAddress) {
+ navigation.push("Profile", { address: peerAddress });
+ }
+ }, [navigation, peerAddress]);
+
+ const onLongPress = useCallback(() => {
+ copyToClipboard(JSON.stringify(topic));
+ }, [topic]);
const { isLoading } = useProfileSocials(peerAddress ?? "");
@@ -66,7 +46,7 @@ export const DmConversationTitle = ({ topic }: DmConversationTitleProps) => {
if (!displayAvatar) return null;
return (
- (
- {} as IConversationGroupContextType
-);
-
-export const ConversationGroupContextProvider = (
- props: IConversationGroupContextProps
-) => {
- const { children } = props;
-
- const topic = useConversationCurrentTopic();
- const currentAccount = useCurrentAccount()!;
-
- const { data: groupName, isLoading: groupNameIsLoading } = useGroupNameQuery(
- currentAccount,
- topic!
- );
-
- const { data: groupPhoto, isLoading: groupPhotoIsLoading } =
- useGroupPhotoQuery(currentAccount, topic!);
-
- // const { data: members, isLoading: membersLoading } =
- // useGroupMembersConversationScreenQuery(currentAccount, topic!);
-
- const value = useMemo(
- () => ({ groupName, groupNameIsLoading, groupPhoto, groupPhotoIsLoading }),
- [groupName, groupNameIsLoading, groupPhoto, groupPhotoIsLoading]
- );
-
- return (
-
- {children}
-
- );
-};
-
-export const useConversationGroupContext = <
- K extends keyof IConversationGroupContextType,
->(
- key: K
-) => {
- return useContextSelector(ConversationGroupContext, (s) => s[key]);
-};
diff --git a/features/conversation/conversation-group-header-title.tsx b/features/conversation/conversation-group-header-title.tsx
new file mode 100644
index 000000000..b5b489a1a
--- /dev/null
+++ b/features/conversation/conversation-group-header-title.tsx
@@ -0,0 +1,165 @@
+import { Text } from "@/design-system/Text";
+import { ConversationTitle } from "@/features/conversation/conversation-title";
+import { useGroupNameConvos } from "@/features/conversation/hooks/use-group-name-convos";
+import { useGroupPendingRequests } from "@/hooks/useGroupPendingRequests";
+import { useProfilesSocials } from "@/hooks/useProfilesSocials";
+import {
+ useGroupMembersConversationScreenQuery,
+ useGroupMembersQuery,
+} from "@/queries/useGroupMembersQuery";
+import { copyToClipboard } from "@/utils/clipboard";
+import { getPreferredAvatar, getPreferredName } from "@/utils/profile";
+import Avatar from "@components/Avatar";
+import { GroupAvatarDumb } from "@components/GroupAvatar";
+import { useCurrentAccount } from "@data/store/accountsStore";
+import { translate } from "@i18n";
+import { useRouter } from "@navigation/useNavigation";
+import { useGroupPhotoQuery } from "@queries/useGroupPhotoQuery";
+import { AvatarSizes } from "@styles/sizes";
+import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
+import { ConversationTopic } from "@xmtp/react-native-sdk";
+import React, { memo, useCallback, useMemo } from "react";
+import { ImageStyle, Platform } from "react-native";
+
+type GroupConversationTitleProps = {
+ topic: ConversationTopic;
+};
+
+export const GroupConversationTitle = memo(
+ ({ topic }: GroupConversationTitleProps) => {
+ const currentAccount = useCurrentAccount()!;
+
+ const { data: groupPhoto, isLoading: groupPhotoLoading } =
+ useGroupPhotoQuery(currentAccount, topic!);
+
+ const { data: members } = useGroupMembersQuery(currentAccount, topic!);
+
+ const { data: memberData } = useGroupMembersAvatarData({ topic });
+
+ const { groupName, isLoading: groupNameLoading } = useGroupNameConvos({
+ topic,
+ account: currentAccount,
+ });
+
+ const navigation = useRouter();
+
+ const { themed } = useAppTheme();
+
+ const onPress = useCallback(() => {
+ navigation.push("Group", { topic });
+ }, [navigation, topic]);
+
+ const onLongPress = useCallback(() => {
+ copyToClipboard(JSON.stringify(topic));
+ }, [topic]);
+
+ const requestsCount = useGroupPendingRequests(topic).length;
+
+ const displayAvatar = !groupPhotoLoading && !groupNameLoading;
+
+ const avatarComponent = useMemo(() => {
+ return groupPhoto ? (
+
+ ) : (
+
+ );
+ }, [groupPhoto, memberData, themed]);
+
+ const memberText =
+ members?.ids.length === 1
+ ? translate("member_count", { count: members?.ids.length })
+ : translate("members_count", { count: members?.ids.length });
+ const displayMemberText = members?.ids.length;
+
+ if (!displayAvatar) return null;
+
+ return (
+
+ {memberText}
+ {requestsCount > 0 && (
+ <>
+ {" • "}
+
+ {translate("pending_count", { count: requestsCount })}
+
+ >
+ )}
+
+ ) : null
+ }
+ avatarComponent={avatarComponent}
+ />
+ );
+ }
+);
+
+const $avatar: ThemedStyle = (theme) => ({
+ marginRight: Platform.OS === "android" ? theme.spacing.lg : theme.spacing.xxs,
+ marginLeft: Platform.OS === "ios" ? theme.spacing.zero : -theme.spacing.xxs,
+});
+
+type UseGroupMembersAvatarDataProps = {
+ topic: ConversationTopic;
+};
+
+const useGroupMembersAvatarData = ({
+ topic,
+}: UseGroupMembersAvatarDataProps) => {
+ const currentAccount = useCurrentAccount()!;
+ const { data: members, ...query } = useGroupMembersConversationScreenQuery(
+ currentAccount,
+ topic
+ );
+
+ const memberAddresses = useMemo(() => {
+ const addresses: string[] = [];
+ for (const memberId of members?.ids ?? []) {
+ const member = members?.byId[memberId];
+ if (
+ member?.addresses[0] &&
+ member?.addresses[0].toLowerCase() !== currentAccount?.toLowerCase()
+ ) {
+ addresses.push(member?.addresses[0]);
+ }
+ }
+ return addresses;
+ }, [members, currentAccount]);
+
+ const data = useProfilesSocials(memberAddresses);
+
+ const memberData: {
+ address: string;
+ uri?: string;
+ name?: string;
+ }[] = useMemo(() => {
+ return data.map(({ data: socials }, index) =>
+ socials
+ ? {
+ address: memberAddresses[index],
+ uri: getPreferredAvatar(socials),
+ name: getPreferredName(socials, memberAddresses[index]),
+ }
+ : {
+ address: memberAddresses[index],
+ uri: undefined,
+ name: memberAddresses[index],
+ }
+ );
+ }, [data, memberAddresses]);
+
+ return { data: memberData, ...query };
+};
diff --git a/features/conversation/conversation-keyboard-filler.tsx b/features/conversation/conversation-keyboard-filler.tsx
new file mode 100644
index 000000000..02ff9a84d
--- /dev/null
+++ b/features/conversation/conversation-keyboard-filler.tsx
@@ -0,0 +1,15 @@
+import { memo } from "react";
+import { AnimatedVStack } from "@design-system/VStack";
+import { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+export const KeyboardFiller = memo(function KeyboardFiller() {
+ const { height: keyboardHeightAV } = useAnimatedKeyboard();
+ const insets = useSafeAreaInsets();
+
+ const as = useAnimatedStyle(() => ({
+ height: Math.max(keyboardHeightAV.value - insets.bottom, 0),
+ }));
+
+ return ;
+});
diff --git a/components/Chat/Message/components/message-bubble.tsx b/features/conversation/conversation-message/conversation-message-bubble.tsx
similarity index 100%
rename from components/Chat/Message/components/message-bubble.tsx
rename to features/conversation/conversation-message/conversation-message-bubble.tsx
diff --git a/components/Chat/Message/components/message-container.tsx b/features/conversation/conversation-message/conversation-message-container.tsx
similarity index 54%
rename from components/Chat/Message/components/message-container.tsx
rename to features/conversation/conversation-message/conversation-message-container.tsx
index 848f4cdd4..775068fc5 100644
--- a/components/Chat/Message/components/message-container.tsx
+++ b/features/conversation/conversation-message/conversation-message-container.tsx
@@ -1,19 +1,20 @@
+import { useSelect } from "@/data/store/storeHelpers";
import { HStack } from "@/design-system/HStack";
-import { useAppTheme } from "@/theme/useAppTheme";
+import { useMessageContextStoreContext } from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { debugBorder } from "@/utils/debug-style";
import { memo } from "react";
export const MessageContainer = memo(function MessageContainer(props: {
children: React.ReactNode;
- fromMe: boolean;
}) {
- const { children, fromMe } = props;
+ const { children } = props;
- const { theme } = useAppTheme();
+ const { fromMe } = useMessageContextStoreContext(useSelect(["fromMe"]));
return (
;
};
-export function ChatGroupUpdatedMessage({
+export function MessageChatGroupUpdate({
message,
-}: IChatGroupUpdatedMessageProps) {
+}: IMessageChatGroupUpdateProps) {
const { themed, theme } = useAppTheme();
const content = message.content();
@@ -37,11 +36,12 @@ export function ChatGroupUpdatedMessage({
return (
-
{/* Member additions */}
{content.membersAdded.map((member) => (
diff --git a/components/Chat/Message/message-content-types/message-remote-attachment.tsx b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-remote-attachment.tsx
similarity index 53%
rename from components/Chat/Message/message-content-types/message-remote-attachment.tsx
rename to features/conversation/conversation-message/conversation-message-content-types/conversation-message-remote-attachment.tsx
index c1a8a9469..9463e0858 100644
--- a/components/Chat/Message/message-content-types/message-remote-attachment.tsx
+++ b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-remote-attachment.tsx
@@ -1,6 +1,5 @@
-import { RemoteAttachmentImage } from "@/components/Chat/Attachment/remote-attachment-image";
-import { MessageLayout } from "@/components/Chat/Message/components/message-layout";
import { VStack } from "@/design-system/VStack";
+import { AttachmentRemoteImage } from "@/features/conversation/conversation-attachment/conversation-attachment-remote-image";
import { useAppTheme } from "@/theme/useAppTheme";
import { DecodedMessage, RemoteAttachmentCodec } from "@xmtp/react-native-sdk";
import { memo } from "react";
@@ -22,19 +21,17 @@ export const MessageRemoteAttachment = memo(function MessageRemoteAttachment({
}
return (
-
-
-
-
-
+
+
+
);
});
diff --git a/components/Chat/Message/message-content-types/message-reply.tsx b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-reply.tsx
similarity index 70%
rename from components/Chat/Message/message-content-types/message-reply.tsx
rename to features/conversation/conversation-message/conversation-message-content-types/conversation-message-reply.tsx
index 278efe3db..284ed83d1 100644
--- a/components/Chat/Message/message-content-types/message-reply.tsx
+++ b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-reply.tsx
@@ -1,12 +1,14 @@
-import { RemoteAttachmentImage } from "@/components/Chat/Attachment/remote-attachment-image";
+import { useSelect } from "@/data/store/storeHelpers";
+import { AttachmentRemoteImage } from "@/features/conversation/conversation-attachment/conversation-attachment-remote-image";
+import { useMessageContextStoreContext } from "@/features/conversation/conversation-message/conversation-message.store-context";
import {
BubbleContainer,
BubbleContentContainer,
-} from "@/components/Chat/Message/components/message-bubble";
-import { MessageLayout } from "@/components/Chat/Message/components/message-layout";
-import { MessageText } from "@/components/Chat/Message/components/message-text";
-import { useMessageContextStoreContext } from "@/components/Chat/Message/stores/message-store";
-import { useSelect } from "@/data/store/storeHelpers";
+} from "@/features/conversation/conversation-message/conversation-message-bubble";
+import { MessageText } from "@/features/conversation/conversation-message/conversation-message-text";
+import { useCurrentConversationTopic } from "@/features/conversation/conversation.store-context";
+import { usePreferredInboxName } from "@/hooks/usePreferredInboxName";
+import { getConversationMessages } from "@/queries/useConversationMessages";
import { useCurrentAccount } from "@data/store/accountsStore";
import { HStack } from "@design-system/HStack";
import { Icon } from "@design-system/Icon/Icon";
@@ -16,7 +18,6 @@ import { getConversationMessageQueryOptions } from "@queries/useConversationMess
import { useQuery } from "@tanstack/react-query";
import { useAppTheme } from "@theme/useAppTheme";
import { sentryTrackError } from "@utils/sentry";
-import { getReadableProfile } from "@utils/str";
import {
DecodedMessage,
InboxId,
@@ -24,8 +25,6 @@ import {
ReplyCodec,
} from "@xmtp/react-native-sdk";
import { memo } from "react";
-import { getCurrentConversationMessages } from "../../../../features/conversation/conversation-service";
-import { usePreferredInboxName } from "@/hooks/usePreferredInboxName";
export const MessageReply = memo(function MessageReply(props: {
message: DecodedMessage;
@@ -52,57 +51,54 @@ export const MessageReply = memo(function MessageReply(props: {
}
return (
-
-
-
+
+
-
-
-
- {!!replyMessageContent.content.remoteAttachment && (
-
+
+ {!!replyMessageContent.content.remoteAttachment && (
+
+
-
-
- )}
-
- {!!replyMessageContent.content.text && (
-
- {replyMessageContent.content.text}
-
- )}
-
-
-
-
+ />
+
+ )}
+
+ {!!replyMessageContent.content.text && (
+
+ {replyMessageContent.content.text}
+
+ )}
+
+
+
);
});
@@ -187,7 +183,7 @@ const MessageReplyReferenceContent = memo(
if (content.content.remoteAttachment) {
return (
- | undefined {
const currentAccount = useCurrentAccount()!;
- const messages = getCurrentConversationMessages();
+ const topic = useCurrentConversationTopic();
+ const messages = getConversationMessages(currentAccount, topic);
const cachedReplyMessage = messages?.byId[messageId] as
| DecodedMessage
diff --git a/features/conversation/conversation-message/conversation-message-content-types/conversation-message-simple-text.tsx b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-simple-text.tsx
new file mode 100644
index 000000000..4b9c2fec1
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-simple-text.tsx
@@ -0,0 +1,32 @@
+import {
+ BubbleContainer,
+ BubbleContentContainer,
+} from "@/features/conversation/conversation-message/conversation-message-bubble";
+import { MessageText } from "@/features/conversation/conversation-message/conversation-message-text";
+import { useMessageContextStoreContext } from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { useSelect } from "@/data/store/storeHelpers";
+import { DecodedMessage, TextCodec } from "@xmtp/react-native-sdk";
+import { memo } from "react";
+
+export const MessageSimpleText = memo(function MessageSimpleText(props: {
+ message: DecodedMessage;
+}) {
+ const { message } = props;
+
+ const textContent = message.content();
+
+ const { hasNextMessageInSeries, fromMe } = useMessageContextStoreContext(
+ useSelect(["hasNextMessageInSeries", "fromMe"])
+ );
+
+ return (
+
+
+ {textContent}
+
+
+ );
+});
diff --git a/components/Chat/Message/message-content-types/message-static-attachment.tsx b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-static-attachment.tsx
similarity index 86%
rename from components/Chat/Message/message-content-types/message-static-attachment.tsx
rename to features/conversation/conversation-message/conversation-message-content-types/conversation-message-static-attachment.tsx
index ae433e400..78e86b422 100644
--- a/components/Chat/Message/message-content-types/message-static-attachment.tsx
+++ b/features/conversation/conversation-message/conversation-message-content-types/conversation-message-static-attachment.tsx
@@ -1,7 +1,6 @@
-import { AttachmentContainer } from "@/components/Chat/Attachment/attachment-container";
-import { AttachmentLoading } from "@/components/Chat/Attachment/attachment-loading";
-import { MessageLayout } from "@/components/Chat/Message/components/message-layout";
import { Text } from "@/design-system/Text";
+import { AttachmentContainer } from "@/features/conversation/conversation-attachment/conversation-attachment-container";
+import { AttachmentLoading } from "@/features/conversation/conversation-attachment/conversation-attachment-loading";
import { translate } from "@/i18n";
import { getLocalAttachment } from "@/utils/attachment/handleAttachment";
import { useQuery } from "@tanstack/react-query";
@@ -32,11 +31,7 @@ export const MessageStaticAttachment = memo(function MessageStaticAttachment({
return null;
}
- return (
-
-
-
- );
+ return ;
});
const Content = memo(function Content(props: {
diff --git a/components/Chat/Message/message-context-menu/message-context-menu-above-message-reactions.tsx b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-above-message-reactions.tsx
similarity index 88%
rename from components/Chat/Message/message-context-menu/message-context-menu-above-message-reactions.tsx
rename to features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-above-message-reactions.tsx
index e6d834402..d7d9a35b8 100644
--- a/components/Chat/Message/message-context-menu/message-context-menu-above-message-reactions.tsx
+++ b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-above-message-reactions.tsx
@@ -1,27 +1,32 @@
-import {
- useConversationMessageById,
- useCurrentAccountInboxId,
-} from "@/components/Chat/Message/message-utils";
import { HStack } from "@/design-system/HStack";
import { Icon } from "@/design-system/Icon/Icon";
import { TouchableOpacity } from "@/design-system/TouchableOpacity";
import { AnimatedVStack, VStack } from "@/design-system/VStack";
-import { messageIsFromCurrentUserV3 } from "@/features/conversations/utils/messageIsFromCurrentUser";
+import { useConversationMessageById } from "@/features/conversation/conversation-message/conversation-message.utils";
+import { messageIsFromCurrentUserV3 } from "@/features/conversation/utils/message-is-from-current-user";
+import { useCurrentAccountInboxId } from "@/hooks/use-current-account-inbox-id";
import { getReactionContent } from "@/utils/xmtpRN/reactions";
import { Text } from "@design-system/Text";
import { useAppTheme } from "@theme/useAppTheme";
import { favoritedEmojis } from "@utils/emojis/favoritedEmojis";
-import { InboxId, MessageId, ReactionContent } from "@xmtp/react-native-sdk";
+import {
+ ConversationTopic,
+ InboxId,
+ MessageId,
+ ReactionContent,
+} from "@xmtp/react-native-sdk";
import React, { memo, useCallback, useMemo } from "react";
import {
EntryAnimationsValues,
FadeIn,
withSpring,
} from "react-native-reanimated";
-import { MESSAGE_CONTEXT_MENU_ABOVE_MESSAGE_REACTIONS_HEIGHT } from "./message-context-menu-constant";
+import { MESSAGE_CONTEXT_MENU_ABOVE_MESSAGE_REACTIONS_HEIGHT } from "./conversation-message-context-menu-constant";
+import { StaggeredAnimation } from "@/design-system/staggered-animation";
export const MessageContextMenuAboveMessageReactions = memo(
function MessageContextMenuAboveMessageReactions({
+ topic,
messageId,
onChooseMoreEmojis,
onSelectReaction,
@@ -29,6 +34,7 @@ export const MessageContextMenuAboveMessageReactions = memo(
originY,
reactors,
}: {
+ topic: ConversationTopic;
messageId: MessageId;
onChooseMoreEmojis: () => void;
onSelectReaction: (emoji: string) => void;
@@ -42,7 +48,10 @@ export const MessageContextMenuAboveMessageReactions = memo(
const { data: currentUserInboxId } = useCurrentAccountInboxId();
- const { message } = useConversationMessageById(messageId);
+ const { message } = useConversationMessageById({
+ messageId,
+ topic,
+ });
const messageFromMe = messageIsFromCurrentUserV3({
message,
@@ -130,19 +139,18 @@ export const MessageContextMenuAboveMessageReactions = memo(
]}
>
{favoritedEmojis.getEmojis().map((emoji, index) => (
-
-
+
))}
void;
+};
+
+type IMessageContextMenuStoreProviderProps =
+ React.PropsWithChildren;
+
+type IMessageContextMenuStore = ReturnType<
+ typeof createMessageContextMenuStore
+>;
+
+export const MessageContextMenuStoreProvider = memo(
+ ({ children, ...props }: IMessageContextMenuStoreProviderProps) => {
+ const storeRef = useRef();
+ if (!storeRef.current) {
+ storeRef.current = createMessageContextMenuStore(props);
+ }
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+const createMessageContextMenuStore = (
+ initProps: IMessageContextMenuStoreProps
+) => {
+ return createStore()(
+ subscribeWithSelector((set) => ({
+ messageContextMenuData: null,
+ setMessageContextMenuData: (data) =>
+ set({ messageContextMenuData: data }),
+ ...initProps,
+ }))
+ );
+};
+
+const MessageContextMenuStoreContext =
+ createContext(null);
+
+export function useMessageContextMenuStoreContext(
+ selector: (state: IMessageContextMenuStoreState) => T
+): T {
+ const store = useContext(MessageContextMenuStoreContext);
+ if (!store)
+ throw new Error("Missing MessageContextMenuStore.Provider in the tree");
+ return useStore(store, selector);
+}
+
+export function useMessageContextMenuStore() {
+ const store = useContext(MessageContextMenuStoreContext);
+ if (!store)
+ throw new Error(`Missing MessageContextMenuStore.Provider in the tree`);
+ return store;
+}
diff --git a/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.tsx b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.tsx
new file mode 100644
index 000000000..6146688c3
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.tsx
@@ -0,0 +1,274 @@
+import { MessageContextMenuBackdrop } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-backdrop";
+import { MessageContextMenuEmojiPicker } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-emoji-picker/conversation-message-context-menu-emoji-picker";
+import { openMessageContextMenuEmojiPicker } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-emoji-picker/conversation-message-context-menu-emoji-picker-utils";
+import { MessageContextMenuItems } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-items";
+import { MessageContextMenuReactors } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu-reactors";
+import {
+ IMessageContextMenuStoreState,
+ useMessageContextMenuStore,
+ useMessageContextMenuStoreContext,
+} from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.store-context";
+import {
+ getMessageById,
+ useConversationMessageReactions,
+} from "@/features/conversation/conversation-message/conversation-message.utils";
+import { useCurrentAccountInboxId } from "@/hooks/use-current-account-inbox-id";
+import { useCurrentAccount } from "@/data/store/accountsStore";
+import { AnimatedVStack, VStack } from "@/design-system/VStack";
+import { useCurrentConversationTopic } from "../../conversation.store-context";
+import { useReactOnMessage } from "@/features/conversation/hooks/use-react-on-message";
+import { useRemoveReactionOnMessage } from "@/features/conversation/hooks/use-remove-reaction-on-message";
+import { messageIsFromCurrentUserV3 } from "@/features/conversation/utils/message-is-from-current-user";
+import { useConversationQuery } from "@/queries/useConversationQuery";
+import { calculateMenuHeight } from "@design-system/ContextMenu/ContextMenu.utils";
+import { Portal } from "@gorhom/portal";
+import { memo, useCallback } from "react";
+import { StyleSheet } from "react-native";
+import { MessageContextMenuAboveMessageReactions } from "./conversation-message-context-menu-above-message-reactions";
+import { MessageContextMenuContainer } from "./conversation-message-context-menu-container";
+import { useMessageContextMenuItems } from "./conversation-message-context-menu.utils";
+
+export const MESSAGE_CONTEXT_MENU_SPACE_BETWEEN_ABOVE_MESSAGE_REACTIONS_AND_MESSAGE = 16;
+
+export const MessageContextMenu = memo(function MessageContextMenu() {
+ const messageContextMenuData = useMessageContextMenuStoreContext(
+ (state) => state.messageContextMenuData
+ );
+
+ if (!messageContextMenuData) {
+ return null;
+ }
+
+ return ;
+});
+
+const Content = memo(function Content(props: {
+ messageContextMenuData: NonNullable<
+ IMessageContextMenuStoreState["messageContextMenuData"]
+ >;
+}) {
+ const { messageContextMenuData } = props;
+
+ const {
+ messageId,
+ itemRectX,
+ itemRectY,
+ itemRectHeight,
+ itemRectWidth,
+ messageComponent,
+ } = messageContextMenuData;
+
+ const account = useCurrentAccount()!;
+ const topic = useCurrentConversationTopic();
+ const messageContextMenuStore = useMessageContextMenuStore();
+ const { data: conversation } = useConversationQuery(account, topic);
+ const { data: currentUserInboxId } = useCurrentAccountInboxId();
+ const { bySender } = useConversationMessageReactions(messageId!);
+
+ const message = getMessageById({
+ messageId,
+ topic,
+ })!;
+
+ const fromMe = messageIsFromCurrentUserV3({ message });
+ const menuItems = useMessageContextMenuItems({
+ messageId: messageId,
+ topic,
+ });
+ const menuHeight = calculateMenuHeight(menuItems.length);
+
+ const reactOnMessage = useReactOnMessage({
+ conversation: conversation!,
+ });
+ const removeReactionOnMessage = useRemoveReactionOnMessage({
+ conversation: conversation!,
+ });
+
+ const handlePressBackdrop = useCallback(() => {
+ messageContextMenuStore.getState().setMessageContextMenuData(null);
+ }, [messageContextMenuStore]);
+
+ const handleSelectReaction = useCallback(
+ (emoji: string) => {
+ const currentUserAlreadyReacted = bySender?.[currentUserInboxId!]?.some(
+ (reaction) => reaction.content === emoji
+ );
+
+ if (currentUserAlreadyReacted) {
+ removeReactionOnMessage({
+ messageId: messageId,
+ emoji,
+ });
+ } else {
+ reactOnMessage({ messageId: messageId, emoji });
+ }
+ messageContextMenuStore.getState().setMessageContextMenuData(null);
+ },
+ [
+ reactOnMessage,
+ messageId,
+ currentUserInboxId,
+ bySender,
+ removeReactionOnMessage,
+ messageContextMenuStore,
+ ]
+ );
+
+ const handleChooseMoreEmojis = useCallback(() => {
+ openMessageContextMenuEmojiPicker();
+ }, []);
+
+ const hasReactions = Boolean(bySender && Object.keys(bySender).length > 0);
+
+ return (
+ <>
+
+
+
+ {!!bySender && }
+
+
+
+ {/* Replace with rowGap when we refactored menu items */}
+
+
+ {messageComponent}
+
+ {/* Put back once we refactor the menu items */}
+ {/* */}
+
+
+
+
+
+
+
+ >
+ );
+});
+
+/**
+ * Tried different approaches to implement native context menu, but it's not
+ * working as expected. Still missing some pieces from libraries to achieve what we want
+ */
+
+// import { MessageContextMenu } from "@/components/Chat/Message/MessageContextMenu";
+// import ContextMenu from "react-native-context-menu-view";
+// import * as ContextMenu from "zeego/context-menu";
+
+{
+ /* {
+ console.log("onMenuWillShow");
+ }}
+ menuConfig={{
+ menuTitle: "Message Options",
+ menuItems: [
+ {
+ actionKey: "copy",
+ actionTitle: "Copy",
+ icon: {
+ type: "IMAGE_SYSTEM",
+ imageValue: {
+ systemName: "doc.on.doc",
+ },
+ },
+ },
+ {
+ actionKey: "delete",
+ actionTitle: "Delete",
+ menuAttributes: ["destructive"],
+ icon: {
+ type: "IMAGE_SYSTEM",
+ imageValue: {
+ systemName: "trash",
+ },
+ },
+ },
+ ],
+ }}
+ auxiliaryPreviewConfig={{
+ transitionEntranceDelay: "RECOMMENDED",
+ anchorPosition: "top",
+ // alignmentHorizontal: "previewTrailing",
+ verticalAnchorPosition: "top",
+ // height: 600,
+ // preferredHeight: {
+ // mode: "percentRelativeToWindow",
+ // percent: 50,
+ // },
+ }}
+ previewConfig={{ previewType: "CUSTOM" }}
+ // renderPreview={() => (
+ //
+ // {textContent}
+ //
+ // )}
+ isAuxiliaryPreviewEnabled={true}
+ renderPreview={() => (
+ //
+
+ {textContent}
+
+ )}
+ renderAuxiliaryPreview={() => (
+
+ 😅
+ 🤣
+ 😂
+ 🤩
+ 🤗
+ 🤔
+
+ )}
+ > */
+}
diff --git a/components/Chat/Message/message-context-menu/message-context-menu-utils.tsx b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.utils.tsx
similarity index 68%
rename from components/Chat/Message/message-context-menu/message-context-menu-utils.tsx
rename to features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.utils.tsx
index c0076b844..2cd39a7ef 100644
--- a/components/Chat/Message/message-context-menu/message-context-menu-utils.tsx
+++ b/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.utils.tsx
@@ -1,21 +1,19 @@
+import { showSnackbar } from "@/components/Snackbar/Snackbar.service";
+import { TableViewItemType } from "@/components/TableView/TableView";
+import { TableViewPicto } from "@/components/TableView/TableViewImage";
+import { useConversationComposerStore } from "@/features/conversation/conversation-composer/conversation-composer.store-context";
+import { useMessageContextMenuStore } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.store-context";
import {
getMessageById,
+ getMessageStringContent,
isRemoteAttachmentMessage,
isStaticAttachmentMessage,
isTransactionReferenceMessage,
- getMessageStringContent,
-} from "@/components/Chat/Message/message-utils";
-import { showSnackbar } from "@/components/Snackbar/Snackbar.service";
-import { TableViewItemType } from "@/components/TableView/TableView";
-import { TableViewPicto } from "@/components/TableView/TableViewImage";
-import {
- setCurrentConversationReplyToMessageId,
- resetMessageContextMenuData,
-} from "@/features/conversation/conversation-service";
+} from "@/features/conversation/conversation-message/conversation-message.utils";
import { translate } from "@/i18n";
import { captureErrorWithToast } from "@/utils/capture-error";
import Clipboard from "@react-native-clipboard/clipboard";
-import { MessageId } from "@xmtp/react-native-sdk";
+import { ConversationTopic, MessageId } from "@xmtp/react-native-sdk";
const CONTEXT_MENU_ACTIONS = {
REPLY: "Reply",
@@ -23,10 +21,16 @@ const CONTEXT_MENU_ACTIONS = {
SHARE_FRAME: "Share",
} as const;
-export function getMessageContextMenuItems(args: { messageId: MessageId }) {
- const { messageId } = args;
+export function useMessageContextMenuItems(args: {
+ messageId: MessageId;
+ topic: ConversationTopic;
+}) {
+ const { messageId, topic } = args;
+
+ const message = getMessageById({ messageId, topic });
- const message = getMessageById(messageId);
+ const composerStore = useConversationComposerStore();
+ const messageContextMenuStore = useMessageContextMenuStore();
if (!message) {
captureErrorWithToast(
@@ -41,8 +45,8 @@ export function getMessageContextMenuItems(args: { messageId: MessageId }) {
items.push({
title: translate("reply"),
action: () => {
- setCurrentConversationReplyToMessageId(messageId);
- resetMessageContextMenuData();
+ composerStore.getState().setReplyToMessageId(messageId);
+ messageContextMenuStore.getState().setMessageContextMenuData(null);
},
id: CONTEXT_MENU_ACTIONS.REPLY,
rightView: ,
@@ -67,7 +71,7 @@ export function getMessageContextMenuItems(args: { messageId: MessageId }) {
});
}
- resetMessageContextMenuData();
+ messageContextMenuStore.getState().setMessageContextMenuData(null);
},
});
}
diff --git a/components/Chat/Message/message-gestures.tsx b/features/conversation/conversation-message/conversation-message-gestures.tsx
similarity index 100%
rename from components/Chat/Message/message-gestures.tsx
rename to features/conversation/conversation-message/conversation-message-gestures.tsx
diff --git a/features/conversation/conversation-message/conversation-message-layout.tsx b/features/conversation/conversation-message/conversation-message-layout.tsx
new file mode 100644
index 000000000..d0acd7919
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-layout.tsx
@@ -0,0 +1,50 @@
+import { useSelect } from "@/data/store/storeHelpers";
+import { VStack } from "@/design-system/VStack";
+import { MessageContainer } from "@/features/conversation/conversation-message/conversation-message-container";
+import { MessageContentContainer } from "@/features/conversation/conversation-message/conversation-message-content-container";
+import { V3MessageSenderAvatar } from "@/features/conversation/conversation-message/conversation-message-sender-avatar";
+import { useMessageContextStoreContext } from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { useAppTheme } from "@/theme/useAppTheme";
+import { ReactNode, memo } from "react";
+
+type IConversationMessageLayoutProps = {
+ children: ReactNode;
+};
+
+export const ConversationMessageLayout = memo(
+ function ConversationMessageLayout({
+ children,
+ }: IConversationMessageLayoutProps) {
+ const { theme } = useAppTheme();
+
+ const { senderAddress, fromMe, hasNextMessageInSeries } =
+ useMessageContextStoreContext(
+ useSelect(["senderAddress", "fromMe", "hasNextMessageInSeries"])
+ );
+
+ return (
+
+
+ {!fromMe && (
+ <>
+ {!hasNextMessageInSeries ? (
+
+ ) : (
+
+ )}
+
+ >
+ )}
+
+ {children}
+
+
+
+ );
+ }
+);
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.service..ts
similarity index 90%
rename from components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts
rename to features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.service..ts
index 6a3a4da80..0e86997a7 100644
--- a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts
+++ b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.service..ts
@@ -1,10 +1,10 @@
import { createBottomSheetModalRef } from "@design-system/BottomSheet/BottomSheet.utils";
-import { RolledUpReactions } from "../MessageReactions.types";
+import { RolledUpReactions } from "../conversation-message-reactions.types";
import {
resetMessageReactionsStore,
useMessageReactionsStore,
-} from "./MessageReactions.store";
+} from "./conversation-message-reaction-drawer.store";
export const bottomSheetModalRef = createBottomSheetModalRef();
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.store.tsx
similarity index 91%
rename from components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx
rename to features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.store.tsx
index 5eacc19d0..6b6e3fa23 100644
--- a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx
+++ b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.store.tsx
@@ -1,6 +1,6 @@
import { create } from "zustand";
-import { RolledUpReactions } from "../MessageReactions.types";
+import { RolledUpReactions } from "../conversation-message-reactions.types";
const initialMessageReactionsState: RolledUpReactions = {
totalCount: 0,
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.tsx
similarity index 99%
rename from components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx
rename to features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.tsx
index 3727763ed..481a14ba3 100644
--- a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx
+++ b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer.tsx
@@ -16,7 +16,7 @@ import {
bottomSheetModalRef,
resetMessageReactionsDrawer,
useMessageReactionsRolledUpReactions,
-} from "./MessageReactionsDrawer.service";
+} from "./conversation-message-reaction-drawer.service.";
export const MessageReactionsDrawer = memo(function MessageReactionsDrawer() {
const { theme, themed } = useAppTheme();
diff --git a/components/Chat/Message/MessageReactions/MessageReactions.tsx b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.tsx
similarity index 61%
rename from components/Chat/Message/MessageReactions/MessageReactions.tsx
rename to features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.tsx
index e856c6fee..dcfafb705 100644
--- a/components/Chat/Message/MessageReactions/MessageReactions.tsx
+++ b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.tsx
@@ -1,5 +1,8 @@
+import { useSelect } from "@/data/store/storeHelpers";
+import { useMessageContextStoreContext } from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { useConversationMessageReactions } from "@/features/conversation/conversation-message/conversation-message.utils";
import { useCurrentAccount, useProfilesStore } from "@data/store/accountsStore";
-import { HStack } from "@design-system/HStack";
+import { AnimatedHStack, HStack } from "@design-system/HStack";
import { Text } from "@design-system/Text";
import { VStack } from "@design-system/VStack";
import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
@@ -10,71 +13,78 @@ import {
} from "@utils/profile";
import { memo, useCallback, useMemo } from "react";
import { TouchableHighlight, ViewStyle } from "react-native";
-import { useConversationMessageReactions } from "@/components/Chat/Message/message-utils";
-import { useMessageContextStoreContext } from "@/components/Chat/Message/stores/message-store";
-import { useSelect } from "@/data/store/storeHelpers";
-import { RolledUpReactions, SortedReaction } from "./MessageReactions.types";
-import { openMessageReactionsDrawer } from "./MessageReactionsDrawer/MessageReactionsDrawer.service";
+import { openMessageReactionsDrawer } from "./conversation-message-reaction-drawer/conversation-message-reaction-drawer.service.";
+import {
+ RolledUpReactions,
+ SortedReaction,
+} from "./conversation-message-reactions.types";
const MAX_REACTION_EMOJIS_SHOWN = 3;
-export const MessageReactions = memo(function MessageReactions() {
- const { themed, theme } = useAppTheme();
+export const ConversationMessageReactions = memo(
+ function ConversationMessageReactions() {
+ const { themed, theme } = useAppTheme();
- const { fromMe } = useMessageContextStoreContext(
- useSelect(["messageId", "fromMe"])
- );
-
- const rolledUpReactions = useMessageReactionsRolledUp();
+ const { fromMe } = useMessageContextStoreContext(useSelect(["fromMe"]));
- const handlePressContainer = useCallback(() => {
- openMessageReactionsDrawer(rolledUpReactions);
- }, [rolledUpReactions]);
+ const rolledUpReactions = useMessageReactionsRolledUp();
- if (rolledUpReactions.totalCount === 0) {
- return null;
- }
+ const { hasNextMessageInSeries } = useMessageContextStoreContext(
+ useSelect(["hasNextMessageInSeries"])
+ );
- return (
-
- {
+ openMessageReactionsDrawer(rolledUpReactions);
+ }, [rolledUpReactions]);
+
+ if (rolledUpReactions.totalCount === 0) {
+ return null;
+ }
+
+ return (
+
-
-
- {rolledUpReactions.preview
- .slice(0, MAX_REACTION_EMOJIS_SHOWN)
- .map((reaction, index) => (
- {reaction.content}
- ))}
-
- {rolledUpReactions.totalCount > 1 && (
-
- {rolledUpReactions.totalCount}
-
- )}
-
-
-
- );
-});
+
+
+ {rolledUpReactions.preview
+ .slice(0, MAX_REACTION_EMOJIS_SHOWN)
+ .map((reaction, index) => (
+ {reaction.content}
+ ))}
+
+ {rolledUpReactions.totalCount > 1 && (
+
+ {rolledUpReactions.totalCount}
+
+ )}
+
+
+
+ );
+ }
+);
function useMessageReactionsRolledUp() {
const { messageId } = useMessageContextStoreContext(useSelect(["messageId"]));
diff --git a/components/Chat/Message/MessageReactions/MessageReactions.types.ts b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.types.ts
similarity index 94%
rename from components/Chat/Message/MessageReactions/MessageReactions.types.ts
rename to features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.types.ts
index 0b0a9c991..d580820ac 100644
--- a/components/Chat/Message/MessageReactions/MessageReactions.types.ts
+++ b/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions.types.ts
@@ -1,6 +1,6 @@
import { ReactionContent } from "@xmtp/react-native-sdk";
-export type MessageReactions = {
+export type ConversationMessageReactions = {
[senderAddress: string]: ReactionContent[];
};
diff --git a/features/conversation/conversation-message/conversation-message-repliable.tsx b/features/conversation/conversation-message/conversation-message-repliable.tsx
new file mode 100644
index 000000000..ede532821
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-repliable.tsx
@@ -0,0 +1,99 @@
+import { Icon } from "@design-system/Icon/Icon";
+import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
+import { Haptics } from "@utils/haptics";
+import { memo, useRef } from "react";
+import { Animated, ViewStyle } from "react-native";
+import { Swipeable } from "react-native-gesture-handler";
+
+// TODO: Switch to import ReanimatedSwipeable from "react-native-gesture-handler/ReanimatedSwipeable"; once we upgrade to Expo SDK 52
+
+// TODO: Once we have Expo SDK 52, let's redo to be more performant and use SharedValue to trigger the onReply etc...
+
+// TODO: When we'll use ReanimatedSwipeable, we'll be able to listen to progress and trigger haptic once the treshold to reply is reached
+
+type IProps = {
+ children: React.ReactNode;
+ onReply: () => void;
+};
+
+export const ConversationMessageRepliable = memo(
+ function ConversationMessageRepliable({ children, onReply }: IProps) {
+ const { themed, theme } = useAppTheme();
+
+ const swipeableRef = useRef(null);
+ const dragOffsetFromLeftEdge = theme.spacing.xs;
+ const xTresholdToReply = theme.spacing["3xl"];
+
+ return (
+ {
+ return (
+ // TODO: Switch to AnimatedVStack once we upgrade to Expo SDK 52
+
+
+
+ );
+ }}
+ onSwipeableWillClose={() => {
+ const translation = swipeableRef.current?.state.rowTranslation;
+ const translationValue = (translation as any)._value;
+ const v = translationValue - dragOffsetFromLeftEdge;
+ if (translation && v > xTresholdToReply) {
+ Haptics.successNotificationAsync();
+ onReply();
+ }
+ }}
+ ref={swipeableRef}
+ >
+ {children}
+
+ );
+ }
+);
+
+const $container: ThemedStyle = ({ colors, spacing }) => ({
+ width: "100%",
+ flexDirection: "row",
+ overflow: "visible",
+});
+
+const $childrenContainer: ThemedStyle = ({ colors, spacing }) => ({
+ width: "100%",
+ flexDirection: "row",
+});
diff --git a/components/Chat/Message/MessageSenderAvatar.tsx b/features/conversation/conversation-message/conversation-message-sender-avatar.tsx
similarity index 100%
rename from components/Chat/Message/MessageSenderAvatar.tsx
rename to features/conversation/conversation-message/conversation-message-sender-avatar.tsx
diff --git a/components/Chat/Message/MessageSender.tsx b/features/conversation/conversation-message/conversation-message-sender.tsx
similarity index 100%
rename from components/Chat/Message/MessageSender.tsx
rename to features/conversation/conversation-message/conversation-message-sender.tsx
diff --git a/components/Chat/Message/components/message-space-between-messages.tsx b/features/conversation/conversation-message/conversation-message-space-between-messages.tsx
similarity index 100%
rename from components/Chat/Message/components/message-space-between-messages.tsx
rename to features/conversation/conversation-message/conversation-message-space-between-messages.tsx
diff --git a/components/Chat/Message/MessageStatusDumb.tsx b/features/conversation/conversation-message/conversation-message-status-dumb.tsx
similarity index 100%
rename from components/Chat/Message/MessageStatusDumb.tsx
rename to features/conversation/conversation-message/conversation-message-status-dumb.tsx
diff --git a/features/conversation/conversation-message/conversation-message-status.tsx b/features/conversation/conversation-message/conversation-message-status.tsx
new file mode 100644
index 000000000..d1a25540c
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-status.tsx
@@ -0,0 +1,115 @@
+/**
+ * TODO
+ */
+// import { DecodedMessageWithCodecsType } from "@/utils/xmtpRN/client.types";
+// import { textSecondaryColor } from "@styles/colors";
+// import React, { useEffect, useRef, useState } from "react";
+// import { StyleSheet, View, useColorScheme } from "react-native";
+// import Animated, {
+// Easing,
+// useAnimatedStyle,
+// useSharedValue,
+// withTiming,
+// } from "react-native-reanimated";
+
+// type Props = {
+// message: DecodedMessageWithCodecsType;
+// };
+
+// const statusMapping: {
+// [key: string]: string | undefined;
+// } = {
+// sent: "Sent",
+// delivered: "Sent",
+// error: "Failed",
+// sending: "Sending",
+// prepared: "Sending",
+// seen: "Read",
+// };
+
+// export default function MessageStatus({ message }: Props) {
+// const styles = useStyles();
+// const prevStatusRef = useRef(message.status);
+// const isSentOrDelivered =
+// message.status === "sent" || message.status === "delivered";
+// const isLatestSettledFromMe = message.isLatestSettledFromMe;
+
+// const [renderText, setRenderText] = useState(false);
+// const opacity = useSharedValue(message.isLatestSettledFromMe ? 1 : 0);
+// const height = useSharedValue(message.isLatestSettledFromMe ? 22 : 0);
+// const scale = useSharedValue(message.isLatestSettledFromMe ? 1 : 0);
+
+// const timingConfig = {
+// duration: 200,
+// easing: Easing.inOut(Easing.quad),
+// };
+
+// const animatedStyle = useAnimatedStyle(() => ({
+// opacity: opacity.value,
+// height: height.value,
+// transform: [{ scale: scale.value }],
+// }));
+
+// useEffect(
+// () => {
+// const prevStatus = prevStatusRef.current;
+// prevStatusRef.current = message.status;
+
+// setTimeout(() => {
+// requestAnimationFrame(() => {
+// if (
+// isSentOrDelivered &&
+// (prevStatus === "sending" || prevStatus === "prepared")
+// ) {
+// opacity.value = withTiming(1, timingConfig);
+// height.value = withTiming(22, timingConfig);
+// scale.value = withTiming(1, timingConfig);
+// setRenderText(true);
+// } else if (isSentOrDelivered && !isLatestSettledFromMe) {
+// opacity.value = withTiming(0, timingConfig);
+// height.value = withTiming(0, timingConfig);
+// scale.value = withTiming(0, timingConfig);
+// setTimeout(() => setRenderText(false), timingConfig.duration);
+// } else if (isLatestSettledFromMe) {
+// opacity.value = 1;
+// height.value = 22;
+// scale.value = 1;
+// setRenderText(true);
+// }
+// });
+// }, 100);
+// },
+// // eslint-disable-next-line react-hooks/exhaustive-deps
+// [isLatestSettledFromMe, isSentOrDelivered]
+// );
+
+// return (
+// message.fromMe &&
+// message.status !== "sending" &&
+// message.status !== "prepared" && (
+//
+//
+//
+// {renderText && statusMapping[message.status]}
+//
+//
+//
+// )
+// );
+// }
+
+// const useStyles = () => {
+// const colorScheme = useColorScheme();
+// return StyleSheet.create({
+// container: {
+// overflow: "hidden",
+// },
+// contentContainer: {
+// paddingTop: 5,
+// },
+// statusText: {
+// fontSize: 12,
+// color: textSecondaryColor(colorScheme),
+// },
+// });
+// };
diff --git a/components/Chat/Message/components/message-text.tsx b/features/conversation/conversation-message/conversation-message-text.tsx
similarity index 100%
rename from components/Chat/Message/components/message-text.tsx
rename to features/conversation/conversation-message/conversation-message-text.tsx
diff --git a/features/conversation/conversation-message/conversation-message-timestamp.tsx b/features/conversation/conversation-message/conversation-message-timestamp.tsx
new file mode 100644
index 000000000..09e4aa63e
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message-timestamp.tsx
@@ -0,0 +1,167 @@
+import {
+ useMessageContextStore,
+ useMessageContextStoreContext,
+} from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { AnimatedHStack } from "@design-system/HStack";
+import { AnimatedText, Text } from "@design-system/Text";
+import { getTextStyle } from "@design-system/Text/Text.utils";
+import { AnimatedVStack } from "@design-system/VStack";
+import { SICK_DAMPING, SICK_STIFFNESS } from "@theme/animations";
+import { useAppTheme } from "@theme/useAppTheme";
+import { getLocalizedTime, getRelativeDate } from "@utils/date";
+import { flattenStyles } from "@utils/styles";
+import { memo, useEffect } from "react";
+import {
+ interpolate,
+ useAnimatedStyle,
+ useDerivedValue,
+ useSharedValue,
+ withSpring,
+} from "react-native-reanimated";
+
+const StandaloneTime = memo(function StandaloneTime({
+ messageTime,
+}: {
+ messageTime: string;
+}) {
+ const { themed, theme } = useAppTheme();
+ const showTimeAV = useSharedValue(0);
+ const messageStore = useMessageContextStore();
+
+ useEffect(() => {
+ const unsubscribe = messageStore.subscribe(
+ (state) => state.isShowingTime,
+ (isShowingTime) => {
+ showTimeAV.value = isShowingTime ? 1 : 0;
+ }
+ );
+ return () => unsubscribe();
+ }, [messageStore, showTimeAV]);
+
+ const textHeight = flattenStyles(
+ getTextStyle(themed, { preset: "smaller" })
+ ).lineHeight;
+
+ const showTimeProgressAV = useDerivedValue(() => {
+ return withSpring(showTimeAV.value ? 1 : 0, {
+ damping: SICK_DAMPING,
+ stiffness: SICK_STIFFNESS,
+ });
+ });
+
+ const timeAnimatedStyle = useAnimatedStyle(
+ () => ({
+ height: interpolate(
+ showTimeProgressAV.value,
+ [0, 1],
+ [0, textHeight || 14]
+ ),
+ opacity: interpolate(showTimeProgressAV.value, [0, 1], [0, 1]),
+ marginVertical: interpolate(
+ showTimeProgressAV.value,
+ [0, 1],
+ [0, theme.spacing.sm]
+ ),
+ transform: [
+ { scale: showTimeProgressAV.value },
+ {
+ translateY: interpolate(
+ showTimeProgressAV.value,
+ [0, 1],
+ [theme.spacing.xl, 0]
+ ),
+ },
+ ],
+ }),
+ [textHeight]
+ );
+
+ return (
+
+
+ {messageTime}
+
+
+ );
+});
+
+const InlineTime = memo(function InlineTime({
+ messageTime,
+ messageDate,
+}: {
+ messageTime: string;
+ messageDate: string;
+}) {
+ const { theme } = useAppTheme();
+ const showTimeAV = useSharedValue(0);
+ const messageStore = useMessageContextStore();
+
+ useEffect(() => {
+ const unsubscribe = messageStore.subscribe(
+ (state) => state.isShowingTime,
+ (isShowingTime) => {
+ showTimeAV.value = isShowingTime ? 1 : 0;
+ }
+ );
+ return () => unsubscribe();
+ }, [messageStore, showTimeAV]);
+
+ const timeInlineAnimatedStyle = useAnimatedStyle(() => ({
+ display: showTimeAV.value ? "flex" : "none",
+ opacity: withSpring(showTimeAV.value ? 1 : 0, {
+ damping: SICK_DAMPING,
+ stiffness: SICK_STIFFNESS,
+ }),
+ }));
+
+ return (
+
+
+ {messageDate}
+
+
+ {messageTime}
+
+
+ );
+});
+
+export const ConversationMessageTimestamp = memo(
+ function ConversationMessageTimestamp() {
+ const [sentAt, showDateChange] = useMessageContextStoreContext((s) => [
+ s.sentAt,
+ s.showDateChange,
+ ]);
+
+ const messageTime = sentAt ? getLocalizedTime(sentAt) : "";
+
+ if (showDateChange) {
+ const messageDate = getRelativeDate(sentAt);
+ return ;
+ }
+
+ return ;
+ }
+);
diff --git a/features/conversation/conversation-message/conversation-message.store-context.tsx b/features/conversation/conversation-message/conversation-message.store-context.tsx
new file mode 100644
index 000000000..9ea22375f
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message.store-context.tsx
@@ -0,0 +1,112 @@
+/**
+ * This store/context is to avoid prop drilling in message components.
+ */
+
+import { convertNanosecondsToMilliseconds } from "@/utils/date";
+import { hasNextMessageInSeries } from "@/features/conversation/utils/has-next-message-in-serie";
+import { hasPreviousMessageInSeries } from "@/features/conversation/utils/has-previous-message-in-serie";
+import { messageIsFromCurrentUserV3 } from "@/features/conversation/utils/message-is-from-current-user";
+import { messageShouldShowDateChange } from "@/features/conversation/utils/message-should-show-date-change";
+import { DecodedMessageWithCodecsType } from "@/utils/xmtpRN/client.types";
+import { InboxId, MessageId } from "@xmtp/react-native-sdk";
+import { createContext, memo, useContext, useEffect, useRef } from "react";
+import { createStore, useStore } from "zustand";
+import { subscribeWithSelector } from "zustand/middleware";
+
+type IMessageContextStoreProps = {
+ message: DecodedMessageWithCodecsType;
+ previousMessage: DecodedMessageWithCodecsType | undefined;
+ nextMessage: DecodedMessageWithCodecsType | undefined;
+};
+
+type IMessageContextStoreState = IMessageContextStoreProps & {
+ messageId: MessageId;
+ hasNextMessageInSeries: boolean;
+ hasPreviousMessageInSeries: boolean;
+ fromMe: boolean;
+ sentAt: number;
+ showDateChange: boolean;
+ senderAddress: InboxId;
+ isHighlighted: boolean;
+ isShowingTime: boolean;
+};
+
+type IMessageContextStoreProviderProps =
+ React.PropsWithChildren;
+
+type IMessageContextStore = ReturnType;
+
+export const MessageContextStoreProvider = memo(
+ ({ children, ...props }: IMessageContextStoreProviderProps) => {
+ const storeRef = useRef();
+
+ if (!storeRef.current) {
+ storeRef.current = createMessageContextStore(props);
+ }
+
+ // Make sure to update the store if a props change
+ useEffect(() => {
+ storeRef.current?.setState({
+ ...getStoreStateBasedOnProps(props),
+ });
+ }, [props]);
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+function getStoreStateBasedOnProps(props: IMessageContextStoreProps) {
+ return {
+ ...props,
+ messageId: props.message.id as MessageId,
+ hasNextMessageInSeries: hasNextMessageInSeries({
+ currentMessage: props.message,
+ nextMessage: props.nextMessage,
+ }),
+ hasPreviousMessageInSeries: hasPreviousMessageInSeries({
+ currentMessage: props.message,
+ previousMessage: props.previousMessage,
+ }),
+ fromMe: messageIsFromCurrentUserV3({
+ message: props.message,
+ }),
+ showDateChange: messageShouldShowDateChange({
+ message: props.message,
+ previousMessage: props.previousMessage,
+ }),
+ sentAt: convertNanosecondsToMilliseconds(props.message.sentNs),
+ senderAddress: props.message.senderAddress,
+ isHighlighted: false,
+ isShowingTime: false,
+ };
+}
+
+const createMessageContextStore = (initProps: IMessageContextStoreProps) => {
+ return createStore()(
+ subscribeWithSelector((set) => getStoreStateBasedOnProps(initProps))
+ );
+};
+
+const MessageContextStoreContext = createContext(
+ null
+);
+
+export function useMessageContextStoreContext(
+ selector: (state: IMessageContextStoreState) => T
+): T {
+ const store = useContext(MessageContextStoreContext);
+ if (!store)
+ throw new Error("Missing MessageContextStore.Provider in the tree");
+ return useStore(store, selector);
+}
+
+export function useMessageContextStore() {
+ const store = useContext(MessageContextStoreContext);
+ if (!store)
+ throw new Error(`Missing MessageContextStore.Provider in the tree`);
+ return store;
+}
diff --git a/features/conversation/conversation-message/conversation-message.tsx b/features/conversation/conversation-message/conversation-message.tsx
new file mode 100644
index 000000000..e5001e217
--- /dev/null
+++ b/features/conversation/conversation-message/conversation-message.tsx
@@ -0,0 +1,77 @@
+import { MessageChatGroupUpdate } from "@/features/conversation/conversation-message/conversation-message-content-types/conversation-message-chat-group-update";
+import { MessageRemoteAttachment } from "@/features/conversation/conversation-message/conversation-message-content-types/conversation-message-remote-attachment";
+import { MessageReply } from "@/features/conversation/conversation-message/conversation-message-content-types/conversation-message-reply";
+import { MessageSimpleText } from "@/features/conversation/conversation-message/conversation-message-content-types/conversation-message-simple-text";
+import { MessageStaticAttachment } from "@/features/conversation/conversation-message/conversation-message-content-types/conversation-message-static-attachment";
+import {
+ isCoinbasePaymentMessage,
+ isGroupUpdatedMessage,
+ isReactionMessage,
+ isReadReceiptMessage,
+ isRemoteAttachmentMessage,
+ isReplyMessage,
+ isStaticAttachmentMessage,
+ isTextMessage,
+ isTransactionReferenceMessage,
+} from "@/features/conversation/conversation-message/conversation-message.utils";
+import { captureError } from "@/utils/capture-error";
+import { DecodedMessageWithCodecsType } from "@/utils/xmtpRN/client";
+import { memo } from "react";
+
+export const ConversationMessage = memo(
+ function ConversationMessage(props: {
+ message: DecodedMessageWithCodecsType;
+ }) {
+ const { message } = props;
+
+ if (isTextMessage(message)) {
+ return ;
+ }
+
+ if (isGroupUpdatedMessage(message)) {
+ return ;
+ }
+
+ if (isReplyMessage(message)) {
+ return ;
+ }
+
+ if (isRemoteAttachmentMessage(message)) {
+ return ;
+ }
+
+ if (isStaticAttachmentMessage(message)) {
+ return ;
+ }
+
+ if (isReactionMessage(message)) {
+ // Handle in message
+ return null;
+ }
+
+ if (isReadReceiptMessage(message)) {
+ // TODO
+ return null;
+ }
+
+ if (isTransactionReferenceMessage(message)) {
+ // TODO
+ return null;
+ }
+
+ if (isCoinbasePaymentMessage(message)) {
+ // TODO
+ return null;
+ }
+
+ const _ensureNever = message;
+
+ captureError(new Error(`Unknown message type ${message.contentTypeId}`));
+
+ return null;
+ },
+ // For now it's okay. For performance. A message shouldn't change
+ (prevProps, nextProps) => {
+ return prevProps.message.id === nextProps.message.id;
+ }
+);
diff --git a/components/Chat/Message/message-utils.tsx b/features/conversation/conversation-message/conversation-message.utils.tsx
similarity index 76%
rename from components/Chat/Message/message-utils.tsx
rename to features/conversation/conversation-message/conversation-message.utils.tsx
index d8d222a99..f12d535cc 100644
--- a/components/Chat/Message/message-utils.tsx
+++ b/features/conversation/conversation-message/conversation-message.utils.tsx
@@ -1,25 +1,24 @@
-import {
- getCurrentConversationMessages,
- getCurrentConversationTopic,
-} from "@/features/conversation/conversation-service";
+import { useCurrentConversationTopic } from "../conversation.store-context";
import { getConversationMessageQueryOptions } from "@/queries/useConversationMessage";
-import { useConversationMessages } from "@/queries/useConversationMessages";
+import {
+ getConversationMessages,
+ useConversationMessages,
+} from "@/queries/useConversationMessages";
+import { CoinbaseMessagingPaymentCodec } from "@/utils/xmtpRN/content-types/coinbasePayment";
+import { getMessageContentType } from "@/utils/xmtpRN/content-types/content-types";
import {
getCurrentAccount,
useCurrentAccount,
} from "@data/store/accountsStore";
-import { queryClient } from "@queries/queryClient";
import { useQuery } from "@tanstack/react-query";
-import { getReadableProfile } from "@utils/str";
+import { getReadableProfile } from "@utils/getReadableProfile";
import {
DecodedMessageWithCodecsType,
SupportedCodecsType,
} from "@utils/xmtpRN/client";
-import { getMessageContentType } from "@utils/xmtpRN/contentTypes";
-import { CoinbaseMessagingPaymentCodec } from "@utils/xmtpRN/contentTypes/coinbasePayment";
-import { getInboxId } from "@utils/xmtpRN/signIn";
import { TransactionReferenceCodec } from "@xmtp/content-type-transaction-reference";
import {
+ ConversationTopic,
DecodedMessage,
GroupUpdatedCodec,
MessageId,
@@ -35,10 +34,6 @@ import {
TextCodec,
} from "@xmtp/react-native-sdk";
-/**
- * TODO: Move this somewhere else? Maybe to @xmtp/react-native-sdk? Or to @utils/xmtpRN/messages.ts?
- */
-
export function isTextMessage(
message: DecodedMessageWithCodecsType
): message is DecodedMessage {
@@ -98,36 +93,19 @@ export function useMessageSenderReadableProfile(
return getReadableProfile(currentAccountAdress, message.senderAddress);
}
-// TMP until we move this into an account store or something like that
-// Maybe instead worth moving into account store?
-export function useCurrentAccountInboxId() {
- return useQuery(getCurrentAccountInboxIdQueryOptions());
-}
-
-function getCurrentAccountInboxIdQueryOptions() {
- const currentAccount = getCurrentAccount();
- return {
- queryKey: ["inboxId", currentAccount],
- queryFn: () => getInboxId(currentAccount!),
- enabled: !!currentAccount,
- };
-}
-
-export function getCurrentUserAccountInboxId() {
- return queryClient.getQueryData(
- getCurrentAccountInboxIdQueryOptions().queryKey
- );
-}
-export function convertNanosecondsToMilliseconds(nanoseconds: number) {
- return nanoseconds / 1000000;
-}
-
-export function getMessageById(messageId: MessageId) {
- const conversationMessages = getCurrentConversationMessages();
- if (!conversationMessages) {
+export function getMessageById({
+ messageId,
+ topic,
+}: {
+ messageId: MessageId;
+ topic: ConversationTopic;
+}) {
+ const currentAccount = getCurrentAccount()!;
+ const messages = getConversationMessages(currentAccount, topic);
+ if (!messages) {
return null;
}
- return conversationMessages.byId[messageId];
+ return messages.byId[messageId];
}
export function getMessageStringContent(
@@ -198,9 +176,15 @@ export function getMessageStringContent(
return "";
}
-export function useConversationMessageById(messageId: MessageId) {
+export function useConversationMessageById({
+ messageId,
+ topic,
+}: {
+ messageId: MessageId;
+ topic: ConversationTopic;
+}) {
const currentAccount = useCurrentAccount()!;
- const messages = getCurrentConversationMessages();
+ const { data: messages } = useConversationMessages(currentAccount, topic);
const cachedMessage = messages?.byId[messageId];
@@ -221,7 +205,7 @@ export function useConversationMessageById(messageId: MessageId) {
export function useConversationMessageReactions(messageId: MessageId) {
const currentAccount = useCurrentAccount()!;
- const topic = getCurrentConversationTopic()!;
+ const topic = useCurrentConversationTopic();
const { data: messages } = useConversationMessages(currentAccount, topic);
diff --git a/features/conversation/conversation-messages-list.tsx b/features/conversation/conversation-messages-list.tsx
new file mode 100644
index 000000000..c9b92c79c
--- /dev/null
+++ b/features/conversation/conversation-messages-list.tsx
@@ -0,0 +1,57 @@
+import { MessageSpaceBetweenMessages } from "@/features/conversation/conversation-message/conversation-message-space-between-messages";
+import { useAppTheme } from "@/theme/useAppTheme";
+import { MessageId } from "@xmtp/react-native-sdk";
+import { ReactElement, memo } from "react";
+import { FlatListProps, Platform } from "react-native";
+import Animated, { AnimatedProps } from "react-native-reanimated";
+
+export const ConversationMessagesList = memo(function ConversationMessagesList(
+ props: Omit<
+ AnimatedProps>,
+ "renderItem" | "data"
+ > & {
+ messageIds: MessageId[];
+ renderMessage: (args: {
+ messageId: MessageId;
+ index: number;
+ }) => ReactElement;
+ }
+) {
+ const { messageIds, renderMessage, ...rest } = props;
+
+ const { theme } = useAppTheme();
+
+ return (
+ // @ts-ignore
+
+ renderMessage({
+ messageId: item,
+ index,
+ })
+ }
+ itemLayoutAnimation={theme.animation.reanimatedLayoutSpringTransition}
+ keyboardDismissMode="interactive"
+ automaticallyAdjustContentInsets={false}
+ contentInsetAdjustmentBehavior="never"
+ keyExtractor={keyExtractor}
+ keyboardShouldPersistTaps="handled"
+ ItemSeparatorComponent={() => }
+ showsVerticalScrollIndicator={Platform.OS === "ios"} // Size glitch on Android
+ pointerEvents="auto"
+ /**
+ * Causes a glitch on Android, no sure we need it for now
+ */
+ // maintainVisibleContentPosition={{
+ // minIndexForVisible: 0,
+ // autoscrollToTopThreshold: 100,
+ // }}
+ // estimatedListSize={Dimensions.get("screen")}
+ {...rest}
+ />
+ );
+});
+
+const keyExtractor = (messageId: MessageId) => messageId;
diff --git a/features/conversations/components/NewConversationTitle.tsx b/features/conversation/conversation-new-dm-header-title.tsx
similarity index 74%
rename from features/conversations/components/NewConversationTitle.tsx
rename to features/conversation/conversation-new-dm-header-title.tsx
index b1eafff5c..579de38c8 100644
--- a/features/conversations/components/NewConversationTitle.tsx
+++ b/features/conversation/conversation-new-dm-header-title.tsx
@@ -1,38 +1,18 @@
-import { useCallback } from "react";
-import { useRouter } from "@navigation/useNavigation";
-import { NativeStackNavigationProp } from "@react-navigation/native-stack";
-import { NavigationParamList } from "@screens/Navigation/Navigation";
-import { usePreferredName } from "@hooks/usePreferredName";
+import { ConversationTitle } from "@/features/conversation/conversation-title";
import Avatar from "@components/Avatar";
import { usePreferredAvatarUri } from "@hooks/usePreferredAvatarUri";
+import { usePreferredName } from "@hooks/usePreferredName";
+import { useProfileSocials } from "@hooks/useProfileSocials";
+import { useRouter } from "@navigation/useNavigation";
import { AvatarSizes } from "@styles/sizes";
import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
+import { useCallback } from "react";
import { ImageStyle, Platform } from "react-native";
-import { useProfileSocials } from "@hooks/useProfileSocials";
-import { ConversationTitleDumb } from "@components/Conversation/ConversationTitleDumb";
type NewConversationTitleProps = {
peerAddress: string;
};
-type UseUserInteractionProps = {
- peerAddress?: string;
- navigation: NativeStackNavigationProp;
-};
-
-const useUserInteraction = ({
- navigation,
- peerAddress,
-}: UseUserInteractionProps) => {
- const onPress = useCallback(() => {
- if (peerAddress) {
- navigation.push("Profile", { address: peerAddress });
- }
- }, [navigation, peerAddress]);
-
- return { onPress };
-};
-
export const NewConversationTitle = ({
peerAddress,
}: NewConversationTitleProps) => {
@@ -40,10 +20,11 @@ export const NewConversationTitle = ({
const { themed } = useAppTheme();
- const { onPress } = useUserInteraction({
- peerAddress,
- navigation,
- });
+ const onPress = useCallback(() => {
+ if (peerAddress) {
+ navigation.push("Profile", { address: peerAddress });
+ }
+ }, [navigation, peerAddress]);
const { isLoading } = useProfileSocials(peerAddress ?? "");
@@ -56,7 +37,7 @@ export const NewConversationTitle = ({
if (!displayAvatar) return null;
return (
- void;
+ isBlockedPeer: boolean;
+};
+
+export function ConversationNewDmNoMessagesPlaceholder(
+ args: IConversationNewDmNoMessagesPlaceholderProps
+) {
+ const { peerAddress, onSendWelcomeMessage, isBlockedPeer } = args;
+
+ const { theme } = useAppTheme();
+
+ const peerPreferredName = usePreferredName(peerAddress);
+
+ if (isBlockedPeer) {
+ // TODO
+ return null;
+ }
+
+ return (
+
+
+
+ {translate("this_is_the_beginning_of_your_conversation_with", {
+ name: peerPreferredName,
+ })}
+
+
+
+
+ );
+}
diff --git a/features/conversation/conversation-new-dm.tsx b/features/conversation/conversation-new-dm.tsx
new file mode 100644
index 000000000..be69e4dea
--- /dev/null
+++ b/features/conversation/conversation-new-dm.tsx
@@ -0,0 +1,141 @@
+import { showSnackbar } from "@/components/Snackbar/Snackbar.service";
+import { getCurrentAccount } from "@/data/store/accountsStore";
+import { Composer } from "@/features/conversation/conversation-composer/conversation-composer";
+import { ConversationComposerStoreProvider } from "@/features/conversation/conversation-composer/conversation-composer.store-context";
+import { KeyboardFiller } from "@/features/conversation/conversation-keyboard-filler";
+import { NewConversationTitle } from "@/features/conversation/conversation-new-dm-header-title";
+import { ConversationNewDmNoMessagesPlaceholder } from "@/features/conversation/conversation-new-dm-no-messages-placeholder";
+import {
+ ISendMessageParams,
+ sendMessage,
+} from "@/features/conversation/hooks/use-send-message";
+import { useRouter } from "@/navigation/useNavigation";
+import { updateConversationQueryData } from "@/queries/useConversationQuery";
+import { updateConversationWithPeerQueryData } from "@/queries/useConversationWithPeerQuery";
+import { updateConversationDataToConversationListQuery } from "@/queries/useV3ConversationListQuery";
+import { sentryTrackError } from "@/utils/sentry";
+import { createConversationByAccount } from "@/utils/xmtpRN/conversations";
+import { useMutation } from "@tanstack/react-query";
+import { ConversationTopic } from "@xmtp/react-native-sdk";
+import { memo, useCallback, useLayoutEffect } from "react";
+
+export const ConversationNewDm = memo(function ConversationNewDm(props: {
+ peerAddress: string;
+ textPrefill?: string;
+}) {
+ const { peerAddress, textPrefill } = props;
+
+ const navigation = useRouter();
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ headerTitle: () => ,
+ });
+ }, [peerAddress, navigation]);
+
+ const sendFirstConversationMessage =
+ useSendFirstConversationMessage(peerAddress);
+
+ const handleSendWelcomeMessage = useCallback(() => {
+ sendFirstConversationMessage({
+ content: { text: "👋" },
+ });
+ }, [sendFirstConversationMessage]);
+
+ return (
+
+ {/* TODO: Add empty state */}
+
+
+
+
+ );
+});
+
+function useSendFirstConversationMessage(peerAddress: string) {
+ const {
+ mutateAsync: createNewConversationAsync,
+ status: createNewConversationStatus,
+ } = useMutation({
+ mutationFn: async (peerAddress: string) => {
+ const currentAccount = getCurrentAccount()!;
+ return createConversationByAccount(currentAccount, peerAddress!);
+ },
+ onSuccess: (newConversation) => {
+ const currentAccount = getCurrentAccount()!;
+
+ // Update the conversation with peer query
+ updateConversationWithPeerQueryData(
+ currentAccount,
+ peerAddress,
+ newConversation
+ );
+
+ // Update the list of conversations
+ updateConversationDataToConversationListQuery(
+ currentAccount,
+ newConversation.topic,
+ newConversation
+ );
+
+ // Update conversation by topic
+ updateConversationQueryData(
+ currentAccount,
+ newConversation.topic,
+ newConversation
+ );
+ },
+ // TODO: Add this for optimistic update and faster UX
+ // onMutate: (peerAddress) => {
+ // const currentAccount = getCurrentAccount()!;
+ // queryClient.setQueryData(
+ // conversationWithPeerQueryKey(currentAccount, peerAddress),
+ // () => ({
+ // topic: `RANDOM_TOPIC_${Math.random()}`,
+ // } satisfies DmWithCodecsType)
+ // );
+ // },
+ });
+
+ const { mutateAsync: sendMessageAsync } = useMutation({
+ mutationFn: sendMessage,
+ // TODO: Add this for optimistic update and faster UX
+ // onMutate: (args) => {
+ // try {
+ // const { conversation } = args;
+ // const currentAccount = getCurrentAccount()!;
+ // addConversationMessage(currentAccount, conversation.topic!, {
+ // id: "RANDOM_MESSAGE_ID",
+ // content: { text: "RANDOM_MESSAGE_TEXT" },
+ // });
+ // } catch (error) {
+ // console.log("error:", error);
+ // }
+ // },
+ });
+
+ return useCallback(
+ async (args: ISendMessageParams) => {
+ try {
+ // First, create the conversation
+ const conversation = await createNewConversationAsync(peerAddress);
+ // Then, send the message
+ await sendMessageAsync({
+ conversation,
+ params: args,
+ });
+ } catch (error) {
+ showSnackbar({ message: "Failed to send message" });
+ sentryTrackError(error);
+ }
+ },
+ [createNewConversationAsync, peerAddress, sendMessageAsync]
+ );
+}
diff --git a/features/conversation/conversation-persisted-stores.ts b/features/conversation/conversation-persisted-stores.ts
deleted file mode 100644
index b7a10f578..000000000
--- a/features/conversation/conversation-persisted-stores.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { zustandMMKVStorage } from "@utils/mmkv";
-import { ConversationTopic, MessageId } from "@xmtp/react-native-sdk";
-import { createStore, useStore } from "zustand";
-import {
- createJSONStorage,
- persist,
- subscribeWithSelector,
-} from "zustand/middleware";
-import { Nullable } from "../../types/general";
-import { useConversationStore } from "./conversation-store";
-
-export type IComposerMediaPreviewStatus =
- | "picked"
- | "uploading"
- | "uploaded"
- | "error"
- | "sending";
-
-export type IComposerMediaPreview = {
- status: IComposerMediaPreviewStatus;
- mediaURI: string;
- mimeType: Nullable;
- dimensions?: {
- width: number;
- height: number;
- };
-} | null;
-
-type IConversationPersistedStore = {
- inputValue: string;
- replyingToMessageId: MessageId | null;
- composerMediaPreview: IComposerMediaPreview;
-};
-
-// Cache of conversation stores
-const conversationPersistedStores = new Map<
- string,
- ReturnType
->();
-
-const creatConversationStore = (name: string) =>
- createStore()(
- subscribeWithSelector(
- persist(
- (set) => ({
- inputValue: "",
- replyingToMessageId: null,
- composerMediaPreview: null,
- }),
- {
- storage: createJSONStorage(() => zustandMMKVStorage),
- name, // unique storage key for each conversation
- partialize: (state) => ({
- inputValue: state.inputValue,
- replyingToMessageId: state.replyingToMessageId,
- mediaPreview: state.composerMediaPreview,
- }),
- }
- )
- )
- );
-
-const newConversationStore = creatConversationStore("new_conversation");
-
-// Custom persistence logic
-// Factory function to create a store for each conversation with persistence
-const createConversationPersistedStore = (conversationTopic: string) =>
- creatConversationStore(`conversation_${conversationTopic}`);
-
-// Hook to get or create the store for a conversation
-export const getConversationPersistedStore = (conversationTopic: string) => {
- if (!conversationPersistedStores.has(conversationTopic)) {
- conversationPersistedStores.set(
- conversationTopic,
- createConversationPersistedStore(conversationTopic)
- );
- }
- return conversationPersistedStores.get(conversationTopic)!;
-};
-
-export function useConversationPersistedStore(topic: string) {
- if (!topic) {
- return newConversationStore;
- }
- return getConversationPersistedStore(topic);
-}
-
-export function useConversationPersistedStoreState(
- conversationTopic: ConversationTopic | null,
- selector: (state: IConversationPersistedStore) => T
-) {
- const store = conversationTopic
- ? getConversationPersistedStore(conversationTopic)
- : newConversationStore;
- return useStore(store, selector);
-}
-
-// Maybe put somewhere else
-export function getCurrentConversationPersistedStore() {
- const topic = useConversationStore.getState().topic;
- if (!topic) {
- return newConversationStore;
- }
- return getConversationPersistedStore(topic);
-}
-
-// Maybe put somewhere else
-export function useCurrentConversationPersistedStoreState(
- selector: (state: IConversationPersistedStore) => T
-) {
- const topic = useConversationStore((state) => state.topic);
- return useConversationPersistedStoreState(topic, selector);
-}
diff --git a/features/conversation/conversation-service.ts b/features/conversation/conversation-service.ts
deleted file mode 100644
index 2a7049f2b..000000000
--- a/features/conversation/conversation-service.ts
+++ /dev/null
@@ -1,251 +0,0 @@
-import { getCurrentAccount } from "@data/store/accountsStore";
-import { MessageAttachmentStatus } from "@data/store/chatStore";
-import { getConversationMessages } from "@queries/useConversationMessages";
-import {
- createFolderForMessage,
- getMessageAttachmentLocalPath,
- saveLocalAttachmentMetaData,
-} from "@utils/attachment/attachment.utils";
-import { moveFileAndReplace } from "@utils/fileSystem";
-import {
- ConversationTopic,
- MessageId,
- RemoteAttachmentContent,
-} from "@xmtp/react-native-sdk";
-import * as ImagePicker from "expo-image-picker";
-import { v4 as uuidv4 } from "uuid";
-import {
- IComposerMediaPreview,
- getCurrentConversationPersistedStore,
- useCurrentConversationPersistedStoreState,
-} from "./conversation-persisted-stores";
-import { IConversationStore, useConversationStore } from "./conversation-store";
-import logger from "@/utils/logger";
-
-export function initializeCurrentConversation(args: {
- topic: ConversationTopic | undefined;
- peerAddress: string | undefined;
- inputValue: string;
-}) {
- const { topic, peerAddress, inputValue } = args;
- useConversationStore.setState({ topic, peerAddress });
- setCurrentConversationInputValue(inputValue);
-}
-
-export function resetCurrentConversation() {
- useConversationStore.setState({ topic: "" as ConversationTopic });
-}
-
-export function updateNewConversation(newTopic: ConversationTopic) {
- setCurrentConversationInputValue("");
- useConversationStore.setState({ topic: newTopic, peerAddress: undefined });
-}
-
-export function getComposerMediaPreview() {
- const conversationStore = getCurrentConversationPersistedStore();
- return conversationStore.getState().composerMediaPreview;
-}
-
-export function setComposerMediaPreview(mediaPreview: IComposerMediaPreview) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState((state) => ({
- ...state,
- composerMediaPreview: mediaPreview,
- }));
-}
-
-export function setComposerMediaPreviewStatus(status: MessageAttachmentStatus) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState((state) => ({
- ...state,
- composerMediaPreview: { ...state.composerMediaPreview!, status },
- }));
-}
-
-export function handleAttachmentSelected(asset: ImagePicker.ImagePickerAsset) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState((state) => ({
- ...state,
- mediaURI: asset.uri,
- status: "picked",
- dimensions: {
- height: asset.height,
- width: asset.width,
- },
- }));
-}
-
-export async function saveAttachmentLocally() {
- const mediaPreview = getComposerMediaPreview();
-
- if (!mediaPreview) {
- throw new Error("No media preview found");
- }
-
- const messageId = uuidv4();
-
- await createFolderForMessage(messageId);
-
- const filename = mediaPreview.mediaURI.split("/").pop() || `${uuidv4()}`;
-
- const attachmentLocalPath = getMessageAttachmentLocalPath(
- messageId,
- filename
- );
-
- await moveFileAndReplace(mediaPreview.mediaURI, attachmentLocalPath);
-
- await saveLocalAttachmentMetaData({
- messageId,
- filename,
- mimeType: mediaPreview.mimeType || undefined,
- });
-}
-
-export function useReplyToMessageId() {
- return useCurrentConversationPersistedStoreState(
- (state) => state.replyingToMessageId
- );
-}
-
-export function setCurrentConversationReplyToMessageId(
- replyingToMessageId: MessageId | null
-) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState((state) => ({
- ...state,
- replyingToMessageId,
- }));
-}
-
-export function setCurrentConversationInputValue(inputValue: string) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState((state) => ({
- ...state,
- inputValue,
- }));
-}
-
-export function waitUntilMediaPreviewIsUploaded() {
- return new Promise((resolve, reject) => {
- const startTime = Date.now();
- const checkStatus = () => {
- const mediaPreview = getComposerMediaPreview();
- if (!mediaPreview?.status) {
- resolve(true);
- return;
- }
- if (mediaPreview.status === "uploaded") {
- resolve(true);
- return;
- }
- if (Date.now() - startTime > 10000) {
- reject(new Error("Media upload timeout after 10 seconds"));
- return;
- }
- setTimeout(checkStatus, 200);
- };
- checkStatus();
- });
-}
-
-export function listenToComposerInputValueChange(
- callback: (value: string, previousValue: string) => void
-) {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.subscribe((state) => state.inputValue, callback);
-}
-
-export function getCurrentConversationInputValue() {
- const conversationStore = getCurrentConversationPersistedStore();
- return conversationStore.getState().inputValue;
-}
-
-export function useCurrentConversationInputValue() {
- return useCurrentConversationPersistedStoreState((state) => state.inputValue);
-}
-
-export function setUploadedRemoteAttachment(
- uploadedRemoteAttachment: RemoteAttachmentContent
-) {
- useConversationStore.setState((state) => ({
- ...state,
- uploadedRemoteAttachment,
- }));
-}
-
-export function getUploadedRemoteAttachment() {
- return useConversationStore.getState().uploadedRemoteAttachment;
-}
-
-export function resetUploadedRemoteAttachment() {
- useConversationStore.setState({
- uploadedRemoteAttachment: null,
- });
-}
-
-export function resetComposerMediaPreview() {
- const conversationStore = getCurrentConversationPersistedStore();
- conversationStore.setState({
- composerMediaPreview: null,
- });
-}
-
-export function getCurrentConversationReplyToMessageId() {
- const conversationStore = getCurrentConversationPersistedStore();
- return conversationStore.getState().replyingToMessageId;
-}
-
-export function useConversationCurrentTopic() {
- return useConversationStore((state) => state.topic);
-}
-
-export function useConversationCurrentPeerAddress() {
- return useConversationStore((state) => state.peerAddress);
-}
-
-export function getCurrentConversationTopic() {
- return useConversationStore.getState().topic;
-}
-
-export function useConversationComposerMediaPreview() {
- return useCurrentConversationPersistedStoreState(
- (state) => state.composerMediaPreview
- );
-}
-
-export function getCurrentConversationMessages() {
- const currentAccount = getCurrentAccount()!;
- const topic = getCurrentConversationTopic();
- if (!topic) {
- logger.error("No topic in getCurrentConversationMessages");
- return {
- byId: {},
- ids: [],
- reactions: {},
- };
- }
- return getConversationMessages(currentAccount, topic);
-}
-
-export function setMessageContextMenuData(
- data: IConversationStore["messageContextMenuData"]
-) {
- useConversationStore.setState({
- messageContextMenuData: data,
- });
-}
-
-export function getMessageContextMenuData() {
- return useConversationStore.getState().messageContextMenuData;
-}
-
-export function resetMessageContextMenuData() {
- useConversationStore.setState({
- messageContextMenuData: null,
- });
-}
-
-export function useMessageContextMenuData() {
- return useConversationStore((state) => state.messageContextMenuData);
-}
diff --git a/features/conversation/conversation-store.ts b/features/conversation/conversation-store.ts
deleted file mode 100644
index a895c3ee1..000000000
--- a/features/conversation/conversation-store.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {
- ConversationTopic,
- MessageId,
- RemoteAttachmentContent,
-} from "@xmtp/react-native-sdk";
-import { create } from "zustand";
-import { subscribeWithSelector } from "zustand/middleware";
-
-export type IConversationStore = {
- topic: ConversationTopic | null;
- peerAddress: string | null;
- uploadedRemoteAttachment: RemoteAttachmentContent | null;
- messageContextMenuData: {
- messageId: MessageId;
- itemRectX: number;
- itemRectY: number;
- itemRectHeight: number;
- itemRectWidth: number;
- messageComponent: React.ReactNode;
- } | null;
- pickingEmojiForMessageId: MessageId | null;
-};
-
-export const useConversationStore = create()(
- subscribeWithSelector(() => ({
- topic: null,
- peerAddress: null,
- uploadedRemoteAttachment: null,
- messageContextMenuData: null,
- pickingEmojiForMessageId: null,
- }))
-);
diff --git a/components/Conversation/ConversationTitleDumb.tsx b/features/conversation/conversation-title.tsx
similarity index 74%
rename from components/Conversation/ConversationTitleDumb.tsx
rename to features/conversation/conversation-title.tsx
index 88c213c05..55090c698 100644
--- a/components/Conversation/ConversationTitleDumb.tsx
+++ b/features/conversation/conversation-title.tsx
@@ -6,41 +6,43 @@ import {
TouchableOpacity,
useColorScheme,
} from "react-native";
-import { getTitleFontScale } from "../../utils/str";
-import { AnimatedHStack } from "@design-system/HStack";
-import { animation } from "@theme/animations";
+import { getTitleFontScale } from "@utils/str";
+import { VStack } from "@/design-system/VStack";
+import { HStack } from "@design-system/HStack";
type ConversationTitleDumbProps = {
title?: string;
+ subtitle?: React.ReactNode;
avatarComponent?: React.ReactNode;
onLongPress?: () => void;
onPress?: () => void;
};
-export function ConversationTitleDumb({
+export function ConversationTitle({
avatarComponent,
title,
+ subtitle,
onLongPress,
onPress,
}: ConversationTitleDumbProps) {
const styles = useStyles();
return (
-
+
{avatarComponent}
-
- {title}
-
+
+
+ {title}
+
+ {subtitle}
+
-
+
);
}
diff --git a/features/conversation/conversation.nav.tsx b/features/conversation/conversation.nav.tsx
new file mode 100644
index 000000000..c4c1683a3
--- /dev/null
+++ b/features/conversation/conversation.nav.tsx
@@ -0,0 +1,33 @@
+import { ConversationScreen } from "@/features/conversation/conversation.screen";
+import { NativeStack } from "@/screens/Navigation/Navigation";
+import type { ConversationTopic } from "@xmtp/react-native-sdk";
+
+export type ConversationNavParams = {
+ topic?: ConversationTopic;
+ text?: string;
+ peer?: string;
+};
+
+export const ConversationScreenConfig = {
+ path: "/conversation",
+ parse: {
+ topic: decodeURIComponent,
+ },
+ stringify: {
+ topic: encodeURIComponent,
+ },
+};
+
+export function ConversationNav() {
+ return (
+
+ );
+}
diff --git a/features/conversation/conversation.screen.tsx b/features/conversation/conversation.screen.tsx
new file mode 100644
index 000000000..b767bb277
--- /dev/null
+++ b/features/conversation/conversation.screen.tsx
@@ -0,0 +1,76 @@
+import { Screen } from "@/components/Screen/ScreenComp/Screen";
+import { useCurrentAccount } from "@/data/store/accountsStore";
+import { Center } from "@/design-system/Center";
+import { Loader } from "@/design-system/loader";
+import { Conversation } from "@/features/conversation/conversation";
+import { ConversationNewDm } from "@/features/conversation/conversation-new-dm";
+import { useConversationWithPeerQuery } from "@/queries/useConversationWithPeerQuery";
+import { captureError } from "@/utils/capture-error";
+import { VStack } from "@design-system/VStack";
+import { NativeStackScreenProps } from "@react-navigation/native-stack";
+import { isV3Topic } from "@utils/groupUtils/groupId";
+import React, { memo } from "react";
+import { NavigationParamList } from "../../screens/Navigation/Navigation";
+
+type IConversationScreenProps = NativeStackScreenProps<
+ NavigationParamList,
+ "Conversation"
+>;
+
+export function ConversationScreen(args: IConversationScreenProps) {
+ const { route } = args;
+ const { peer, topic, text } = route.params || {};
+
+ if (!peer && !topic) {
+ captureError(new Error("No peer or topic found in ConversationScreen"));
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {topic && isV3Topic(topic) ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+type IPeerAddressFlowProps = {
+ peerAddress: string;
+ textPrefill?: string;
+};
+
+const PeerAddressFlow = memo(function PeerAddressFlow(
+ args: IPeerAddressFlowProps
+) {
+ const { peerAddress, textPrefill } = args;
+ const currentAccount = useCurrentAccount()!;
+ const { data: conversation, isLoading } = useConversationWithPeerQuery(
+ currentAccount,
+ peerAddress
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (conversation?.topic) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+});
diff --git a/features/conversation/conversation.store-context.tsx b/features/conversation/conversation.store-context.tsx
new file mode 100644
index 000000000..06c09c123
--- /dev/null
+++ b/features/conversation/conversation.store-context.tsx
@@ -0,0 +1,64 @@
+import { ConversationId, ConversationTopic } from "@xmtp/react-native-sdk";
+import { createContext, memo, useContext, useRef } from "react";
+import { createStore, useStore } from "zustand";
+
+type IConversationStoreProps = {
+ topic: ConversationTopic;
+ conversationId: ConversationId;
+};
+
+type IConversationStoreState = IConversationStoreProps & {};
+
+type IConversationStoreProviderProps =
+ React.PropsWithChildren;
+
+type IConversationStore = ReturnType;
+
+export const ConversationStoreProvider = memo(
+ ({ children, ...props }: IConversationStoreProviderProps) => {
+ const storeRef = useRef();
+ if (!storeRef.current) {
+ storeRef.current = createConversationStore(props);
+ }
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+const createConversationStore = (initProps: IConversationStoreProps) => {
+ const DEFAULT_PROPS: IConversationStoreProps = {
+ topic: null as unknown as ConversationTopic,
+ conversationId: null as unknown as ConversationId,
+ };
+ return createStore()((set) => ({
+ ...DEFAULT_PROPS,
+ ...initProps,
+ }));
+};
+
+const ConversationStoreContext = createContext(null);
+
+export function useConversationStoreContext(
+ selector: (state: IConversationStoreState) => T
+): T {
+ const store = useContext(ConversationStoreContext);
+ if (!store) throw new Error("Missing ConversationStore.Provider in the tree");
+ return useStore(store, selector);
+}
+
+export function useConversationStore() {
+ const store = useContext(ConversationStoreContext);
+ if (!store) throw new Error(`Missing ConversationStore.Provider in the tree`);
+ return store;
+}
+
+export function useCurrentConversationTopic() {
+ return useConversationStoreContext((state) => state.topic);
+}
+
+export function useConversationCurrentConversationId() {
+ return useConversationStoreContext((state) => state.conversationId);
+}
diff --git a/features/conversation/conversation.tsx b/features/conversation/conversation.tsx
new file mode 100644
index 000000000..f7af70326
--- /dev/null
+++ b/features/conversation/conversation.tsx
@@ -0,0 +1,375 @@
+import { VStack } from "@/design-system/VStack";
+import { Loader } from "@/design-system/loader";
+import { ExternalWalletPicker } from "@/features/ExternalWalletPicker/ExternalWalletPicker";
+import { ExternalWalletPickerContextProvider } from "@/features/ExternalWalletPicker/ExternalWalletPicker.context";
+import { useConversationIsUnread } from "@/features/conversation-list/hooks/useMessageIsUnread";
+import { useToggleReadStatus } from "@/features/conversation-list/hooks/useToggleReadStatus";
+import { Composer } from "@/features/conversation/conversation-composer/conversation-composer";
+import {
+ ConversationComposerStoreProvider,
+ useConversationComposerStore,
+} from "@/features/conversation/conversation-composer/conversation-composer.store-context";
+import { DmConsentPopup } from "@/features/conversation/conversation-consent-popup/conversation-consent-popup-dm";
+import { GroupConsentPopup } from "@/features/conversation/conversation-consent-popup/conversation-consent-popup-group";
+import { DmConversationTitle } from "@/features/conversation/conversation-dm-header-title";
+import { GroupConversationTitle } from "@/features/conversation/conversation-group-header-title";
+import { KeyboardFiller } from "@/features/conversation/conversation-keyboard-filler";
+import {
+ IMessageGesturesOnLongPressArgs,
+ MessageGestures,
+} from "@/features/conversation/conversation-message/conversation-message-gestures";
+import { ConversationMessageTimestamp } from "@/features/conversation/conversation-message/conversation-message-timestamp";
+import {
+ MessageContextStoreProvider,
+ useMessageContextStore,
+} from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { ConversationMessage } from "@/features/conversation/conversation-message/conversation-message";
+import { MessageContextMenu } from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu";
+import {
+ MessageContextMenuStoreProvider,
+ useMessageContextMenuStore,
+} from "@/features/conversation/conversation-message/conversation-message-context-menu/conversation-message-context-menu.store-context";
+import { ConversationMessageLayout } from "@/features/conversation/conversation-message/conversation-message-layout";
+import { MessageReactionsDrawer } from "@/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reaction-drawer/conversation-message-reaction-drawer";
+import { ConversationMessageReactions } from "@/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions";
+import { ConversationMessageRepliable } from "@/features/conversation/conversation-message/conversation-message-repliable";
+import { ConversationMessagesList } from "@/features/conversation/conversation-messages-list";
+import { useSendMessage } from "@/features/conversation/hooks/use-send-message";
+import { isConversationAllowed } from "@/features/conversation/utils/is-conversation-allowed";
+import { isConversationDm } from "@/features/conversation/utils/is-conversation-dm";
+import { isConversationGroup } from "@/features/conversation/utils/is-conversation-group";
+import { useConversationQuery } from "@/queries/useConversationQuery";
+import { useGroupNameQuery } from "@/queries/useGroupNameQuery";
+import {
+ ConversationWithCodecsType,
+ DecodedMessageWithCodecsType,
+} from "@/utils/xmtpRN/client.types";
+import { useCurrentAccount } from "@data/store/accountsStore";
+import { Button } from "@design-system/Button/Button";
+import { Center } from "@design-system/Center";
+import { Text } from "@design-system/Text";
+import { translate } from "@i18n/translate";
+import { useRouter } from "@navigation/useNavigation";
+import { useConversationMessages } from "@queries/useConversationMessages";
+import { useAppTheme } from "@theme/useAppTheme";
+import { ConversationTopic, MessageId } from "@xmtp/react-native-sdk";
+import React, {
+ memo,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+} from "react";
+import {
+ ConversationStoreProvider,
+ useCurrentConversationTopic,
+} from "./conversation.store-context";
+import { debugBorder } from "@/utils/debug-style";
+
+export const Conversation = memo(function Conversation(props: {
+ topic: ConversationTopic;
+ textPrefill?: string;
+}) {
+ const { topic, textPrefill = "" } = props;
+
+ const currentAccount = useCurrentAccount()!;
+
+ const navigation = useRouter();
+
+ const { data: conversation, isLoading: isLoadingConversation } =
+ useConversationQuery(currentAccount, topic);
+
+ useLayoutEffect(() => {
+ if (!conversation) {
+ return;
+ }
+ if (isConversationDm(conversation)) {
+ navigation.setOptions({
+ headerTitle: () => ,
+ });
+ } else if (isConversationGroup(conversation)) {
+ navigation.setOptions({
+ headerTitle: () => ,
+ });
+ }
+ }, [topic, navigation, conversation]);
+
+ if (!conversation && !isLoadingConversation) {
+ // TODO: Use EmptyState component
+ return (
+
+
+ {translate("group_not_found")}
+
+
+ );
+ }
+
+ if (!conversation) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+const ComposerWrapper = memo(function ComposerWrapper(props: {
+ conversation: ConversationWithCodecsType;
+}) {
+ const { conversation } = props;
+ const sendMessage = useSendMessage({
+ conversation,
+ });
+ return ;
+});
+
+const Messages = memo(function Messages(props: {
+ conversation: ConversationWithCodecsType;
+}) {
+ const { conversation } = props;
+
+ const currentAccount = useCurrentAccount()!;
+ const topic = useCurrentConversationTopic()!;
+
+ const {
+ data: messages,
+ isLoading: messagesLoading,
+ isRefetching: isRefetchingMessages,
+ refetch,
+ } = useConversationMessages(currentAccount, topic!);
+
+ const isUnread = useConversationIsUnread({
+ topic,
+ lastMessage: messages?.byId[messages?.ids[0]], // Get latest message
+ timestampNs: messages?.byId[messages?.ids[0]]?.sentNs ?? 0,
+ });
+
+ const toggleReadStatus = useToggleReadStatus({
+ topic,
+ isUnread,
+ currentAccount,
+ });
+
+ const hasMarkedAsRead = useRef(false);
+
+ useEffect(() => {
+ if (isUnread && !messagesLoading && !hasMarkedAsRead.current) {
+ toggleReadStatus();
+ hasMarkedAsRead.current = true;
+ }
+ }, [isUnread, messagesLoading, toggleReadStatus]);
+
+ return (
+
+ ) : (
+
+ )
+ }
+ ListHeaderComponent={
+ !isConversationAllowed(conversation) ? (
+ isConversationDm(conversation) ? (
+
+ ) : (
+
+ )
+ ) : undefined
+ }
+ renderMessage={({ messageId, index }) => {
+ const message = messages?.byId[messageId]!;
+ const previousMessage = messages?.byId[messages?.ids[index + 1]];
+ const nextMessage = messages?.byId[messages?.ids[index - 1]];
+
+ return (
+
+ );
+ }}
+ />
+ );
+});
+
+const ConversationMessagesListItem = memo(
+ function ConversationMessagesListItem(props: {
+ message: DecodedMessageWithCodecsType;
+ previousMessage: DecodedMessageWithCodecsType | undefined;
+ nextMessage: DecodedMessageWithCodecsType | undefined;
+ }) {
+ const { message, previousMessage, nextMessage } = props;
+ const composerStore = useConversationComposerStore();
+
+ const handleReply = useCallback(() => {
+ composerStore.getState().setReplyToMessageId(message.id as MessageId);
+ }, [composerStore, message]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+const WithGestures = memo(function WithGestures({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const messageStore = useMessageContextStore();
+
+ const messageContextMenuStore = useMessageContextMenuStore();
+
+ const handleLongPress = useCallback(
+ (e: IMessageGesturesOnLongPressArgs) => {
+ const messageId = messageStore.getState().messageId;
+ const message = messageStore.getState().message;
+ const previousMessage = messageStore.getState().previousMessage;
+ const nextMessage = messageStore.getState().nextMessage;
+
+ messageContextMenuStore.getState().setMessageContextMenuData({
+ messageId,
+ itemRectX: e.pageX,
+ itemRectY: e.pageY,
+ itemRectHeight: e.height,
+ itemRectWidth: e.width,
+ // Need to have MessageContextStoreProvider here.
+ // Not the cleanest...
+ // Might want to find another solution later but works for now.
+ // Solution might be to remove the context and just pass props
+ messageComponent: (
+
+ {children}
+
+ ),
+ });
+ },
+ [messageStore, messageContextMenuStore, children]
+ );
+
+ const handleTap = useCallback(() => {
+ const isShowingTime = !messageStore.getState().isShowingTime;
+ messageStore.setState({
+ isShowingTime,
+ });
+ }, [messageStore]);
+
+ return (
+
+ {children}
+
+ );
+});
+
+const DmConversationEmpty = memo(function DmConversationEmpty() {
+ return null;
+});
+
+const GroupConversationEmpty = memo(() => {
+ const { theme } = useAppTheme();
+
+ const currentAccount = useCurrentAccount()!;
+ const topic = useCurrentConversationTopic();
+
+ const { data: groupName } = useGroupNameQuery(currentAccount, topic);
+
+ const { data: conversation } = useConversationQuery(currentAccount, topic);
+
+ const sendMessage = useSendMessage({
+ conversation: conversation!,
+ });
+
+ const handleSend = useCallback(() => {
+ sendMessage({
+ content: {
+ text: "👋",
+ },
+ });
+ }, [sendMessage]);
+
+ return (
+
+
+ {translate("group_placeholder.placeholder_text", {
+ groupName,
+ })}
+
+
+
+
+ );
+});
diff --git a/features/conversation/dm-conversation.screen.tsx b/features/conversation/dm-conversation.screen.tsx
deleted file mode 100644
index 2f1f958f7..000000000
--- a/features/conversation/dm-conversation.screen.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- *
- * WORK IN PROGRESS!
- * This is to decouple group conversations from DM conversations and maybe even new DM conversation
- *
- */
-import {
- KeyboardFiller,
- MessagesList,
-} from "@/components/Conversation/V3Conversation";
-import { Screen } from "@/components/Screen/ScreenComp/Screen";
-import { showSnackbar } from "@/components/Snackbar/Snackbar.service";
-import {
- getCurrentAccount,
- useCurrentAccount,
-} from "@/data/store/accountsStore";
-import { Center } from "@/design-system/Center";
-import { VStack } from "@/design-system/VStack";
-import { Loader } from "@/design-system/loader";
-import {
- Composer,
- IComposerSendArgs,
-} from "@/features/conversation/composer/composer";
-import { useConversationCurrentTopic } from "@/features/conversation/conversation-service";
-import { DmConversationTitle } from "@/features/conversations/components/DmConversationTitle";
-import { NewConversationTitle } from "@/features/conversations/components/NewConversationTitle";
-import { useRouter } from "@/navigation/useNavigation";
-import {
- conversationWithPeerQueryKey,
- conversationsQueryKey,
-} from "@/queries/QueryKeys";
-import { queryClient } from "@/queries/queryClient";
-import { useConversationMessages } from "@/queries/useConversationMessages";
-import { useConversationWithPeerQuery } from "@/queries/useConversationWithPeerQuery";
-import { V3ConversationListType } from "@/queries/useV3ConversationListQuery";
-import { NavigationParamList } from "@/screens/Navigation/Navigation";
-import { sentryTrackError } from "@/utils/sentry";
-import { ConversationWithCodecsType } from "@/utils/xmtpRN/client";
-import { createConversationByAccount } from "@/utils/xmtpRN/conversations";
-import { NativeStackScreenProps } from "@react-navigation/native-stack";
-import { useMutation } from "@tanstack/react-query";
-import { MessageId, RemoteAttachmentContent } from "@xmtp/react-native-sdk";
-import React, { memo, useCallback, useEffect } from "react";
-
-export const DmConversationScreen = memo(function DmConversationScreen(
- props: NativeStackScreenProps
-) {
- // @ts-ignore
- const { peerAddress } = props.route.params;
-
- const currentAccount = useCurrentAccount()!;
-
- const { data: conversation, isLoading } = useConversationWithPeerQuery(
- currentAccount,
- peerAddress,
- {
- enabled: !!peerAddress,
- }
- );
-
- if (isLoading) {
- return (
-
-
-
-
-
- );
- }
-
- return (
-
- {!!conversation ? (
-
- ) : (
-
- )}
-
-
-
- );
-});
-
-const ComposerWrapper = memo(function ComposerWrapper(props: {
- peerAddress: string;
-}) {
- const { peerAddress } = props;
-
- const {
- mutateAsync: createNewConversationAsync,
- status: createNewConversationStatus,
- } = useMutation({
- mutationFn: async (peerAddress: string) => {
- const currentAccount = getCurrentAccount()!;
- return createConversationByAccount(currentAccount, peerAddress!);
- },
- onSuccess: (newConversation) => {
- const currentAccount = getCurrentAccount()!;
-
- // Update the conversation with peer query
- queryClient.setQueryData(
- conversationWithPeerQueryKey(currentAccount, peerAddress),
- () => newConversation
- );
-
- // Update the list of conversations
- queryClient.setQueryData(
- conversationsQueryKey(currentAccount),
- (conversations) => [...(conversations || []), newConversation]
- );
- },
- // TODO: Add this for optimistic update and faster UX
- // onMutate: (peerAddress) => {
- // const currentAccount = getCurrentAccount()!;
- // queryClient.setQueryData(
- // conversationWithPeerQueryKey(currentAccount, peerAddress),
- // () => ({
- // topic: `RANDOM_TOPIC_${Math.random()}`,
- // } satisfies DmWithCodecsType)
- // );
- // },
- });
-
- const { mutateAsync: sendMessageAsync, status: sendMessageStatus } =
- useMutation({
- mutationFn: async (args: {
- conversation: ConversationWithCodecsType;
- text?: string;
- remoteAttachment?: RemoteAttachmentContent;
- referencedMessageId?: MessageId;
- }) => {
- const { conversation, text, remoteAttachment, referencedMessageId } =
- args;
-
- if (referencedMessageId) {
- if (remoteAttachment) {
- await conversation.send({
- reply: {
- reference: referencedMessageId,
- content: { remoteAttachment },
- },
- });
- }
- if (text) {
- await conversation.send({
- reply: {
- reference: referencedMessageId,
- content: { text },
- },
- });
- }
- return;
- }
-
- if (remoteAttachment) {
- await conversation.send({
- remoteAttachment,
- });
- }
-
- if (text) {
- await conversation.send(text);
- }
- },
- // TODO: Add this for optimistic update and faster UX
- // onMutate: (args) => {
- // try {
- // const { conversation } = args;
- // const currentAccount = getCurrentAccount()!;
- // addConversationMessage(currentAccount, conversation.topic!, {
- // id: "RANDOM_MESSAGE_ID",
- // content: { text: "RANDOM_MESSAGE_TEXT" },
- // });
- // } catch (error) {
- // console.log("error:", error);
- // }
- // },
- });
-
- const handleSendMessage = useCallback(
- async (args: IComposerSendArgs) => {
- try {
- const {
- content: { text, remoteAttachment },
- referencedMessageId,
- } = args;
- const newConversation = await createNewConversationAsync(peerAddress);
- await sendMessageAsync({
- conversation: newConversation,
- text,
- remoteAttachment,
- referencedMessageId,
- });
- } catch (error) {
- showSnackbar({
- message: "Failed to send message",
- });
- sentryTrackError(error);
- }
- },
- [sendMessageAsync, peerAddress, createNewConversationAsync]
- );
-
- return ;
-});
-
-const NewDmConversation = memo(function NewDmConversation(props: {
- peerAddress: string;
-}) {
- const { peerAddress } = props;
-
- useNewConversationHeader(peerAddress);
-
- // TODO: Add empty state
- return (
-
- );
-});
-
-function useNewConversationHeader(peerAddresss: string) {
- const navigation = useRouter();
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: () => ,
- });
- }, [peerAddresss, navigation]);
-}
-
-const ExistingDmConversation = memo(function ExistingDmConversation(props: {
- conversation: ConversationWithCodecsType;
-}) {
- const { conversation } = props;
-
- const currentAccount = useCurrentAccount()!;
-
- const {
- data: messages,
- isLoading: messagesLoading,
- isRefetching: isRefetchingMessages,
- refetch,
- } = useConversationMessages(currentAccount, conversation.topic!);
-
- useDmHeader();
-
- if (messages?.ids.length === 0 && !messagesLoading) {
- // TODO: Add empty state
- return null;
- }
-
- return (
-
-
-
- );
-});
-
-function useDmHeader() {
- const navigation = useRouter();
-
- const topic = useConversationCurrentTopic();
-
- useEffect(() => {
- navigation.setOptions({
- headerTitle: () => ,
- });
- }, [topic, navigation]);
-}
diff --git a/features/conversation/hooks/use-group-name-convos.ts b/features/conversation/hooks/use-group-name-convos.ts
new file mode 100644
index 000000000..a426aa957
--- /dev/null
+++ b/features/conversation/hooks/use-group-name-convos.ts
@@ -0,0 +1,37 @@
+/**
+ * Will return the group name if exists OR it will create a default name
+ * based on members name
+ */
+
+import { usePreferredNames } from "@/hooks/usePreferredNames";
+import { useGroupMembersQuery } from "@/queries/useGroupMembersQuery";
+import { useGroupNameQuery } from "@/queries/useGroupNameQuery";
+import { ConversationTopic } from "@xmtp/react-native-sdk";
+
+export function useGroupNameConvos(args: {
+ topic: ConversationTopic;
+ account: string;
+}) {
+ const { topic, account } = args;
+
+ const { data: groupName, isLoading: groupNameLoading } = useGroupNameQuery(
+ account,
+ topic
+ );
+
+ const { data: members, isLoading: membersLoading } = useGroupMembersQuery(
+ account,
+ topic
+ );
+
+ const memberAddresses = members?.ids
+ .map((id) => members?.byId[id]?.addresses[0])
+ .filter((address) => address !== account);
+
+ const names = usePreferredNames(memberAddresses ?? []);
+
+ return {
+ groupName: groupName || names.join(", "),
+ isLoading: groupNameLoading || membersLoading,
+ };
+}
diff --git a/features/conversation/hooks/use-react-on-message.ts b/features/conversation/hooks/use-react-on-message.ts
new file mode 100644
index 000000000..ed4201df7
--- /dev/null
+++ b/features/conversation/hooks/use-react-on-message.ts
@@ -0,0 +1,89 @@
+import { getCurrentAccount } from "@/data/store/accountsStore";
+import { getCurrentUserAccountInboxId } from "@/hooks/use-current-account-inbox-id";
+import {
+ addConversationMessage,
+ refetchConversationMessages,
+} from "@/queries/useConversationMessages";
+import { captureError, captureErrorWithToast } from "@/utils/capture-error";
+import { getTodayNs } from "@/utils/date";
+import { getRandomId } from "@/utils/general";
+import { Haptics } from "@/utils/haptics";
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client.types";
+import { contentTypesPrefixes } from "@/utils/xmtpRN/content-types/content-types";
+import { useMutation } from "@tanstack/react-query";
+import {
+ MessageDeliveryStatus,
+ MessageId,
+ ReactionContent,
+} from "@xmtp/react-native-sdk";
+import { useCallback } from "react";
+
+export function useReactOnMessage(props: {
+ conversation: ConversationWithCodecsType;
+}) {
+ const { conversation } = props;
+
+ const { mutateAsync: reactOnMessageMutationAsync } = useMutation({
+ mutationFn: async (variables: { reaction: ReactionContent }) => {
+ const { reaction } = variables;
+ await conversation.send({
+ reaction,
+ });
+ },
+ onMutate: (variables) => {
+ const currentAccount = getCurrentAccount()!;
+ const currentUserInboxId = getCurrentUserAccountInboxId()!;
+
+ // Add the reaction to the message
+ addConversationMessage({
+ account: currentAccount,
+ topic: conversation.topic,
+ message: {
+ id: getRandomId(),
+ client: conversation.client,
+ contentTypeId: contentTypesPrefixes.reaction,
+ sentNs: getTodayNs(),
+ fallback: variables.reaction.content,
+ deliveryStatus: MessageDeliveryStatus.PUBLISHED,
+ topic: conversation.topic,
+ senderAddress: currentUserInboxId,
+ nativeContent: {},
+ content: () => {
+ return variables.reaction;
+ },
+ },
+ });
+ },
+ onError: (error) => {
+ captureError(error);
+ const currentAccount = getCurrentAccount()!;
+ refetchConversationMessages(currentAccount, conversation.topic).catch(
+ captureErrorWithToast
+ );
+ },
+ });
+
+ const reactOnMessage = useCallback(
+ async (args: { messageId: MessageId; emoji: string }) => {
+ try {
+ if (!conversation) {
+ throw new Error("Conversation not found when reacting on message");
+ }
+ Haptics.softImpactAsync();
+ await reactOnMessageMutationAsync({
+ reaction: {
+ reference: args.messageId,
+ content: args.emoji,
+ schema: "unicode",
+ action: "added",
+ },
+ });
+ } catch (error) {
+ captureErrorWithToast(error);
+ }
+ },
+ [reactOnMessageMutationAsync, conversation]
+ );
+
+ return reactOnMessage;
+}
diff --git a/features/conversation/hooks/use-remove-reaction-on-message.ts b/features/conversation/hooks/use-remove-reaction-on-message.ts
new file mode 100644
index 000000000..7355138c9
--- /dev/null
+++ b/features/conversation/hooks/use-remove-reaction-on-message.ts
@@ -0,0 +1,89 @@
+import { getCurrentAccount } from "@/data/store/accountsStore";
+import { getCurrentUserAccountInboxId } from "@/hooks/use-current-account-inbox-id";
+import {
+ addConversationMessage,
+ refetchConversationMessages,
+} from "@/queries/useConversationMessages";
+import { captureError, captureErrorWithToast } from "@/utils/capture-error";
+import { getTodayNs } from "@/utils/date";
+import { getRandomId } from "@/utils/general";
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client.types";
+import { contentTypesPrefixes } from "@/utils/xmtpRN/content-types/content-types";
+import { useMutation } from "@tanstack/react-query";
+import {
+ MessageDeliveryStatus,
+ MessageId,
+ ReactionContent,
+} from "@xmtp/react-native-sdk";
+import { useCallback } from "react";
+
+export function useRemoveReactionOnMessage(props: {
+ conversation: ConversationWithCodecsType;
+}) {
+ const { conversation } = props;
+
+ const { mutateAsync: removeReactionMutationAsync } = useMutation({
+ mutationFn: async (variables: { reaction: ReactionContent }) => {
+ const { reaction } = variables;
+ await conversation.send({
+ reaction,
+ });
+ },
+ onMutate: (variables) => {
+ const currentAccount = getCurrentAccount()!;
+ const currentUserInboxId = getCurrentUserAccountInboxId()!;
+
+ // Add the removal reaction message
+ addConversationMessage({
+ account: currentAccount,
+ topic: conversation.topic,
+ message: {
+ id: getRandomId(),
+ client: conversation.client,
+ contentTypeId: contentTypesPrefixes.reaction,
+ sentNs: getTodayNs(),
+ fallback: variables.reaction.content,
+ deliveryStatus: MessageDeliveryStatus.PUBLISHED,
+ topic: conversation.topic,
+ senderAddress: currentUserInboxId,
+ nativeContent: {},
+ content: () => {
+ return variables.reaction;
+ },
+ },
+ });
+ },
+ onError: (error) => {
+ captureError(error);
+ const currentAccount = getCurrentAccount()!;
+ refetchConversationMessages(currentAccount, conversation.topic).catch(
+ captureErrorWithToast
+ );
+ },
+ });
+
+ const removeReactionFromMessage = useCallback(
+ async (args: { messageId: MessageId; emoji: string }) => {
+ try {
+ if (!conversation) {
+ throw new Error(
+ "Conversation not found when removing reaction from message"
+ );
+ }
+ await removeReactionMutationAsync({
+ reaction: {
+ reference: args.messageId,
+ content: args.emoji,
+ schema: "unicode",
+ action: "removed",
+ },
+ });
+ } catch (error) {
+ captureErrorWithToast(error);
+ }
+ },
+ [removeReactionMutationAsync, conversation]
+ );
+
+ return removeReactionFromMessage;
+}
diff --git a/features/conversation/hooks/use-send-message.ts b/features/conversation/hooks/use-send-message.ts
new file mode 100644
index 000000000..63e2ed904
--- /dev/null
+++ b/features/conversation/hooks/use-send-message.ts
@@ -0,0 +1,119 @@
+import { getCurrentAccount } from "@/data/store/accountsStore";
+import { refetchConversationMessages } from "@/queries/useConversationMessages";
+import { captureError, captureErrorWithToast } from "@/utils/capture-error";
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client.types";
+import { useMutation } from "@tanstack/react-query";
+import { MessageId, RemoteAttachmentContent } from "@xmtp/react-native-sdk";
+import { useCallback } from "react";
+
+export type ISendMessageParams = {
+ referencedMessageId?: MessageId;
+ content:
+ | { text: string; remoteAttachment?: RemoteAttachmentContent }
+ | { text?: string; remoteAttachment: RemoteAttachmentContent };
+};
+
+export function sendMessage(args: {
+ conversation: ConversationWithCodecsType;
+ params: ISendMessageParams;
+}) {
+ const { conversation, params } = args;
+
+ const { referencedMessageId, content } = params;
+
+ if (referencedMessageId) {
+ return conversation.send({
+ reply: {
+ reference: referencedMessageId,
+ content: content.remoteAttachment
+ ? { remoteAttachment: content.remoteAttachment }
+ : { text: content.text },
+ },
+ });
+ }
+
+ return conversation.send(
+ content.remoteAttachment
+ ? { remoteAttachment: content.remoteAttachment }
+ : { text: content.text! }
+ );
+}
+
+export function useSendMessage(props: {
+ conversation: ConversationWithCodecsType;
+}) {
+ const { conversation } = props;
+
+ const { mutateAsync: sendMessageMutationAsync } = useMutation({
+ mutationFn: (variables: ISendMessageParams) =>
+ sendMessage({ conversation, params: variables }),
+ // WIP
+ // onMutate: (variables) => {
+ // const currentAccount = getCurrentAccount()!;
+ // const currentUserInboxId = getCurrentUserAccountInboxId()!;
+
+ // // For now only optimistic message for simple text message
+ // if (variables.content.text && !variables.referencedMessageId) {
+ // const generatedMessageId = getRandomId();
+
+ // const textMessage: DecodedMessage = {
+ // id: generatedMessageId,
+ // client: conversation.client,
+ // contentTypeId: variables.content.text
+ // ? contentTypesPrefixes.text
+ // : contentTypesPrefixes.remoteAttachment,
+ // sentNs: getTodayNs(),
+ // fallback: "new-message",
+ // deliveryStatus: MessageDeliveryStatus.PUBLISHED,
+ // topic: conversation.topic,
+ // senderAddress: currentUserInboxId,
+ // nativeContent: {},
+ // content: () => {
+ // return variables.content.text!;
+ // },
+ // };
+
+ // addConversationMessage({
+ // account: currentAccount,
+ // topic: conversation.topic,
+ // message: textMessage,
+ // // isOptimistic: true,
+ // });
+
+ // return {
+ // generatedMessageId,
+ // };
+ // }
+ // },
+ // WIP
+ // onSuccess: (messageId, _, context) => {
+ // if (context && messageId) {
+ // updateConversationMessagesOptimisticMessages(
+ // context.generatedMessageId,
+ // messageId
+ // );
+ // }
+ // },
+ onError: (error) => {
+ captureError(error);
+ const currentAccount = getCurrentAccount()!;
+ refetchConversationMessages(currentAccount, conversation.topic).catch(
+ captureErrorWithToast
+ );
+ },
+ });
+
+ return useCallback(
+ async (args: ISendMessageParams) => {
+ try {
+ if (!conversation) {
+ throw new Error("Conversation not found when sending message");
+ }
+ await sendMessageMutationAsync(args);
+ } catch (error) {
+ captureErrorWithToast(error);
+ }
+ },
+ [sendMessageMutationAsync, conversation]
+ );
+}
diff --git a/features/conversations/utils/__tests__/search.test.ts b/features/conversation/utils/__tests__/search.test.ts
similarity index 100%
rename from features/conversations/utils/__tests__/search.test.ts
rename to features/conversation/utils/__tests__/search.test.ts
diff --git a/features/conversations/utils/hasNextMessageInSeries.ts b/features/conversation/utils/has-next-message-in-serie.ts
similarity index 79%
rename from features/conversations/utils/hasNextMessageInSeries.ts
rename to features/conversation/utils/has-next-message-in-serie.ts
index a35f77408..a1a3f6d59 100644
--- a/features/conversations/utils/hasNextMessageInSeries.ts
+++ b/features/conversation/utils/has-next-message-in-serie.ts
@@ -2,12 +2,13 @@ import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
type HasNextMessageInSeriesPayload = {
currentMessage: DecodedMessageWithCodecsType;
- nextMessage: DecodedMessageWithCodecsType;
+ nextMessage: DecodedMessageWithCodecsType | undefined;
};
export const hasNextMessageInSeries = ({
currentMessage,
nextMessage,
}: HasNextMessageInSeriesPayload) => {
+ if (!nextMessage) return false;
return nextMessage.senderAddress === currentMessage.senderAddress;
};
diff --git a/features/conversations/utils/hasPreviousMessageInSeries.ts b/features/conversation/utils/has-previous-message-in-serie.ts
similarity index 100%
rename from features/conversations/utils/hasPreviousMessageInSeries.ts
rename to features/conversation/utils/has-previous-message-in-serie.ts
diff --git a/features/conversation/utils/is-conversation-allowed.ts b/features/conversation/utils/is-conversation-allowed.ts
new file mode 100644
index 000000000..6b06f4568
--- /dev/null
+++ b/features/conversation/utils/is-conversation-allowed.ts
@@ -0,0 +1,7 @@
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client.types";
+
+export function isConversationAllowed(
+ conversation: ConversationWithCodecsType
+) {
+ return conversation.state === "allowed";
+}
diff --git a/features/conversation/utils/is-conversation-dm.ts b/features/conversation/utils/is-conversation-dm.ts
new file mode 100644
index 000000000..288e3cbf4
--- /dev/null
+++ b/features/conversation/utils/is-conversation-dm.ts
@@ -0,0 +1,6 @@
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client";
+import { ConversationVersion } from "@xmtp/react-native-sdk";
+
+export function isConversationDm(conversation: ConversationWithCodecsType) {
+ return conversation.version === ConversationVersion.DM;
+}
diff --git a/features/conversation/utils/is-conversation-group.ts b/features/conversation/utils/is-conversation-group.ts
new file mode 100644
index 000000000..6eca52322
--- /dev/null
+++ b/features/conversation/utils/is-conversation-group.ts
@@ -0,0 +1,6 @@
+import { ConversationWithCodecsType } from "@/utils/xmtpRN/client";
+import { ConversationVersion } from "@xmtp/react-native-sdk";
+
+export function isConversationGroup(conversation: ConversationWithCodecsType) {
+ return conversation.version === ConversationVersion.GROUP;
+}
diff --git a/features/conversations/utils/isLatestMessageSettledFromPeer.ts b/features/conversation/utils/is-latest-message-settled-from-peer.ts
similarity index 88%
rename from features/conversations/utils/isLatestMessageSettledFromPeer.ts
rename to features/conversation/utils/is-latest-message-settled-from-peer.ts
index 7b87d6a8a..7343c4211 100644
--- a/features/conversations/utils/isLatestMessageSettledFromPeer.ts
+++ b/features/conversation/utils/is-latest-message-settled-from-peer.ts
@@ -1,5 +1,5 @@
import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
-import { messageIsFromCurrentUser } from "./messageIsFromCurrentUser";
+import { messageIsFromCurrentUser } from "./message-is-from-current-user";
type IsLatestMessageSettledFromPeerPayload = {
message?: DecodedMessageWithCodecsType;
diff --git a/features/conversations/utils/isLatestSettledFromCurrentUser.ts b/features/conversation/utils/is-latest-settled-from-current-user.ts
similarity index 82%
rename from features/conversations/utils/isLatestSettledFromCurrentUser.ts
rename to features/conversation/utils/is-latest-settled-from-current-user.ts
index 50a79b7b4..4feb67f2f 100644
--- a/features/conversations/utils/isLatestSettledFromCurrentUser.ts
+++ b/features/conversation/utils/is-latest-settled-from-current-user.ts
@@ -1,5 +1,5 @@
import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
-import { messageIsFromCurrentUser } from "./messageIsFromCurrentUser";
+import { messageIsFromCurrentUser } from "./message-is-from-current-user";
type IsLatestSettledFromCurrentUserPayload = {
message?: DecodedMessageWithCodecsType;
diff --git a/features/conversations/utils/messageIsFromCurrentUser.ts b/features/conversation/utils/message-is-from-current-user.ts
similarity index 88%
rename from features/conversations/utils/messageIsFromCurrentUser.ts
rename to features/conversation/utils/message-is-from-current-user.ts
index 6a2a60cf8..eb1f23d4b 100644
--- a/features/conversations/utils/messageIsFromCurrentUser.ts
+++ b/features/conversation/utils/message-is-from-current-user.ts
@@ -1,4 +1,4 @@
-import { getCurrentUserAccountInboxId } from "@/components/Chat/Message/message-utils";
+import { getCurrentUserAccountInboxId } from "@/hooks/use-current-account-inbox-id";
import { getCurrentAccount } from "@data/store/accountsStore";
import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
diff --git a/features/conversations/utils/messageShouldShowDateChange.ts b/features/conversation/utils/message-should-show-date-change.ts
similarity index 100%
rename from features/conversations/utils/messageShouldShowDateChange.ts
rename to features/conversation/utils/message-should-show-date-change.ts
diff --git a/features/conversations/utils/search.ts b/features/conversation/utils/search.ts
similarity index 100%
rename from features/conversations/utils/search.ts
rename to features/conversation/utils/search.ts
diff --git a/features/conversations/Messages.types.ts b/features/conversations/Messages.types.ts
deleted file mode 100644
index 44814eead..000000000
--- a/features/conversations/Messages.types.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client";
-
-export type V3MessageToDisplay = {
- message?: DecodedMessageWithCodecsType;
- hasPreviousMessageInSeries: boolean;
- hasNextMessageInSeries: boolean;
- dateChange: boolean;
- fromMe: boolean;
- isLatestSettledFromMe: boolean;
- isLatestSettledFromPeer: boolean;
- isLoadingAttachment: boolean | undefined;
- nextMessageIsLoadingAttachment: boolean | undefined;
-};
diff --git a/features/conversations/components/GroupConversationTitle.tsx b/features/conversations/components/GroupConversationTitle.tsx
deleted file mode 100644
index 6b31c7e4e..000000000
--- a/features/conversations/components/GroupConversationTitle.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React, { memo, useCallback, useMemo } from "react";
-import { useGroupNameQuery } from "@queries/useGroupNameQuery";
-import { ConversationTopic } from "@xmtp/react-native-sdk";
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { NativeStackNavigationProp } from "@react-navigation/native-stack";
-import { NavigationParamList } from "@screens/Navigation/Navigation";
-import { ImageStyle, Platform } from "react-native";
-import { useRouter } from "@navigation/useNavigation";
-import { useGroupPhotoQuery } from "@queries/useGroupPhotoQuery";
-import Avatar from "@components/Avatar";
-import { AvatarSizes } from "@styles/sizes";
-import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
-import { GroupAvatarDumb } from "@components/GroupAvatar";
-import { useConversationTitleLongPress } from "../hooks/useConversationTitleLongPress";
-import { useGroupMembersAvatarData } from "../hooks/useGroupMembersAvatarData";
-import { ConversationTitleDumb } from "@components/Conversation/ConversationTitleDumb";
-
-type GroupConversationTitleProps = {
- topic: ConversationTopic;
-};
-
-type UseUserInteractionProps = {
- topic: ConversationTopic;
- navigation: NativeStackNavigationProp;
-};
-
-const useUserInteraction = ({ navigation, topic }: UseUserInteractionProps) => {
- const onPress = useCallback(() => {
- // textInputRef?.current?.blur();
- navigation.push("Group", { topic });
- }, [navigation, topic]);
-
- const onLongPress = useConversationTitleLongPress(topic);
-
- return { onPress, onLongPress };
-};
-
-export const GroupConversationTitle = memo(
- ({ topic }: GroupConversationTitleProps) => {
- const currentAccount = useCurrentAccount()!;
-
- const { data: groupName, isLoading: groupNameLoading } = useGroupNameQuery(
- currentAccount,
- topic!
- );
-
- const { data: groupPhoto, isLoading: groupPhotoLoading } =
- useGroupPhotoQuery(currentAccount, topic!);
-
- const { data: memberData } = useGroupMembersAvatarData({ topic });
-
- const navigation = useRouter();
-
- const { themed } = useAppTheme();
-
- const { onPress, onLongPress } = useUserInteraction({
- topic,
- navigation,
- });
-
- const displayAvatar = !groupPhotoLoading && !groupNameLoading;
-
- const avatarComponent = useMemo(() => {
- return groupPhoto ? (
-
- ) : (
-
- );
- }, [groupPhoto, memberData, themed]);
-
- if (!displayAvatar) return null;
-
- return (
-
- );
- }
-);
-
-const $avatar: ThemedStyle = (theme) => ({
- marginRight: Platform.OS === "android" ? theme.spacing.lg : theme.spacing.xxs,
- marginLeft: Platform.OS === "ios" ? theme.spacing.zero : -theme.spacing.xxs,
-});
diff --git a/features/conversations/components/V3ConversationFromPeer.tsx b/features/conversations/components/V3ConversationFromPeer.tsx
deleted file mode 100644
index 832c401cb..000000000
--- a/features/conversations/components/V3ConversationFromPeer.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useConversationWithPeerQuery } from "@/queries/useConversationWithPeerQuery";
-import ActivityIndicator from "@components/ActivityIndicator/ActivityIndicator";
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { memo } from "react";
-import { VStack } from "@design-system/VStack";
-import { ThemedStyle, useAppTheme } from "@theme/useAppTheme";
-import { ViewStyle } from "react-native";
-import { V3Conversation } from "@components/Conversation/V3Conversation";
-
-type V3ConversationFromPeerProps = {
- peer: string;
- textPrefill?: string;
- skipLoading: boolean;
-};
-
-/**
- * A component that renders a conversation from a peer.
- * It is used to render a conversation from a peer.
- * This is a wrapper around the V3Conversation component to help load the conversation and abstract some logic.
- * If we want the best peformance we should rework this component
- */
-export const V3ConversationFromPeer = memo(
- ({ peer, textPrefill, skipLoading }: V3ConversationFromPeerProps) => {
- const currentAccount = useCurrentAccount()!;
-
- const { data: conversation, isLoading } = useConversationWithPeerQuery(
- currentAccount,
- peer,
- {
- enabled: !skipLoading,
- }
- );
-
- const { themed } = useAppTheme();
- if (isLoading && !skipLoading) {
- return (
-
-
-
- );
- }
- return (
-
- );
- }
-);
-
-const $container: ThemedStyle = (theme) => ({
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: theme.colors.background.surface,
-});
diff --git a/features/conversations/hooks/useConversationTitleLongPress.ts b/features/conversations/hooks/useConversationTitleLongPress.ts
deleted file mode 100644
index af9fe6d32..000000000
--- a/features/conversations/hooks/useConversationTitleLongPress.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useDebugEnabled } from "@components/DebugButton";
-import Clipboard from "@react-native-clipboard/clipboard";
-import { useCallback } from "react";
-
-export const useConversationTitleLongPress = (topic: string) => {
- const debugEnabled = useDebugEnabled();
-
- return useCallback(() => {
- if (!debugEnabled) return;
- Clipboard.setString(
- JSON.stringify({
- topic,
- })
- );
- }, [debugEnabled, topic]);
-};
diff --git a/features/conversations/hooks/useGroupMembersAvatarData.ts b/features/conversations/hooks/useGroupMembersAvatarData.ts
deleted file mode 100644
index f1c3eda33..000000000
--- a/features/conversations/hooks/useGroupMembersAvatarData.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { useProfilesSocials } from "@hooks/useProfilesSocials";
-import { useGroupMembersConversationScreenQuery } from "@queries/useGroupMembersQuery";
-import { getPreferredAvatar } from "@utils/profile/getPreferredAvatar";
-import { getPreferredName } from "@utils/profile/getPreferredName";
-import { ConversationTopic } from "@xmtp/react-native-sdk";
-import { useMemo } from "react";
-
-type UseGroupMembersAvatarDataProps = {
- topic: ConversationTopic;
-};
-
-export const useGroupMembersAvatarData = ({
- topic,
-}: UseGroupMembersAvatarDataProps) => {
- const currentAccount = useCurrentAccount()!;
- const { data: members, ...query } = useGroupMembersConversationScreenQuery(
- currentAccount,
- topic
- );
-
- const memberAddresses = useMemo(() => {
- const addresses: string[] = [];
- for (const memberId of members?.ids ?? []) {
- const member = members?.byId[memberId];
- if (
- member?.addresses[0] &&
- member?.addresses[0].toLowerCase() !== currentAccount?.toLowerCase()
- ) {
- addresses.push(member?.addresses[0]);
- }
- }
- return addresses;
- }, [members, currentAccount]);
-
- const data = useProfilesSocials(memberAddresses);
-
- const memberData: {
- address: string;
- uri?: string;
- name?: string;
- }[] = useMemo(() => {
- return data.map(({ data: socials }, index) =>
- socials
- ? {
- address: memberAddresses[index],
- uri: getPreferredAvatar(socials),
- name: getPreferredName(socials, memberAddresses[index]),
- }
- : {
- address: memberAddresses[index],
- uri: undefined,
- name: memberAddresses[index],
- }
- );
- }, [data, memberAddresses]);
-
- return { data: memberData, ...query };
-};
diff --git a/features/notifications/utils/accountTopicSubscription.ts b/features/notifications/utils/accountTopicSubscription.ts
new file mode 100644
index 000000000..60875932f
--- /dev/null
+++ b/features/notifications/utils/accountTopicSubscription.ts
@@ -0,0 +1,38 @@
+import { createV3ConversationListQueryObserver } from "@/queries/useV3ConversationListQuery";
+import { subscribeToNotifications } from "./subscribeToNotifications";
+import logger from "@/utils/logger";
+
+const accountTopicUnsubscribeMap: Record void> = {};
+
+export const setupAccountTopicSubscription = (account: string) => {
+ if (accountTopicUnsubscribeMap[account]) {
+ logger.info(
+ `[setupAccountTopicSubscription] already subscribed to account ${account}`
+ );
+ return accountTopicUnsubscribeMap[account];
+ }
+ logger.info(
+ `[setupAccountTopicSubscription] subscribing to account ${account}`
+ );
+ const observer = createV3ConversationListQueryObserver(account, "sync");
+ let previous: number | undefined;
+ const unsubscribe = observer.subscribe((conversationList) => {
+ if (conversationList.data && conversationList.dataUpdatedAt !== previous) {
+ previous = conversationList.dataUpdatedAt;
+ subscribeToNotifications({
+ conversations: conversationList.data,
+ account,
+ });
+ }
+ });
+ accountTopicUnsubscribeMap[account] = unsubscribe;
+ return unsubscribe;
+};
+
+export const unsubscribeFromAccountTopicSubscription = (account: string) => {
+ const unsubscribe = accountTopicUnsubscribeMap[account];
+ if (unsubscribe) {
+ unsubscribe();
+ delete accountTopicUnsubscribeMap[account];
+ }
+};
diff --git a/features/notifications/utils/background/notificationContent.ts b/features/notifications/utils/background/notificationContent.ts
index a4dc9a1cf..b7a7a0674 100644
--- a/features/notifications/utils/background/notificationContent.ts
+++ b/features/notifications/utils/background/notificationContent.ts
@@ -6,7 +6,7 @@ import {
import {
getMessageContentType,
isContentType,
-} from "@utils/xmtpRN/contentTypes";
+} from "@/utils/xmtpRN/content-types/content-types";
import type {
MessageId,
ReactionContent,
diff --git a/features/notifications/utils/background/notificationSpamScore.ts b/features/notifications/utils/background/notificationSpamScore.ts
index 400af56bb..e2535c560 100644
--- a/features/notifications/utils/background/notificationSpamScore.ts
+++ b/features/notifications/utils/background/notificationSpamScore.ts
@@ -3,7 +3,7 @@ import {
DecodedMessageWithCodecsType,
GroupWithCodecsType,
} from "@utils/xmtpRN/client";
-import { isContentType } from "@utils/xmtpRN/contentTypes";
+import { isContentType } from "@/utils/xmtpRN/content-types/content-types";
import { getSendersSpamScores } from "@/utils/api";
import { InboxId } from "@xmtp/react-native-sdk";
import { computeMessageContentSpamScore } from "@/data/helpers/conversations/spamScore";
diff --git a/features/notifications/utils/subscribeToNotifications.ts b/features/notifications/utils/subscribeToNotifications.ts
index f6ce0e35c..973129179 100644
--- a/features/notifications/utils/subscribeToNotifications.ts
+++ b/features/notifications/utils/subscribeToNotifications.ts
@@ -96,7 +96,9 @@ export const subscribeToNotifications = async ({
status: "PUSH",
};
- logger.info("[subscribeToNotifications] saving notifications subscribe");
+ logger.info(
+ `[subscribeToNotifications] saving notifications subscribed to ${Object.keys(topicsToUpdateForPeriod).length} topics for account ${account}`
+ );
await saveNotificationsSubscribe(
account,
nativePushToken,
diff --git a/features/search/components/NavigationChatButton.tsx b/features/search/components/NavigationChatButton.tsx
index e25e4fc28..f09764b70 100644
--- a/features/search/components/NavigationChatButton.tsx
+++ b/features/search/components/NavigationChatButton.tsx
@@ -39,7 +39,7 @@ export function NavigationChatButton({
navigation.popToTop();
navigate("Conversation", {
- mainConversationWithPeer: address,
+ peer: address,
});
}, [address, navigation]);
diff --git a/hooks/use-current-account-inbox-id.ts b/hooks/use-current-account-inbox-id.ts
new file mode 100644
index 000000000..c8d5ad380
--- /dev/null
+++ b/hooks/use-current-account-inbox-id.ts
@@ -0,0 +1,24 @@
+import {
+ getCurrentAccount,
+ useCurrentAccount,
+} from "@/data/store/accountsStore";
+import {
+ getInboxIdFromQueryData,
+ prefetchInboxIdQuery,
+ useInboxIdQuery,
+} from "../queries/use-inbox-id-query";
+
+export function useCurrentAccountInboxId() {
+ const currentAccount = useCurrentAccount()!;
+ return useInboxIdQuery({ account: currentAccount });
+}
+
+export function getCurrentUserAccountInboxId() {
+ const currentAccount = getCurrentAccount()!;
+ return getInboxIdFromQueryData({ account: currentAccount });
+}
+
+export function prefetchCurrentUserAccountInboxId() {
+ const currentAccount = getCurrentAccount()!;
+ return prefetchInboxIdQuery({ account: currentAccount });
+}
diff --git a/hooks/useCurrentAccountXmtpClient.ts b/hooks/useCurrentAccountXmtpClient.ts
new file mode 100644
index 000000000..92f6ed95c
--- /dev/null
+++ b/hooks/useCurrentAccountXmtpClient.ts
@@ -0,0 +1,13 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { useCurrentAccount } from "@/data/store/accountsStore";
+import { getXmtpClient } from "@/utils/xmtpRN/sync";
+
+export function useCurrentAccountXmtpClient() {
+ const address = useCurrentAccount();
+ return useQuery({
+ queryKey: ["xmtpClient", address],
+ queryFn: () => getXmtpClient(address!),
+ enabled: !!address,
+ });
+}
diff --git a/hooks/useGroupMembers.ts b/hooks/useGroupMembers.ts
index 9819013c1..afa2da503 100644
--- a/hooks/useGroupMembers.ts
+++ b/hooks/useGroupMembers.ts
@@ -1,11 +1,6 @@
-import { QueryObserverOptions } from "@tanstack/react-query";
-
import { currentAccount } from "../data/store/accountsStore";
import { useAddToGroupMutation } from "../queries/useAddToGroupMutation";
-import {
- GroupMembersSelectData,
- useGroupMembersQuery,
-} from "../queries/useGroupMembersQuery";
+import { useGroupMembersQuery } from "../queries/useGroupMembersQuery";
import { usePromoteToAdminMutation } from "../queries/usePromoteToAdminMutation";
import { usePromoteToSuperAdminMutation } from "../queries/usePromoteToSuperAdminMutation";
import { useRemoveFromGroupMutation } from "../queries/useRemoveFromGroupMutation";
@@ -13,17 +8,14 @@ import { useRevokeAdminMutation } from "../queries/useRevokeAdminMutation";
import { useRevokeSuperAdminMutation } from "../queries/useRevokeSuperAdminMutation";
import type { ConversationTopic } from "@xmtp/react-native-sdk";
-export const useGroupMembers = (
- topic: ConversationTopic,
- queryOptions?: Partial>
-) => {
+export const useGroupMembers = (topic: ConversationTopic) => {
const account = currentAccount();
const {
data: members,
isLoading,
isError,
- } = useGroupMembersQuery(account, topic, queryOptions);
+ } = useGroupMembersQuery(account, topic);
const { mutateAsync: promoteToAdmin } = usePromoteToAdminMutation(
account,
topic
diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts
index 012b50b33..0c3b42e9c 100644
--- a/i18n/translations/en.ts
+++ b/i18n/translations/en.ts
@@ -165,6 +165,7 @@ export const en = {
unblock_and_restore: "Unblock and restore",
cancel: "Cancel",
back: "Back",
+ close: "Close",
view_only: "View only",
view_and_restore: "View and Restore",
view_removed_group_chat: "View removed group chat?",
@@ -178,6 +179,12 @@ export const en = {
add_an_account: "Add an account",
group_info: "Group info",
converse_match_maker: "Converse Match Maker",
+ today: "Today",
+ yesterday: "Yesterday",
+ member_count: "{{count}} member",
+ members_count: "{{count}} members",
+ modify: "Modify",
+ pending_count: "{{count}} pending",
// Requests
requests: "Requests",
@@ -193,6 +200,7 @@ export const en = {
"You currently have no message requests. We'll make suggestions here as you connect with others.",
hidden_requests_warn:
"Requests containing messages that may be offensive or unwanted are moved to this folder.",
+ message_requests: "Message requests",
// Conversation
accept: "Accept",
@@ -327,6 +335,7 @@ export const en = {
add_members: "Add members",
edit_group_info: "Edit group info",
members_can: "MEMBERS CAN",
+ title: "New group",
},
new_conversation: {
@@ -433,6 +442,9 @@ export const en = {
seen: "Read",
},
+ this_is_the_beginning_of_your_conversation_with:
+ "This is the beginning of your conversation with {{name}}",
+
group_placeholder: {
placeholder_text:
"This is the beginning of your conversation in {{groupName}}",
@@ -504,6 +516,11 @@ export const en = {
},
file_preview: "File preview",
+
+ share_profile: {
+ link_copied: "Link copied",
+ copy_link: "Copy link",
+ },
};
export type Translations = typeof en;
diff --git a/i18n/translations/fr.ts b/i18n/translations/fr.ts
index 51c7db090..e8b65e13f 100644
--- a/i18n/translations/fr.ts
+++ b/i18n/translations/fr.ts
@@ -181,6 +181,12 @@ export const fr = {
add_an_account: "Ajouter un compte",
group_info: "Informations sur le groupe",
converse_match_maker: "Converse Match Maker",
+ today: "Aujourd'hui",
+ yesterday: "Hier",
+ member_count: "{{count}} membre",
+ members_count: "{{count}} membres",
+ modify: "Modifier",
+ pending_count: "{{count}} en attente",
// Requests
requests: "Demandes",
@@ -196,7 +202,7 @@ export const fr = {
"Vous n'avez actuellement aucune demande de message. Nous ferons des suggestions ici à mesure que vous vous connecterez avec d'autres.",
hidden_requests_warn:
"Les demandes contenant des messages potentiellement offensants ou indésirables sont déplacées dans ce dossier.",
-
+ message_requests: "Demandes de message",
// Conversation
accept: "Accepter",
block: "Bloquer",
@@ -334,6 +340,7 @@ export const fr = {
add_members: "Ajouter des membres",
edit_group_info: "Modifier les informations du groupe",
members_can: "LES MEMBRES PEUVENT",
+ title: "Nouveau groupe",
},
new_conversation: {
@@ -515,4 +522,9 @@ export const fr = {
},
file_preview: "Aperçu du fichier",
+
+ share_profile: {
+ link_copied: "Lien copié",
+ copy_link: "Copier le lien",
+ },
};
diff --git a/ios/ConverseNotificationExtension/MMKV.swift b/ios/ConverseNotificationExtension/MMKV.swift
index fa257b53d..61b1cb026 100644
--- a/ios/ConverseNotificationExtension/MMKV.swift
+++ b/ios/ConverseNotificationExtension/MMKV.swift
@@ -56,15 +56,32 @@ func getAccountsState() -> Accounts? {
}
}
-func getProfilesStore(account: String) -> ProfilesStore? {
+func getProfilesStore(account: String, address: String) -> ProfileSocials? {
let mmkv = getMmkv()
- let profilesString = mmkv?.string(forKey: "store-\(account)-profiles")
+ let key = "profileSocials-\(account.lowercased())-\(address.lowercased())"
+ let profilesString = mmkv?.string(forKey: key)
if (profilesString == nil) {
return nil
}
let decoder = JSONDecoder()
do {
- let decoded = try decoder.decode(ProfilesStore.self, from: profilesString!.data(using: .utf8)!)
+ let decoded = try decoder.decode(ProfileSocials.self, from: profilesString!.data(using: .utf8)!)
+ return decoded
+ } catch {
+ return nil
+ }
+}
+
+func getInboxIdProfilesStore(account: String, inboxId: String) -> ProfileSocials? {
+ let mmkv = getMmkv()
+ let key = "inboxProfileSocials-\(account.lowercased())-\(inboxId.lowercased())"
+ let profilesString = mmkv?.string(forKey: key)
+ if (profilesString == nil) {
+ return nil
+ }
+ let decoder = JSONDecoder()
+ do {
+ let decoded = try decoder.decode(ProfileSocials.self, from: profilesString!.data(using: .utf8)!)
return decoded
} catch {
return nil
@@ -72,19 +89,21 @@ func getProfilesStore(account: String) -> ProfilesStore? {
}
func saveProfileSocials(account: String, address: String, socials: ProfileSocials) {
- var profilesStore = getProfilesStore(account: account) ?? ProfilesStore(state: Profiles(profiles: [:]), version: 0)
- if profilesStore.state.profiles == nil {
- profilesStore.state.profiles = [:]
+ let updatedAt = Int(Date().timeIntervalSince1970)
+ let newProfile = Profile(updatedAt: updatedAt, socials: socials)
+ let mmkv = getMmkv()
+ if let jsonData = try? JSONEncoder().encode(newProfile), let jsonString = String(data: jsonData, encoding: .utf8) {
+ mmkv?.set(jsonString, forKey: "profileSocials-\(account.lowercased())-\(address.lowercased())")
}
-
+}
+
+func saveInboxIdProfileSocials(account: String, inboxId: String, socials: ProfileSocials) {
let updatedAt = Int(Date().timeIntervalSince1970)
let newProfile = Profile(updatedAt: updatedAt, socials: socials)
- profilesStore.state.profiles![address] = newProfile
let mmkv = getMmkv()
- if let jsonData = try? JSONEncoder().encode(profilesStore), let jsonString = String(data: jsonData, encoding: .utf8) {
- mmkv?.set(jsonString, forKey: "store-\(account)-profiles")
+ if let jsonData = try? JSONEncoder().encode(newProfile), let jsonString = String(data: jsonData, encoding: .utf8) {
+ mmkv?.set(jsonString, forKey: "inboxProfileSocials-\(account.lowercased())-\(inboxId.lowercased())")
}
-
}
func getCurrentAccount() -> String? {
diff --git a/ios/ConverseNotificationExtension/NotificationService.swift b/ios/ConverseNotificationExtension/NotificationService.swift
index cb239398e..0272f8262 100644
--- a/ios/ConverseNotificationExtension/NotificationService.swift
+++ b/ios/ConverseNotificationExtension/NotificationService.swift
@@ -38,7 +38,7 @@ func handleNotificationAsync(contentHandler: ((UNNotificationContent) -> Void),
if let xmtpClient = await getXmtpClient(account: account), !isIntroTopic(topic: contentTopic) {
if isV3WelcomeTopic(topic: contentTopic) {
- guard let conversation = await getNewConversation(xmtpClient: xmtpClient, contentTopic: contentTopic)else {
+ guard let conversation = await getNewConversation(xmtpClient: xmtpClient, contentTopic: contentTopic) else {
contentHandler(UNNotificationContent())
return
}
@@ -51,7 +51,7 @@ func handleNotificationAsync(contentHandler: ((UNNotificationContent) -> Void),
welcomeTopic: contentTopic,
bestAttemptContent: &content
)
- } else if isV3MessageTopic(topic: contentTopic) {
+ } else if isV3MessageTopic(topic: contentTopic) {
let encryptedMessageData = Data(base64Encoded: Data(encodedMessage.utf8))!
let envelope = XMTP.Xmtp_MessageApi_V1_Envelope .with { envelope in
envelope.message = encryptedMessageData
@@ -59,18 +59,7 @@ func handleNotificationAsync(contentHandler: ((UNNotificationContent) -> Void),
}
(shouldShowNotification, messageId, messageIntent) = await handleV3Message(xmtpClient: xmtpClient, envelope: envelope, apiURI: apiURI, bestAttemptContent: &content)
} else {
- let encryptedMessageData = Data(base64Encoded: Data(encodedMessage.utf8))!
- let envelope = XMTP.Xmtp_MessageApi_V1_Envelope.with { envelope in
- envelope.message = encryptedMessageData
- envelope.contentTopic = contentTopic
- }
sentryAddBreadcrumb(message: "topic \(contentTopic) is not invite topic")
- (shouldShowNotification, messageId, messageIntent) = await handleOngoingConversationMessage(
- xmtpClient: xmtpClient,
- envelope: envelope,
- bestAttemptContent: &content,
- body: body
- )
}
}
diff --git a/ios/ConverseNotificationExtension/Profile.swift b/ios/ConverseNotificationExtension/Profile.swift
index a88c3bcd4..871418f49 100644
--- a/ios/ConverseNotificationExtension/Profile.swift
+++ b/ios/ConverseNotificationExtension/Profile.swift
@@ -8,18 +8,34 @@
import Foundation
import Alamofire
-func getProfile(account: String, address: String) async -> Profile? {
- var profileState = getProfilesStore(account: account)?.state
+func getProfile(account: String, address: String) async -> ProfileSocials? {
+ var profileFromStore = getProfilesStore(account: account, address: address)
let formattedAddress = address.lowercased()
- if let profile = profileState?.profiles?[address] ?? profileState?.profiles?[formattedAddress] {
+ if let profile = profileFromStore {
return profile
}
// If profile is nil, let's refresh it
try? await refreshProfileFromBackend(account: account, address: formattedAddress)
- profileState = getProfilesStore(account: account)?.state
- if let profile = profileState?.profiles?[formattedAddress] {
+ profileFromStore = getProfilesStore(account: account, address: address)
+ if let profile = profileFromStore {
+ return profile
+ }
+ return nil
+}
+
+func getInboxIdProfile(account: String, inboxId: String) async -> ProfileSocials? {
+ var profileFromStore = getInboxIdProfilesStore(account: account, inboxId: inboxId)
+ if let profile = profileFromStore {
+ return profile
+ }
+
+ // If profile is nil, let's refresh it
+ try? await refreshInboxProfileFromBackend(account: account, inboxId: inboxId)
+
+ profileFromStore = getInboxIdProfilesStore(account: account, inboxId: inboxId)
+ if let profile = profileFromStore {
return profile
}
return nil
@@ -53,3 +69,32 @@ func refreshProfileFromBackend(account: String, address: String) async throws {
}
}
+
+func refreshInboxProfileFromBackend(account: String, inboxId: String) async throws {
+ let apiURI = getApiURI()
+ if (apiURI != nil && !apiURI!.isEmpty) {
+ let profileURI = "\(apiURI ?? "")/api/inbox"
+
+ let response = try await withUnsafeThrowingContinuation { continuation in
+ AF.request(profileURI, method: .get, parameters: ["ids": [inboxId]]).validate().responseData { response in
+ if let data = response.data {
+ continuation.resume(returning: data)
+ return
+ }
+ if let err = response.error {
+ continuation.resume(throwing: err)
+ return
+ }
+ }
+ }
+
+ // Create an instance of JSONDecoder
+ let decoder = JSONDecoder()
+
+ if let socials = try? decoder.decode(ProfileSocials.self, from: response) {
+ saveInboxIdProfileSocials(account: account, inboxId: inboxId, socials: socials)
+ }
+
+ }
+
+}
diff --git a/ios/ConverseNotificationExtension/Spam.swift b/ios/ConverseNotificationExtension/Spam.swift
index 2c6770c65..2b4d1c72c 100644
--- a/ios/ConverseNotificationExtension/Spam.swift
+++ b/ios/ConverseNotificationExtension/Spam.swift
@@ -89,13 +89,18 @@ func containsRestrictedWords(in searchString: String) -> Bool {
func computeSpamScoreV3Welcome(client: XMTP.Client, conversation: XMTP.Conversation, apiURI: String?) async -> Double {
do {
- try await client.preferences.syncConsent()
- // Probably an unlikely case until consent proofs for groups exist
- let convoState = try await client.preferences.conversationState(conversationId: conversation.id)
- let convoAllowed = convoState == .allowed
- if convoAllowed {
- return -1
- }
+// try await client.preferences.syncConsent()
+// // Probably an unlikely case until consent proofs for groups exist
+// do {
+// let convoState = try await client.preferences.conversationState(conversationId: conversation.id)
+// let convoAllowed = convoState == .allowed
+// if convoAllowed {
+// return -1
+// }
+// } catch {
+//
+// }
+
if case .group(let group) = conversation {
let inviterInboxId = try group.addedByInboxId()
let inviterState = try await client.preferences.inboxIdState(inboxId: inviterInboxId)
@@ -140,6 +145,7 @@ func computeSpamScoreV3Welcome(client: XMTP.Client, conversation: XMTP.Conversat
}
} catch {
+ sentryTrackError(error: error, extras: ["message": "Failed to compute Spam Score for V3 Welcome"])
return 0
}
@@ -147,10 +153,9 @@ func computeSpamScoreV3Welcome(client: XMTP.Client, conversation: XMTP.Conversat
}
func computeSpamScoreV3Message(client: XMTP.Client, conversation: XMTP.Conversation, decodedMessage: DecodedMessage, apiURI: String?) async -> Double {
- var senderSpamScore: Double = 0
do {
- try await client.preferences.syncConsent()
+// try await client.preferences.syncConsent()
let groupDenied = try await client.preferences.conversationState(conversationId: conversation.id) == .denied
if groupDenied {
// Network consent will override other checks
@@ -202,11 +207,12 @@ func computeSpamScoreV3Message(client: XMTP.Client, conversation: XMTP.Conversat
}
} catch {
//
+ sentryTrackError(error: error, extras: ["message": "Failed to compute Spam Score for V3 Message"])
}
let contentType = getContentTypeString(type: decodedMessage.encodedContent.type)
let messageContent = String(data: decodedMessage.encodedContent.content, encoding: .utf8)
let messageSpamScore = getMessageSpamScore(message: messageContent, contentType: contentType)
- return senderSpamScore + messageSpamScore
+ return messageSpamScore
}
diff --git a/ios/ConverseNotificationExtension/Xmtp/Conversations.swift b/ios/ConverseNotificationExtension/Xmtp/Conversations.swift
index 4ac1e8074..f02f96fe5 100644
--- a/ios/ConverseNotificationExtension/Xmtp/Conversations.swift
+++ b/ios/ConverseNotificationExtension/Xmtp/Conversations.swift
@@ -14,7 +14,7 @@ func getNewConversation(xmtpClient: XMTP.Client, contentTopic: String) async ->
if (isV3WelcomeTopic(topic: contentTopic)) {
// Weclome envelopes are too large to send in a push, so a bit of a hack to get the latest group
try await xmtpClient.conversations.sync()
- let conversation = try xmtpClient.findConversationByTopic(topic: contentTopic)
+ let conversation = try await xmtpClient.conversations.list().last
try await conversation?.sync()
return conversation
}
diff --git a/ios/ConverseNotificationExtension/Xmtp/Messages.swift b/ios/ConverseNotificationExtension/Xmtp/Messages.swift
index 0fcb7e0fd..7e69fae98 100644
--- a/ios/ConverseNotificationExtension/Xmtp/Messages.swift
+++ b/ios/ConverseNotificationExtension/Xmtp/Messages.swift
@@ -83,18 +83,30 @@ func handleV3Message(xmtpClient: XMTP.Client, envelope: XMTP.Xmtp_MessageApi_V1_
}
// We replaced decodedMessage.senderAddress from inboxId to actual address
// so it appears well in the app until inboxId is a first class citizen
- if let senderProfile = await getProfile(account: xmtpClient.address, address: decodedMessage.senderAddress) {
- bestAttemptContent.subtitle = getPreferredName(address: decodedMessage.senderAddress, socials: senderProfile.socials)
+ if let senderProfileSocials = await getProfile(account: xmtpClient.address, address: decodedMessage.senderAddress) {
+ bestAttemptContent.subtitle = getPreferredName(address: decodedMessage.senderAddress, socials: senderProfileSocials)
}
if let content = decodedMessageResult.content {
bestAttemptContent.body = content
}
-
let groupImage = try? group.groupImageUrlSquare()
messageIntent = getIncomingGroupMessageIntent(group: group, content: bestAttemptContent.body, senderId: decodedMessage.senderAddress, senderName: bestAttemptContent.subtitle)
} else if case .dm(let dm) = conversation {
- print("It's a DM with details: \(dm)")
+ var senderAvatar: String? = nil
+ if let senderProfileSocials = await getProfile(account: xmtpClient.address, address: decodedMessage.senderAddress) {
+ bestAttemptContent.title = getPreferredName(address: decodedMessage.senderAddress, socials: senderProfileSocials)
+ senderAvatar = getPreferredAvatar(socials: senderProfileSocials)
+ }
+ if let content = decodedMessageResult.content {
+ bestAttemptContent.body = content
+ }
+ messageIntent = getIncoming1v1MessageIntent(
+ topic: contentTopic,
+ senderId: decodedMessage.senderAddress,
+ senderName: bestAttemptContent.title,
+ senderAvatar: senderAvatar, content: bestAttemptContent.body
+ )
}
} else if spamScore == 0 { // Message is Request
@@ -133,9 +145,9 @@ func handleOngoingConversationMessage(xmtpClient: XMTP.Client, envelope: XMTP.Xm
bestAttemptContent.body = content
var senderAvatar: String? = nil
- if let senderAddress = decodedMessageResult.senderAddress, let senderProfile = await getProfile(account: xmtpClient.address, address: senderAddress) {
- conversationTitle = getPreferredName(address: senderAddress, socials: senderProfile.socials)
- senderAvatar = getPreferredAvatar(socials: senderProfile.socials)
+ if let senderAddress = decodedMessageResult.senderAddress, let senderProfileSocials = await getProfile(account: xmtpClient.address, address: senderAddress) {
+ conversationTitle = getPreferredName(address: senderAddress, socials: senderProfileSocials)
+ senderAvatar = getPreferredAvatar(socials: senderProfileSocials)
}
if (conversationTitle == nil), let senderAddress = decodedMessageResult.senderAddress {
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 9fd340d18..ee22fbdc6 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2690,7 +2690,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- boost: 1dca942403ed9342f98334bf4c3621f011aa7946
+ boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
CoinbaseWalletSDK: ea1f37512bbc69ebe07416e3b29bf840f5cc3152
CoinbaseWalletSDKExpo: fc6cc756974827763d7a0decf7140c2902dafca2
ComputableLayout: c50faffac4ed9f8f05b0ce5e6f3a60df1f6042c8
@@ -2858,6 +2858,6 @@ SPEC CHECKSUMS:
XMTPReactNative: f3e1cbf80b7278b817bd42982703a95a9250497d
Yoga: a9ef4f5c2cd79ad812110525ef61048be6a582a4
-PODFILE CHECKSUM: f65822bc74cd8d80ac89cdd1acf99590bd69f357
+PODFILE CHECKSUM: 7ed5cefb992e438c67772278d7c473ace4b42753
COCOAPODS: 1.15.2
diff --git a/jest.setup.ts b/jest.setup.ts
index 01ead1e19..70a1b0f6e 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -82,3 +82,8 @@ jest.mock("uuid", () => ({
jest.mock("path", () => ({
join: jest.fn(() => ""),
}));
+
+jest.mock("expo-localization", () => ({
+ // TODO: Update later to begin returning more locales and mock within individual tests
+ getLocales: jest.fn(() => [{ languageTag: "en-US" }]),
+}));
diff --git a/package.json b/package.json
index 432929644..f99a1546e 100644
--- a/package.json
+++ b/package.json
@@ -69,10 +69,10 @@
"@shopify/flash-list": "1.6.4",
"@stardazed/streams-polyfill": "^2.4.0",
"@statelyai/inspect": "^0.4.0",
- "@tanstack/query-persist-client-core": "^5.54.1",
- "@tanstack/query-sync-storage-persister": "^5.45.0",
- "@tanstack/react-query": "^5.45.0",
- "@tanstack/react-query-persist-client": "^5.45.0",
+ "@tanstack/query-persist-client-core": "5.62.2",
+ "@tanstack/query-sync-storage-persister": "5.62.2",
+ "@tanstack/react-query": "5.62.2",
+ "@tanstack/react-query-persist-client": "5.62.2",
"@thirdweb-dev/react-native-adapter": "^1.5.0",
"@walletconnect/react-native-compat": "^2.17.1",
"@web3modal/ethers5": "^3.5.5",
diff --git a/queries/queryClient.constants.ts b/queries/queryClient.constants.ts
new file mode 100644
index 000000000..8e8877009
--- /dev/null
+++ b/queries/queryClient.constants.ts
@@ -0,0 +1,3 @@
+export const GC_TIME = 1000 * 60 * 60 * 24; // 24 hours
+
+export const STALE_TIME = 1000 * 60 * 60; // 1 hour
diff --git a/queries/queryClient.ts b/queries/queryClient.ts
index cc5a03c5c..8c92d276b 100644
--- a/queries/queryClient.ts
+++ b/queries/queryClient.ts
@@ -1,8 +1,5 @@
import { QueryClient } from "@tanstack/react-query";
-import { reactQueryPersister } from "@utils/mmkv";
-
-export const GC_TIME = 1000 * 60 * 60 * 24; // 24 hours
-const STALE_TIME = 1000 * 60 * 60; // 1 hour
+import { GC_TIME, STALE_TIME } from "./queryClient.constants";
export const queryClient = new QueryClient({
defaultOptions: {
@@ -11,9 +8,10 @@ export const queryClient = new QueryClient({
gcTime: GC_TIME,
staleTime: STALE_TIME,
structuralSharing: false,
- // Using a query based persister rather than persisting
- // the whole state on each query change for performance reasons
- persister: reactQueryPersister,
+ // DON'T USE HERE
+ // Use a query based persister instead of the whole tree.
+ // Using it here seems to break the query client.
+ // persister: reactQueryPersister,
},
},
});
diff --git a/queries/use-inbox-id-query.ts b/queries/use-inbox-id-query.ts
new file mode 100644
index 000000000..fae8426be
--- /dev/null
+++ b/queries/use-inbox-id-query.ts
@@ -0,0 +1,39 @@
+import { queryClient } from "@/queries/queryClient";
+import { useQuery } from "@tanstack/react-query";
+import { getInboxId } from "@/utils/xmtpRN/signIn";
+
+export type IGetInboxIdQueryData = Awaited>;
+
+export type IGetInboxIdQueryOptions = {
+ queryKey: ["inboxId", string];
+ queryFn: () => Promise;
+ enabled: boolean;
+};
+
+export function getInboxIdQueryOptions(args: {
+ account: string;
+}): IGetInboxIdQueryOptions {
+ const { account } = args;
+ return {
+ queryKey: ["inboxId", account],
+ queryFn: () => getInboxId(account),
+ enabled: !!account,
+ };
+}
+
+export function useInboxIdQuery(args: { account: string }) {
+ const { account } = args;
+ return useQuery(getInboxIdQueryOptions({ account }));
+}
+
+export function getInboxIdFromQueryData(args: { account: string }) {
+ const { account } = args;
+ return queryClient.getQueryData(
+ getInboxIdQueryOptions({ account }).queryKey
+ );
+}
+
+export function prefetchInboxIdQuery(args: { account: string }) {
+ const { account } = args;
+ return queryClient.prefetchQuery(getInboxIdQueryOptions({ account }));
+}
diff --git a/queries/useConversationMessages.ts b/queries/useConversationMessages.ts
index c14ecae95..9c47dfcea 100644
--- a/queries/useConversationMessages.ts
+++ b/queries/useConversationMessages.ts
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
-
-import { isReactionMessage } from "@/components/Chat/Message/message-utils";
+import { isReactionMessage } from "@/features/conversation/conversation-message/conversation-message.utils";
+import { contentTypesPrefixes } from "@/utils/xmtpRN/content-types/content-types";
import logger from "@utils/logger";
import {
ConversationWithCodecsType,
@@ -25,82 +25,152 @@ export type ConversationMessagesQueryData = Awaited<
ReturnType
>;
-export const conversationMessagesQueryFn = async (
- conversation: ConversationWithCodecsType,
- options?: MessagesOptions
-) => {
- logger.info("[useConversationMessages] queryFn fetching messages");
-
- if (!conversation) {
- throw new Error("Conversation not found in conversationMessagesQueryFn");
- }
+const ignoredContentTypesPrefixes = [
+ contentTypesPrefixes.coinbasePayment,
+ contentTypesPrefixes.transactionReference,
+ contentTypesPrefixes.readReceipt,
+];
- const messages = await conversation.messages(options);
- const ids: MessageId[] = [];
- const byId: Record = {};
- const reactions: Record<
+type IMessageAccumulator = {
+ ids: MessageId[];
+ byId: Record;
+ reactions: Record<
MessageId,
{
bySender: Record;
byReactionContent: Record;
}
- > = {};
+ >;
+};
- for (const message of messages) {
- ids.push(message.id as MessageId);
- byId[message.id as MessageId] = message;
+function processMessages(args: {
+ messages: DecodedMessageWithCodecsType[];
+ existingData?: IMessageAccumulator;
+ prependNewMessages?: boolean;
+}): IMessageAccumulator {
+ const { messages, existingData, prependNewMessages = false } = args;
- if (!message.contentTypeId.includes("reaction:")) {
- continue;
+ const result: IMessageAccumulator = existingData
+ ? { ...existingData }
+ : {
+ ids: [],
+ byId: {},
+ reactions: {},
+ };
+
+ // For now, we ignore messages with these content types
+ const validMessages = messages.filter(
+ (message) =>
+ !ignoredContentTypesPrefixes.some((prefix) =>
+ message.contentTypeId.startsWith(prefix)
+ )
+ );
+
+ // First process regular messages
+ for (const message of validMessages) {
+ // We handle reactions after
+ if (!isReactionMessage(message)) {
+ const messageId = message.id as MessageId;
+
+ // WIP
+ // Find matching optimistic message using correlationId from message root
+ // const optimisticMessage = optimisticMessages.find(
+ // (msg) => msg.messageId === message.id
+ // );
+
+ // if (optimisticMessage) {
+ // // Remove the optimistic message from tracking and from the result
+ // removeOptimisticMessage(messageId);
+ // // Remove from the query data
+ // result.ids = result.ids.filter((id) => id !== optimisticMessage.tempId);
+ // delete result.byId[optimisticMessage.tempId as MessageId];
+ // }
+
+ // Add the new message
+ result.byId[messageId] = message;
+ if (prependNewMessages) {
+ result.ids = [messageId, ...result.ids];
+ } else if (!result.ids.includes(messageId)) {
+ result.ids.push(messageId);
+ }
}
+ }
+
+ const reactionsMessages = validMessages.filter(isReactionMessage);
+
+ // Track which reactions we've already processed
+ const processedReactions = new Set();
- const reactionContent = message.content() as ReactionContent;
+ for (const reactionMessage of reactionsMessages) {
+ const reactionContent = reactionMessage.content() as ReactionContent;
const referenceMessageId = reactionContent?.reference as MessageId;
+ const senderAddress = reactionMessage.senderAddress as InboxId;
if (!reactionContent || !referenceMessageId) {
continue;
}
- if (!reactions[referenceMessageId]) {
- reactions[referenceMessageId] = {
+ const reactionKey = `${reactionContent.content}-${referenceMessageId}`;
+
+ // Skip if we've already processed a reaction from this sender for this content
+ if (processedReactions.has(reactionKey)) {
+ continue;
+ }
+
+ // Mark this reaction as processed
+ processedReactions.add(reactionKey);
+
+ if (!result.reactions[referenceMessageId]) {
+ result.reactions[referenceMessageId] = {
bySender: {},
byReactionContent: {},
};
}
- const messageReactions = reactions[referenceMessageId];
- const senderAddress = message.senderAddress as InboxId;
+ const messageReactions = result.reactions[referenceMessageId];
if (reactionContent.action === "added") {
- // Add sender to the list of users who used this reaction
- messageReactions.byReactionContent[reactionContent.content] = [
- ...(messageReactions.byReactionContent[reactionContent.content] || []),
- senderAddress,
- ];
-
- // Add reaction to sender's list of reactions
- messageReactions.bySender[senderAddress] = [
- ...(messageReactions.bySender[senderAddress] || []),
- reactionContent,
- ];
+ // Check if this sender already has this reaction for this message
+ const hasExistingReaction = messageReactions.bySender[
+ senderAddress
+ ]?.some((reaction) => reaction.content === reactionContent.content);
+
+ if (!hasExistingReaction) {
+ messageReactions.byReactionContent[reactionContent.content] = [
+ ...(messageReactions.byReactionContent[reactionContent.content] ||
+ []),
+ senderAddress,
+ ];
+ messageReactions.bySender[senderAddress] = [
+ ...(messageReactions.bySender[senderAddress] || []),
+ reactionContent,
+ ];
+ }
} else if (reactionContent.action === "removed") {
- // Remove sender from the list of users who used this reaction
messageReactions.byReactionContent[reactionContent.content] = (
messageReactions.byReactionContent[reactionContent.content] || []
).filter((id) => id !== senderAddress);
-
- // Remove reaction from sender's list of reactions
messageReactions.bySender[senderAddress] = (
messageReactions.bySender[senderAddress] || []
).filter((reaction) => reaction.content !== reactionContent.content);
}
}
- return {
- ids,
- byId,
- reactions,
- };
+ return result;
+}
+
+export const conversationMessagesQueryFn = async (
+ conversation: ConversationWithCodecsType,
+ options?: MessagesOptions
+) => {
+ logger.info("[useConversationMessages] queryFn fetching messages");
+
+ if (!conversation) {
+ throw new Error("Conversation not found in conversationMessagesQueryFn");
+ }
+
+ const messages = await conversation.messages(options);
+ return processMessages({ messages });
};
const conversationMessagesByTopicQueryFn = async (
@@ -143,68 +213,41 @@ export const getConversationMessages = (
);
};
-export const addConversationMessage = (
+export function refetchConversationMessages(
account: string,
- topic: ConversationTopic,
- message: DecodedMessageWithCodecsType
-) => {
- queryClient.setQueryData(
- conversationMessagesQueryKey(account, topic),
- (previousMessages) => {
- if (!previousMessages) {
- return {
- ids: [message.id as MessageId],
- byId: { [message.id]: message },
- reactions: {},
- };
- }
+ topic: ConversationTopic
+) {
+ return queryClient.refetchQueries({
+ queryKey: conversationMessagesQueryKey(account, topic),
+ });
+}
- const newPreviousMessages = {
- ...previousMessages,
- byId: {
- ...previousMessages.byId,
- [message.id as MessageId]: message,
- },
- ids: [message.id as MessageId, ...previousMessages.ids],
- };
+export const addConversationMessage = (args: {
+ account: string;
+ topic: ConversationTopic;
+ message: DecodedMessageWithCodecsType;
+ // isOptimistic?: boolean;
+}) => {
+ const {
+ account,
+ topic,
+ message,
+ // isOptimistic
+ } = args;
- if (isReactionMessage(message)) {
- const reactionContent = message.content() as ReactionContent;
- const reactionContentString = reactionContent.content;
- const referenceMessageId = reactionContent.reference as MessageId;
- const senderAddress = message.senderAddress as InboxId;
-
- const existingReactions = previousMessages.reactions[
- referenceMessageId
- ] || {
- bySender: {},
- byReactionContent: {},
- };
-
- newPreviousMessages.reactions = {
- ...previousMessages.reactions,
- [referenceMessageId]: {
- bySender: {
- ...existingReactions.bySender,
- [senderAddress]: [
- ...(existingReactions.bySender[senderAddress] || []),
- reactionContent,
- ],
- },
- byReactionContent: {
- ...existingReactions.byReactionContent,
- [reactionContentString]: [
- ...(existingReactions.byReactionContent[
- reactionContentString
- ] || []),
- senderAddress,
- ],
- },
- },
- };
- }
+ // WIP
+ // if (isOptimistic) {
+ // addOptimisticMessage(message.id);
+ // }
- return newPreviousMessages;
+ queryClient.setQueryData(
+ conversationMessagesQueryKey(account, topic),
+ (previousMessages) => {
+ return processMessages({
+ messages: [message],
+ existingData: previousMessages,
+ prependNewMessages: true,
+ });
}
);
};
@@ -221,3 +264,36 @@ export const prefetchConversationMessages = async (
},
});
};
+
+// WIP
+// type IOptimisticMessage = {
+// tempId: string;
+// messageId?: MessageId;
+// };
+
+// // Keep track of optimistic messages
+// let optimisticMessages: IOptimisticMessage[] = [];
+
+// function addOptimisticMessage(tempId: string) {
+// optimisticMessages.push({
+// tempId,
+// });
+// }
+
+// function removeOptimisticMessage(messageId: MessageId) {
+// optimisticMessages = optimisticMessages.filter(
+// (msg) => msg.messageId !== messageId
+// );
+// }
+
+// export function updateConversationMessagesOptimisticMessages(
+// tempId: string,
+// messageId: MessageId
+// ) {
+// const optimisticMessage = optimisticMessages.find(
+// (msg) => msg.tempId === tempId
+// );
+// if (optimisticMessage) {
+// optimisticMessage.messageId = messageId;
+// }
+// }
diff --git a/queries/useConversationQuery.ts b/queries/useConversationQuery.ts
index a37d16df9..b4a12449f 100644
--- a/queries/useConversationQuery.ts
+++ b/queries/useConversationQuery.ts
@@ -36,6 +36,17 @@ export const invalidateConversationQuery = (
});
};
+export function updateConversationQueryData(
+ account: string,
+ topic: ConversationTopic,
+ conversation: ConversationQueryData
+) {
+ queryClient.setQueryData(
+ conversationQueryKey(account, topic),
+ conversation
+ );
+}
+
export const setConversationQueryData = (
account: string,
topic: ConversationTopic,
diff --git a/queries/useConversationWithPeerQuery.ts b/queries/useConversationWithPeerQuery.ts
index 175c02ad2..03bbdea4a 100644
--- a/queries/useConversationWithPeerQuery.ts
+++ b/queries/useConversationWithPeerQuery.ts
@@ -1,32 +1,55 @@
-import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import { queryClient } from "@/queries/queryClient";
+import { setConversationQueryData } from "@/queries/useConversationQuery";
+import { useQuery } from "@tanstack/react-query";
import logger from "@utils/logger";
-import { ConversationWithCodecsType } from "@utils/xmtpRN/client";
+import { DmWithCodecsType } from "@utils/xmtpRN/client";
import { getConversationByPeerByAccount } from "@utils/xmtpRN/conversations";
import { conversationWithPeerQueryKey } from "./QueryKeys";
-export const useConversationWithPeerQuery = (
- account: string,
- peer: string | undefined,
- options?: Partial<
- UseQueryOptions
- >
-) => {
+type ConversationWithPeerQueryData = Awaited<
+ ReturnType
+>;
+
+async function fetchConversationWithPeer(args: {
+ account: string;
+ peer: string;
+}) {
+ const { account, peer } = args;
+
+ logger.info("[Crash Debug] queryFn fetching conversation with peer");
+ if (!peer) {
+ return null;
+ }
+
+ const conversation = await getConversationByPeerByAccount({
+ account,
+ peer,
+ includeSync: true,
+ });
+
+ if (!conversation) {
+ return null;
+ }
+
+ setConversationQueryData(account, conversation.topic, conversation);
+ return conversation;
+}
+
+export const useConversationWithPeerQuery = (account: string, peer: string) => {
return useQuery({
- ...options,
queryKey: conversationWithPeerQueryKey(account, peer!),
- queryFn: async () => {
- logger.info("[Crash Debug] queryFn fetching conversation with peer");
- if (!peer) {
- return null;
- }
- const conversation = await getConversationByPeerByAccount({
- account,
- peer,
- includeSync: true,
- });
-
- return conversation;
- },
+ queryFn: () => fetchConversationWithPeer({ account, peer }),
enabled: !!peer,
});
};
+
+export function updateConversationWithPeerQueryData(
+ account: string,
+ peer: string,
+ newConversation: DmWithCodecsType
+) {
+ queryClient.setQueryData(
+ conversationWithPeerQueryKey(account, peer),
+ () => newConversation
+ );
+}
diff --git a/queries/useGroupMembersQuery.ts b/queries/useGroupMembersQuery.ts
index 4acd32ac2..6c63697ad 100644
--- a/queries/useGroupMembersQuery.ts
+++ b/queries/useGroupMembersQuery.ts
@@ -1,7 +1,7 @@
import {
- QueryObserverOptions,
SetDataOptions,
useQuery,
+ UseQueryOptions,
} from "@tanstack/react-query";
import { getCleanAddress } from "@utils/evm/getCleanAddress";
import { ConversationTopic, Member } from "@xmtp/react-native-sdk";
@@ -11,65 +11,69 @@ import { groupMembersQueryKey } from "./QueryKeys";
import { EntityObjectWithAddress, entifyWithAddress } from "./entify";
import { queryClient } from "./queryClient";
import { useGroupQuery } from "./useGroupQuery";
+import { GroupWithCodecsType } from "@/utils/xmtpRN/client.types";
export type GroupMembersSelectData = EntityObjectWithAddress;
+const fetchGroupMembers = async (
+ group: GroupWithCodecsType | undefined | null
+) => {
+ if (!group) {
+ return {
+ byId: {},
+ byAddress: {},
+ ids: [],
+ };
+ }
+ const members = await group.members();
+
+ return entifyWithAddress(
+ members,
+ (member) => member.inboxId,
+ (member) => getCleanAddress(member.addresses[0])
+ );
+};
+
+const groupMembersQueryConfig = (
+ account: string,
+ group: GroupWithCodecsType | undefined | null,
+ enabled: boolean
+): UseQueryOptions => ({
+ queryKey: groupMembersQueryKey(account, group?.topic!),
+ queryFn: () => fetchGroupMembers(group!),
+ enabled,
+});
+
export const useGroupMembersQuery = (
account: string,
- topic: ConversationTopic,
- queryOptions?: Partial>
+ topic: ConversationTopic
) => {
const { data: group } = useGroupQuery(account, topic);
- return useQuery({
- queryKey: groupMembersQueryKey(account, topic!),
- queryFn: async () => {
- if (!group) {
- return {
- byId: {},
- byAddress: {},
- ids: [],
- };
- }
- const updatedMembers = await group.members();
- return entifyWithAddress(
- updatedMembers,
- (member) => member.inboxId,
- // TODO: Multiple addresses support
- (member) => getCleanAddress(member.addresses[0])
- );
- },
- enabled: !!group && !!topic,
- ...queryOptions,
- });
+ const enabled = !!group && !!topic;
+ return useQuery(
+ groupMembersQueryConfig(account, group, enabled)
+ );
};
export const useGroupMembersConversationScreenQuery = (
account: string,
- topic: ConversationTopic,
- queryOptions?: Partial>
+ topic: ConversationTopic
) => {
const { data: group } = useGroupQuery(account, topic);
- return useQuery({
- queryKey: groupMembersQueryKey(account, topic),
- queryFn: async () => {
- if (!group) {
- return {
- byId: {},
- byAddress: {},
- ids: [],
- };
- }
- const members = await group.members();
- return entifyWithAddress(
- members,
- (member) => member.inboxId,
- // TODO: Multiple addresses support
- (member) => getCleanAddress(member.addresses[0])
- );
- },
- enabled: !!group,
- ...queryOptions,
- });
+ const enabled = !!group && !!topic;
+ return useQuery(
+ groupMembersQueryConfig(account, group, enabled)
+ );
+};
+
+export const useConversationListMembersQuery = (
+ account: string,
+ group: GroupWithCodecsType | undefined | null
+) => {
+ const enabled = !!group && !group.imageUrlSquare;
+ return useQuery(
+ groupMembersQueryConfig(account, group, enabled)
+ );
};
export const getGroupMembersQueryData = (
@@ -84,7 +88,7 @@ export const setGroupMembersQueryData = (
members: GroupMembersSelectData,
options?: SetDataOptions
) => {
- queryClient.setQueryData(
+ queryClient.setQueryData(
groupMembersQueryKey(account, topic),
members,
options
diff --git a/queries/useInboxProfileSocialsQuery.ts b/queries/useInboxProfileSocialsQuery.ts
index 69c6a11f8..c4cbb1392 100644
--- a/queries/useInboxProfileSocialsQuery.ts
+++ b/queries/useInboxProfileSocialsQuery.ts
@@ -9,13 +9,17 @@ import {
import { queryClient } from "./queryClient";
import { InboxId } from "@xmtp/react-native-sdk";
+import mmkv from "@/utils/mmkv";
const profileSocialsQueryKey = (account: string, peerAddress: string) => [
"inboxProfileSocials",
- account,
- peerAddress,
+ account?.toLowerCase(),
+ peerAddress?.toLowerCase(),
];
+const profileSocialsQueryStorageKey = (account: string, inboxId: InboxId) =>
+ profileSocialsQueryKey(account, inboxId).join("-");
+
const profileSocials = create({
fetcher: async (inboxIds: InboxId[]) => {
const data = await getProfilesForInboxIds({ inboxIds });
@@ -28,8 +32,17 @@ const profileSocials = create({
}),
});
-const fetchInboxProfileSocials = async (inboxIds: InboxId) => {
- const data = await profileSocials.fetch(inboxIds);
+const fetchInboxProfileSocials = async (account: string, inboxId: InboxId) => {
+ const data = await profileSocials.fetch(inboxId);
+
+ const key = profileSocialsQueryStorageKey(account, inboxId);
+
+ mmkv.delete(key);
+
+ if (data) {
+ mmkv.set(key, JSON.stringify(data));
+ }
+
return data;
};
@@ -38,7 +51,7 @@ const inboxProfileSocialsQueryConfig = (
inboxId: InboxId | undefined
) => ({
queryKey: profileSocialsQueryKey(account, inboxId!),
- queryFn: () => fetchInboxProfileSocials(inboxId!),
+ queryFn: () => fetchInboxProfileSocials(account, inboxId!),
enabled: !!account && !!inboxId,
// Store for 30 days
gcTime: 1000 * 60 * 60 * 24 * 30,
@@ -48,6 +61,7 @@ const inboxProfileSocialsQueryConfig = (
// And automatic retries if there was an error fetching
refetchOnMount: false,
staleTime: 1000 * 60 * 60 * 24,
+ // persister: reactQueryPersister,
});
export const useInboxProfileSocialsQuery = (
diff --git a/queries/useProfileSocialsQuery.ts b/queries/useProfileSocialsQuery.ts
index 8b43b8f42..a57d2a8ef 100644
--- a/queries/useProfileSocialsQuery.ts
+++ b/queries/useProfileSocialsQuery.ts
@@ -8,15 +8,19 @@ import {
} from "@yornaath/batshit";
import { queryClient } from "./queryClient";
+import mmkv from "@/utils/mmkv";
type ProfileSocialsData = ProfileSocials | null | undefined;
const profileSocialsQueryKey = (account: string, peerAddress: string) => [
"profileSocials",
- account,
+ account?.toLowerCase(),
peerAddress,
];
+const profileSocialsQueryStorageKey = (account: string, peerAddress: string) =>
+ profileSocialsQueryKey(account, peerAddress).join("-");
+
const profileSocials = create({
fetcher: async (addresses: string[]) => {
const data = await getProfilesForAddresses(addresses);
@@ -29,14 +33,23 @@ const profileSocials = create({
}),
});
-const fetchProfileSocials = async (peerAddress: string) => {
+const fetchProfileSocials = async (account: string, peerAddress: string) => {
const data = await profileSocials.fetch(peerAddress);
+
+ const key = profileSocialsQueryStorageKey(account, peerAddress);
+
+ mmkv.delete(key);
+
+ if (data) {
+ mmkv.set(key, JSON.stringify(data));
+ }
+
return data;
};
const profileSocialsQueryConfig = (account: string, peerAddress: string) => ({
queryKey: profileSocialsQueryKey(account, peerAddress),
- queryFn: () => fetchProfileSocials(peerAddress),
+ queryFn: () => fetchProfileSocials(account, peerAddress),
enabled: !!account,
// Store for 30 days
gcTime: 1000 * 60 * 60 * 24 * 30,
@@ -46,6 +59,7 @@ const profileSocialsQueryConfig = (account: string, peerAddress: string) => ({
// And automatic retries if there was an error fetching
refetchOnMount: false,
staleTime: 1000 * 60 * 60 * 24,
+ // persister: reactQueryPersister,
});
export const useProfileSocialsQuery = (
diff --git a/queries/useV3ConversationListQuery.ts b/queries/useV3ConversationListQuery.ts
index edad31362..253aff9f3 100644
--- a/queries/useV3ConversationListQuery.ts
+++ b/queries/useV3ConversationListQuery.ts
@@ -1,5 +1,9 @@
import { QueryKeys } from "@queries/QueryKeys";
-import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import {
+ useQuery,
+ UseQueryOptions,
+ QueryObserver,
+} from "@tanstack/react-query";
import logger from "@utils/logger";
import {
ConversationWithCodecsType,
@@ -14,7 +18,6 @@ import { setGroupIsActiveQueryData } from "./useGroupIsActive";
import { setGroupNameQueryData } from "./useGroupNameQuery";
import { setGroupPhotoQueryData } from "./useGroupPhotoQuery";
import { ConversationTopic, ConversationVersion } from "@xmtp/react-native-sdk";
-import { useAppStore } from "@/data/store/appStore";
export const conversationListKey = (account: string) => [
QueryKeys.V3_CONVERSATION_LIST,
@@ -80,51 +83,67 @@ const v3ConversationListQueryFn = async (
}
};
+const v3ConversationListQueryConfig = (
+ account: string,
+ context: string,
+ includeSync: boolean = true
+) => ({
+ queryKey: conversationListKey(account),
+ queryFn: () => v3ConversationListQueryFn(account, context, includeSync),
+ staleTime: 2000,
+ enabled: !!account,
+});
+
+export const createV3ConversationListQueryObserver = (
+ account: string,
+ context: string,
+ includeSync: boolean = true
+) => {
+ return new QueryObserver(
+ queryClient,
+ v3ConversationListQueryConfig(account, context, includeSync)
+ );
+};
+
export const useV3ConversationListQuery = (
account: string,
queryOptions?: Partial>,
context?: string
) => {
return useQuery({
- staleTime: 2000,
+ ...v3ConversationListQueryConfig(account, context ?? ""),
...queryOptions,
- queryKey: conversationListKey(account),
- queryFn: () => v3ConversationListQueryFn(account, context ?? ""),
- enabled: !!account,
});
};
export const fetchPersistedConversationListQuery = (account: string) => {
- return queryClient.fetchQuery({
- queryKey: conversationListKey(account),
- queryFn: () =>
- v3ConversationListQueryFn(
- account,
- "fetchPersistedConversationListQuery",
- false
- ),
- });
+ return queryClient.fetchQuery(
+ v3ConversationListQueryConfig(
+ account,
+ "fetchPersistedConversationListQuery",
+ false
+ )
+ );
};
export const fetchConversationListQuery = (account: string) => {
- return queryClient.fetchQuery({
- queryKey: conversationListKey(account),
- queryFn: () =>
- v3ConversationListQueryFn(account, "fetchGroupsConversationListQuery"),
- });
+ return queryClient.fetchQuery(
+ v3ConversationListQueryConfig(account, "fetchConversationListQuery")
+ );
};
export const prefetchConversationListQuery = (account: string) => {
- return queryClient.prefetchQuery({
- queryKey: conversationListKey(account),
- queryFn: () =>
- v3ConversationListQueryFn(account, "prefetchConversationListQuery"),
- });
+ return queryClient.prefetchQuery(
+ v3ConversationListQueryConfig(account, "prefetchConversationListQuery")
+ );
};
export const invalidateGroupsConversationListQuery = (account: string) => {
return queryClient.invalidateQueries({
- queryKey: conversationListKey(account),
+ queryKey: v3ConversationListQueryConfig(
+ account,
+ "invalidateGroupsConversationListQuery"
+ ).queryKey,
});
};
@@ -132,7 +151,8 @@ const getConversationListQueryData = (
account: string
): V3ConversationListType | undefined => {
return queryClient.getQueryData(
- conversationListKey(account)
+ v3ConversationListQueryConfig(account, "getConversationListQueryData")
+ .queryKey
);
};
@@ -141,7 +161,8 @@ const setConversationListQueryData = (
conversations: V3ConversationListType
) => {
return queryClient.setQueryData(
- conversationListKey(account),
+ v3ConversationListQueryConfig(account, "setConversationListQueryData")
+ .queryKey,
conversations
);
};
diff --git a/screens/Accounts/Accounts.tsx b/screens/Accounts/Accounts.tsx
index 7e1d2b6fa..206f39529 100644
--- a/screens/Accounts/Accounts.tsx
+++ b/screens/Accounts/Accounts.tsx
@@ -16,17 +16,16 @@ import {
useErroredAccountsMap,
} from "../../data/store/accountsStore";
import { useRouter } from "../../navigation/useNavigation";
-import { useAccountsProfiles } from "../../utils/str";
+import { useAccountsProfiles } from "@utils/useAccountsProfiles";
import { NavigationParamList } from "../Navigation/Navigation";
-import { shortAddress } from "@utils/strings/shortAddress";
import { translate } from "@/i18n";
export default function Accounts(
props: NativeStackScreenProps
) {
const styles = useStyles();
- const accounts = useAccountsList();
const erroredAccounts = useErroredAccountsMap();
+ const accounts = useAccountsList();
const accountsProfiles = useAccountsProfiles();
const setCurrentAccount = useAccountsStore((s) => s.setCurrentAccount);
const colorScheme = useColorScheme();
@@ -40,9 +39,9 @@ export default function Accounts(
style={styles.accounts}
>
({
+ items={accounts.map((a, index) => ({
id: a,
- title: accountsProfiles[a] || shortAddress(a),
+ title: accountsProfiles[index],
action: () => {
setCurrentAccount(a, false);
router.navigate("Chats");
diff --git a/screens/Conversation.tsx b/screens/Conversation.tsx
deleted file mode 100644
index 3aa17fe2c..000000000
--- a/screens/Conversation.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { NativeStackScreenProps } from "@react-navigation/native-stack";
-import React from "react";
-import { gestureHandlerRootHOC } from "react-native-gesture-handler";
-import { NavigationParamList } from "./Navigation/Navigation";
-import { V3ConversationFromPeer } from "../features/conversations/components/V3ConversationFromPeer";
-import { VStack } from "@design-system/VStack";
-import { isV3Topic } from "@utils/groupUtils/groupId";
-import { V3Conversation } from "@components/Conversation/V3Conversation";
-
-const ConversationHoc = ({
- route,
-}: NativeStackScreenProps) => {
- if (route.params?.topic && isV3Topic(route.params.topic)) {
- return (
-
- );
- }
- if (route.params?.mainConversationWithPeer) {
- return (
-
- );
- }
- return ;
-};
-
-export default gestureHandlerRootHOC(ConversationHoc);
diff --git a/screens/ConversationList.tsx b/screens/ConversationList.tsx
index 2492a645e..7f94c2c16 100644
--- a/screens/ConversationList.tsx
+++ b/screens/ConversationList.tsx
@@ -17,7 +17,7 @@ import {
import { gestureHandlerRootHOC } from "react-native-gesture-handler";
import { SearchBarCommands } from "react-native-screens";
-import ChatNullState from "../components/Chat/ChatNullState";
+import ChatNullState from "../components/ConversationList/ChatNullState";
import ConversationFlashList from "../components/ConversationFlashList";
import NewConversationButton from "../components/ConversationList/NewConversationButton";
import RequestsButton from "../components/ConversationList/RequestsButton";
@@ -46,7 +46,7 @@ import { ConversationVersion } from "@xmtp/react-native-sdk";
import {
dmMatchesSearchQuery,
groupMatchesSearchQuery,
-} from "@/features/conversations/utils/search";
+} from "@/features/conversation/utils/search";
import { translate } from "@/i18n";
type Props = {
diff --git a/screens/ConversationReadOnly.tsx b/screens/ConversationReadOnly.tsx
index e3a747b32..ac3085d34 100644
--- a/screens/ConversationReadOnly.tsx
+++ b/screens/ConversationReadOnly.tsx
@@ -1,56 +1,78 @@
-import { MessagesList } from "@/components/Conversation/V3Conversation";
import { useCurrentAccount } from "@/data/store/accountsStore";
-import { AnimatedVStack } from "@/design-system/VStack";
-import { ConversationContextProvider } from "@/features/conversation/conversation-context";
-import {
- initializeCurrentConversation,
- useConversationCurrentTopic,
-} from "@/features/conversation/conversation-service";
+import { Center } from "@/design-system/Center";
+import { Text } from "@/design-system/Text";
+import { VStack } from "@/design-system/VStack";
+import { Loader } from "@/design-system/loader";
+import { ConversationMessageTimestamp } from "@/features/conversation/conversation-message/conversation-message-timestamp";
+import { MessageContextStoreProvider } from "@/features/conversation/conversation-message/conversation-message.store-context";
+import { ConversationMessage } from "@/features/conversation/conversation-message/conversation-message";
+import { ConversationMessageLayout } from "@/features/conversation/conversation-message/conversation-message-layout";
+import { ConversationMessageReactions } from "@/features/conversation/conversation-message/conversation-message-reactions/conversation-message-reactions";
+import { ConversationMessagesList } from "@/features/conversation/conversation-messages-list";
+import { ConversationStoreProvider } from "@/features/conversation/conversation.store-context";
import { useConversationPreviewMessages } from "@/queries/useConversationPreviewMessages";
-import { useAppTheme } from "@/theme/useAppTheme";
+import { useConversationQuery } from "@/queries/useConversationQuery";
+import { $globalStyles } from "@/theme/styles";
import type { ConversationTopic } from "@xmtp/react-native-sdk";
-import React, { memo } from "react";
+import React from "react";
type ConversationReadOnlyProps = {
topic: ConversationTopic;
};
export const ConversationReadOnly = ({ topic }: ConversationReadOnlyProps) => {
- initializeCurrentConversation({
- topic,
- peerAddress: undefined,
- inputValue: "",
- });
-
- return (
-
-
-
- );
-};
-
-const Content = memo(function Content() {
const currentAccount = useCurrentAccount()!;
- const { theme } = useAppTheme();
-
- const topic = useConversationCurrentTopic();
-
const { data: messages, isLoading: isLoadingMessages } =
useConversationPreviewMessages(currentAccount, topic!);
- if (isLoadingMessages) {
- return null;
- }
+ const { data: conversation, isLoading: isLoadingConversation } =
+ useConversationQuery(currentAccount, topic);
+
+ const isLoading = isLoadingMessages || isLoadingConversation;
return (
-
-
-
+ {isLoading ? (
+
+
+
+ ) : !conversation ? (
+
+ Conversation not found
+
+ ) : (
+
+ {
+ const message = messages?.byId[messageId]!;
+ const previousMessage = messages?.byId[messages?.ids[index + 1]];
+ const nextMessage = messages?.byId[messages?.ids[index - 1]];
+
+ return (
+
+
+
+
+
+
+
+ );
+ }}
+ />
+
+ )}
+
);
-});
+};
diff --git a/screens/ConverseMatchMaker.tsx b/screens/ConverseMatchMaker.tsx
index 181f493be..c494bfabe 100644
--- a/screens/ConverseMatchMaker.tsx
+++ b/screens/ConverseMatchMaker.tsx
@@ -12,6 +12,7 @@ import {
import { NavigationParamList } from "./Navigation/Navigation";
import AndroidBackAction from "../components/AndroidBackAction";
import Recommendations from "../components/Recommendations/Recommendations";
+import { translate } from "@/i18n";
export default function ConverseMatchMaker({
route,
@@ -22,7 +23,7 @@ export default function ConverseMatchMaker({
headerLeft: () =>
Platform.OS === "ios" ? (