From 0ff307af616ae41be7ff604ad4a4d402389e0d87 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 20 Sep 2024 10:37:56 +0200 Subject: [PATCH 01/20] WIP Conversation context menu --- components/ConversationListItem.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index a9e52cb7b..3ea80c062 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -25,6 +25,7 @@ import React, { } from "react"; import { ColorSchemeName, + LayoutChangeEvent, Platform, StyleSheet, Text, @@ -34,6 +35,7 @@ import { import { RectButton } from "react-native-gesture-handler"; import Swipeable from "react-native-gesture-handler/Swipeable"; import { TouchableRipple } from "react-native-paper"; +import { useSharedValue, useAnimatedRef } from "react-native-reanimated"; import Avatar from "./Avatar"; import GroupAvatar from "./GroupAvatar"; @@ -114,6 +116,26 @@ const ConversationListItem = memo(function ConversationListItem({ gcTime: Infinity, }); + const [isContextMenuVisible, setIsContextMenuVisible] = useState(false); + const itemRect = useSharedValue({ x: 0, y: 0, width: 0, height: 0 }); + const containerRef = useAnimatedRef(); + + const onLayoutView = useCallback( + (event: LayoutChangeEvent) => { + const { x, y, width, height } = event.nativeEvent.layout; + itemRect.value = { x, y, width, height }; + }, + [itemRect] + ); + + const showContextMenu = useCallback(() => { + setIsContextMenuVisible(true); + }, []); + + const closeContextMenu = useCallback(() => { + setIsContextMenuVisible(false); + }, []); + const openConversation = useCallback(async () => { const getUserAction = async () => { const methods = { @@ -418,6 +440,7 @@ const ConversationListItem = memo(function ConversationListItem({ ); }, [showUnread, styles.leftAction, colorScheme]); + // TODO: Move to context menu const onLongPress = useCallback(() => { setPinnedConversations([conversationTopic]); }, [conversationTopic, setPinnedConversations]); From bedec11d6066c69a1cd0d2f4c0a9daaa1ee43754 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Wed, 25 Sep 2024 12:14:20 +0200 Subject: [PATCH 02/20] Add translations --- i18n/translations/en.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts index dacc78cca..752a4f8da 100644 --- a/i18n/translations/en.ts +++ b/i18n/translations/en.ts @@ -203,6 +203,10 @@ const en = { do_you_want_to_join_this_group: "Do you want to join this group?", join_this_group: "Join this group", + // Conversation Context Menu + pin: "Pin", + mark_as_unread: "Mark as unread", + // NewGroupSummary group_name: "GROUP NAME", group_description: "GROUP DESCRIPTION", From db3265e6f426ce52716c196d039d769c84b39408 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Wed, 25 Sep 2024 12:14:44 +0200 Subject: [PATCH 03/20] WIP Conversation context menu --- components/ConversationContextMenu.tsx | 92 +++++++++++++++++ components/ConversationListItem.tsx | 132 ++++++++++++++++--------- 2 files changed, 178 insertions(+), 46 deletions(-) create mode 100644 components/ConversationContextMenu.tsx diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx new file mode 100644 index 000000000..c9fd71967 --- /dev/null +++ b/components/ConversationContextMenu.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { Modal, View, Text, TouchableOpacity, StyleSheet } from "react-native"; +import Animated, { + SharedValue, + useAnimatedStyle, + withTiming, +} from "react-native-reanimated"; + +type ConversationContextMenuProps = { + isVisible: boolean; + onClose: () => void; + items: { title: string; action: () => void; id: string }[]; + itemRect: SharedValue<{ + x: number; + y: number; + width: number; + height: number; + }>; +}; + +export const ConversationContextMenu: React.FC< + ConversationContextMenuProps +> = ({ isVisible, onClose, items, itemRect }) => { + const animatedStyle = useAnimatedStyle(() => { + return { + position: "absolute", + top: itemRect.value.y, + left: itemRect.value.x, + width: itemRect.value.width, + height: itemRect.value.height, + opacity: withTiming(isVisible ? 1 : 0), + }; + }); + + if (!isVisible) return null; + + return ( + + + + + + {/* Add conversation preview here */} + Preview + + {items.map((item) => ( + + {item.title} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + contextMenuContainer: { + backgroundColor: "white", + borderRadius: 12, + padding: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + previewContainer: { + marginBottom: 8, + padding: 8, + backgroundColor: "#f0f0f0", + borderRadius: 8, + }, + menuItem: { + padding: 12, + borderBottomWidth: 1, + borderBottomColor: "#e0e0e0", + }, +}); diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index 3ea80c062..8711b9423 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -32,12 +32,21 @@ import { TouchableHighlight, View, } from "react-native"; -import { RectButton } from "react-native-gesture-handler"; +import { + Gesture, + GestureDetector, + RectButton, +} from "react-native-gesture-handler"; import Swipeable from "react-native-gesture-handler/Swipeable"; import { TouchableRipple } from "react-native-paper"; -import { useSharedValue, useAnimatedRef } from "react-native-reanimated"; +import Animated, { + useSharedValue, + useAnimatedRef, + runOnJS, +} from "react-native-reanimated"; import Avatar from "./Avatar"; +import { ConversationContextMenu } from "./ConversationContextMenu"; import GroupAvatar from "./GroupAvatar"; import Picto from "./Picto/Picto"; import { showActionSheetWithOptions } from "./StateHandlers/ActionSheetStateHandler"; @@ -136,6 +145,54 @@ const ConversationListItem = memo(function ConversationListItem({ setIsContextMenuVisible(false); }, []); + const triggerHapticFeedback = useCallback(() => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + }, []); + + const longPressGesture = useMemo(() => { + return Gesture.LongPress() + .onStart(() => { + runOnJS(triggerHapticFeedback)(); + runOnJS(showContextMenu)(); + }) + .minDuration(500); + }, [triggerHapticFeedback, showContextMenu]); + + const contextMenuItems = useMemo( + () => [ + { + title: translate("pin"), + action: () => { + setPinnedConversations([conversationTopic]); + closeContextMenu(); + }, + id: "pin", + }, + { + title: translate("mark_as_unread"), + action: () => { + setTopicsData({ + [conversationTopic]: { + status: "unread", + timestamp: new Date().getTime(), + }, + }); + closeContextMenu(); + }, + id: "markAsUnread", + }, + { + title: translate("delete"), + action: () => { + // Implement delete logic here + closeContextMenu(); + }, + id: "delete", + }, + ], + [closeContextMenu, conversationTopic, setPinnedConversations, setTopicsData] + ); + const openConversation = useCallback(async () => { const getUserAction = async () => { const methods = { @@ -445,49 +502,27 @@ const ConversationListItem = memo(function ConversationListItem({ setPinnedConversations([conversationTopic]); }, [conversationTopic, setPinnedConversations]); - const rowItem = - Platform.OS === "ios" || Platform.OS === "web" ? ( - { - if (!isSplitScreen) return; - openConversation(); - }} - onPress={() => { - if (isSplitScreen) return; - openConversation(); - setSelected(true); - }} - style={{ - backgroundColor: - selected || (isSplitScreen && conversationOpened) - ? clickedItemBackgroundColor(colorScheme) - : backgroundColor(colorScheme), - height: 76, - }} - > - {listItemContent} - - ) : ( - { - if (!isSplitScreen) return; - openConversation(); - }} - onPress={() => { - if (isSplitScreen) return; - openConversation(); - }} - onLongPress={onLongPress} - style={styles.rippleRow} - rippleColor={clickedItemBackgroundColor(colorScheme)} - > - {listItemContent} - - ); + const rowItem = ( + + + {Platform.OS === "ios" ? ( + + {listItemContent} + + ) : ( + + {listItemContent} + + )} + + + ); const toggleUnreadStatusOnClose = useRef(false); const [swipeableKey, setSwipeableKey] = useState(0); @@ -541,8 +576,13 @@ const ConversationListItem = memo(function ConversationListItem({ hitSlop={{ left: isSplitScreen ? 0 : -6 }} > {rowItem} + - {/* Hide part of the border to mimic margin*/} {Platform.OS === "ios" && } ); From 95543c03cffce91c42d6fe33919598964e4059df Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Wed, 25 Sep 2024 16:01:35 +0200 Subject: [PATCH 04/20] Use Portal and TableView --- components/ConversationContextMenu.tsx | 304 ++++++++++++++++++++----- components/ConversationListItem.tsx | 1 + 2 files changed, 249 insertions(+), 56 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index c9fd71967..480d817fd 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -1,92 +1,284 @@ -import React from "react"; -import { Modal, View, Text, TouchableOpacity, StyleSheet } from "react-native"; +import TableView, { TableViewItemType } from "@components/TableView/TableView"; +import { backgroundColor } from "@styles/colors"; +import { calculateMenuHeight } from "@utils/contextMenu/calculateMenuHeight"; +import { + AUXILIARY_VIEW_MIN_HEIGHT, + BACKDROP_DARK_BACKGROUND_COLOR, + BACKDROP_LIGHT_BACKGROUND_COLOR, + HOLD_ITEM_TRANSFORM_DURATION, + ITEM_WIDTH, + SIDE_MARGIN, + SPRING_CONFIGURATION, +} from "@utils/contextMenu/constants"; +import { ConversationContext } from "@utils/conversation"; +import { BlurView } from "expo-blur"; +import React, { FC, memo, useEffect, useMemo } from "react"; +import { + Platform, + StyleSheet, + TouchableWithoutFeedback, + useColorScheme, + useWindowDimensions, + View, +} from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { Portal } from "react-native-paper"; import Animated, { SharedValue, + useAnimatedProps, useAnimatedStyle, + useSharedValue, + withDelay, + withSpring, withTiming, } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useContext } from "use-context-selector"; + +const AnimatedBlurView = + Platform.OS === "ios" + ? Animated.createAnimatedComponent(BlurView) + : Animated.createAnimatedComponent(View); type ConversationContextMenuProps = { isVisible: boolean; onClose: () => void; - items: { title: string; action: () => void; id: string }[]; + items: TableViewItemType[]; itemRect: SharedValue<{ x: number; y: number; width: number; height: number; }>; + conversation: { + name: string; + lastMessagePreview?: string; + }; + fromMe?: boolean; + children?: React.ReactNode; }; -export const ConversationContextMenu: React.FC< - ConversationContextMenuProps -> = ({ isVisible, onClose, items, itemRect }) => { - const animatedStyle = useAnimatedStyle(() => { +const ConversationContextMenuComponent: FC = ({ + isVisible, + onClose, + items, + itemRect, + conversation, + fromMe = false, + children, +}) => { + const conversationContext = useContext(ConversationContext); + const activeValue = useSharedValue(false); + const opacityValue = useSharedValue(0); + const intensityValue = useSharedValue(0); + const { height, width } = useWindowDimensions(); + const safeAreaInsets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + + useEffect(() => { + activeValue.value = isVisible; + opacityValue.value = withTiming(isVisible ? 1 : 0, { + duration: HOLD_ITEM_TRANSFORM_DURATION, + }); + intensityValue.value = withTiming(isVisible ? 50 : 0, { + duration: HOLD_ITEM_TRANSFORM_DURATION, + }); + }, [activeValue, isVisible, opacityValue, intensityValue]); + + const menuHeight = useMemo(() => { + return calculateMenuHeight(items.length); + }, [items]); + + const animatedContainerProps = useAnimatedProps(() => ({ + intensity: intensityValue.value, + })); + + const animatedInnerContainerStyle = useAnimatedStyle(() => ({ + backgroundColor: + colorScheme === "dark" + ? BACKDROP_DARK_BACKGROUND_COLOR + : BACKDROP_LIGHT_BACKGROUND_COLOR, + })); + + const animatedPreviewStyle = useAnimatedStyle(() => { + const getTransformValue = () => { + if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { + const spacing = 16; + const topTransform = + itemRect.value.y + + itemRect.value.height + + menuHeight + + spacing + + (safeAreaInsets?.bottom || 0); + return topTransform > height ? height - topTransform : 0; + } else { + return ( + -1 * + (itemRect.value.y - AUXILIARY_VIEW_MIN_HEIGHT - safeAreaInsets.top) + ); + } + }; + const tY = getTransformValue(); + return { + position: "absolute", + bottom: + height - + Math.max(itemRect.value.y - 10 + tY, AUXILIARY_VIEW_MIN_HEIGHT), + height: Math.max( + itemRect.value.y - itemRect.value.height - safeAreaInsets.top + tY, + AUXILIARY_VIEW_MIN_HEIGHT + ), + width: width - 2 * SIDE_MARGIN, + left: fromMe ? undefined : itemRect.value.x, + right: fromMe + ? width - itemRect.value.x - itemRect.value.width + : undefined, + marginRight: fromMe ? 0 : SIDE_MARGIN, + marginLeft: fromMe ? SIDE_MARGIN : 0, + }; + }); + + const animatedMenuStyle = useAnimatedStyle(() => { + const getTransformValue = () => { + if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { + const spacing = 10; + const topTransform = + itemRect.value.y + + itemRect.value.height + + menuHeight + + spacing + + (safeAreaInsets?.bottom || 0); + const ty = topTransform > height ? height - topTransform : 0; + return ty; + } else { + return ( + -1 * + (itemRect.value.y - + AUXILIARY_VIEW_MIN_HEIGHT - + safeAreaInsets.top - + 5) + ); + } + }; + + const tY = getTransformValue(); + const transformAnimation = () => + isVisible + ? withSpring(tY, SPRING_CONFIGURATION) + : withTiming(0, { duration: HOLD_ITEM_TRANSFORM_DURATION }); + return { + position: "absolute", + top: itemRect.value.y + itemRect.value.height, + left: fromMe ? undefined : itemRect.value.x, + right: fromMe + ? width - itemRect.value.x - itemRect.value.width + : undefined, + width: ITEM_WIDTH, + transform: [ + { + translateY: transformAnimation(), + }, + ], + }; + }); + + const animatedConversationStyle = useAnimatedStyle(() => { + const animateOpacity = () => + withDelay(HOLD_ITEM_TRANSFORM_DURATION, withTiming(0, { duration: 0 })); + const getTransformValue = () => { + if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { + const spacing = 15; + const topTransform = + itemRect.value.y + + itemRect.value.height + + menuHeight + + spacing + + (safeAreaInsets?.bottom || 0); + const ty = topTransform > height ? height - topTransform : 0; + return ty; + } else { + return ( + -1 * + (itemRect.value.y - AUXILIARY_VIEW_MIN_HEIGHT - safeAreaInsets.top) + ); + } + }; + + const tY = getTransformValue(); + const transformAnimation = () => + isVisible + ? withSpring(tY, SPRING_CONFIGURATION) + : withTiming(-0.1, { duration: HOLD_ITEM_TRANSFORM_DURATION }); return { position: "absolute", top: itemRect.value.y, left: itemRect.value.x, - width: itemRect.value.width, height: itemRect.value.height, - opacity: withTiming(isVisible ? 1 : 0), + width: itemRect.value.width, + opacity: isVisible ? 1 : animateOpacity(), + transform: [ + { + translateY: transformAnimation(), + }, + { + scale: 1.05, + }, + ], }; }); - if (!isVisible) return null; + if (!isVisible) { + return null; + } return ( - - - - - - {/* Add conversation preview here */} - Preview - - {items.map((item) => ( - + + + + + - {item.title} - - ))} - - - - + + {children} + + + {/* Add conversation preview here */} + + + + + + + + + + ); }; const styles = StyleSheet.create({ - overlay: { + gestureHandlerContainer: { flex: 1, - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - contextMenuContainer: { - backgroundColor: "white", - borderRadius: 12, - padding: 8, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 3.84, - elevation: 5, }, - previewContainer: { - marginBottom: 8, - padding: 8, - backgroundColor: "#f0f0f0", - borderRadius: 8, - }, - menuItem: { - padding: 12, - borderBottomWidth: 1, - borderBottomColor: "#e0e0e0", + flex: { + flex: 1, }, }); + +export const ConversationContextMenu = memo(ConversationContextMenuComponent); diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index 8711b9423..52a054fde 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -581,6 +581,7 @@ const ConversationListItem = memo(function ConversationListItem({ onClose={closeContextMenu} items={contextMenuItems} itemRect={itemRect} + conversation={{ name: conversationName, lastMessagePreview }} /> {Platform.OS === "ios" && } From 2f25b7567761896e3c7e2fd314c2aa1dda8835cb Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Wed, 25 Sep 2024 19:53:25 +0200 Subject: [PATCH 05/20] Implement preview and context menu functionality with initial styling, and blurred background --- components/ConversationContextMenu.tsx | 267 +++++++------------------ components/ConversationListItem.tsx | 6 - 2 files changed, 67 insertions(+), 206 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 480d817fd..57225304e 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -1,19 +1,10 @@ import TableView, { TableViewItemType } from "@components/TableView/TableView"; import { backgroundColor } from "@styles/colors"; -import { calculateMenuHeight } from "@utils/contextMenu/calculateMenuHeight"; -import { - AUXILIARY_VIEW_MIN_HEIGHT, - BACKDROP_DARK_BACKGROUND_COLOR, - BACKDROP_LIGHT_BACKGROUND_COLOR, - HOLD_ITEM_TRANSFORM_DURATION, - ITEM_WIDTH, - SIDE_MARGIN, - SPRING_CONFIGURATION, -} from "@utils/contextMenu/constants"; -import { ConversationContext } from "@utils/conversation"; +import { HOLD_ITEM_TRANSFORM_DURATION } from "@utils/contextMenu/constants"; import { BlurView } from "expo-blur"; -import React, { FC, memo, useEffect, useMemo } from "react"; +import React, { FC, memo, useEffect } from "react"; import { + Text, Platform, StyleSheet, TouchableWithoutFeedback, @@ -21,19 +12,14 @@ import { useWindowDimensions, View, } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Portal } from "react-native-paper"; import Animated, { - SharedValue, useAnimatedProps, useAnimatedStyle, useSharedValue, - withDelay, - withSpring, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useContext } from "use-context-selector"; const AnimatedBlurView = Platform.OS === "ios" @@ -44,34 +30,22 @@ type ConversationContextMenuProps = { isVisible: boolean; onClose: () => void; items: TableViewItemType[]; - itemRect: SharedValue<{ - x: number; - y: number; - width: number; - height: number; - }>; conversation: { name: string; lastMessagePreview?: string; }; - fromMe?: boolean; - children?: React.ReactNode; }; const ConversationContextMenuComponent: FC = ({ isVisible, onClose, items, - itemRect, conversation, - fromMe = false, - children, }) => { - const conversationContext = useContext(ConversationContext); const activeValue = useSharedValue(false); const opacityValue = useSharedValue(0); const intensityValue = useSharedValue(0); - const { height, width } = useWindowDimensions(); + const { height } = useWindowDimensions(); const safeAreaInsets = useSafeAreaInsets(); const colorScheme = useColorScheme(); @@ -85,145 +59,19 @@ const ConversationContextMenuComponent: FC = ({ }); }, [activeValue, isVisible, opacityValue, intensityValue]); - const menuHeight = useMemo(() => { - return calculateMenuHeight(items.length); - }, [items]); + const translateY = useSharedValue(height); - const animatedContainerProps = useAnimatedProps(() => ({ - intensity: intensityValue.value, - })); + useEffect(() => { + translateY.value = withTiming(isVisible ? 0 : height, { duration: 300 }); + }, [isVisible, translateY, height]); - const animatedInnerContainerStyle = useAnimatedStyle(() => ({ - backgroundColor: - colorScheme === "dark" - ? BACKDROP_DARK_BACKGROUND_COLOR - : BACKDROP_LIGHT_BACKGROUND_COLOR, + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], })); - const animatedPreviewStyle = useAnimatedStyle(() => { - const getTransformValue = () => { - if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { - const spacing = 16; - const topTransform = - itemRect.value.y + - itemRect.value.height + - menuHeight + - spacing + - (safeAreaInsets?.bottom || 0); - return topTransform > height ? height - topTransform : 0; - } else { - return ( - -1 * - (itemRect.value.y - AUXILIARY_VIEW_MIN_HEIGHT - safeAreaInsets.top) - ); - } - }; - const tY = getTransformValue(); - return { - position: "absolute", - bottom: - height - - Math.max(itemRect.value.y - 10 + tY, AUXILIARY_VIEW_MIN_HEIGHT), - height: Math.max( - itemRect.value.y - itemRect.value.height - safeAreaInsets.top + tY, - AUXILIARY_VIEW_MIN_HEIGHT - ), - width: width - 2 * SIDE_MARGIN, - left: fromMe ? undefined : itemRect.value.x, - right: fromMe - ? width - itemRect.value.x - itemRect.value.width - : undefined, - marginRight: fromMe ? 0 : SIDE_MARGIN, - marginLeft: fromMe ? SIDE_MARGIN : 0, - }; - }); - - const animatedMenuStyle = useAnimatedStyle(() => { - const getTransformValue = () => { - if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { - const spacing = 10; - const topTransform = - itemRect.value.y + - itemRect.value.height + - menuHeight + - spacing + - (safeAreaInsets?.bottom || 0); - const ty = topTransform > height ? height - topTransform : 0; - return ty; - } else { - return ( - -1 * - (itemRect.value.y - - AUXILIARY_VIEW_MIN_HEIGHT - - safeAreaInsets.top - - 5) - ); - } - }; - - const tY = getTransformValue(); - const transformAnimation = () => - isVisible - ? withSpring(tY, SPRING_CONFIGURATION) - : withTiming(0, { duration: HOLD_ITEM_TRANSFORM_DURATION }); - return { - position: "absolute", - top: itemRect.value.y + itemRect.value.height, - left: fromMe ? undefined : itemRect.value.x, - right: fromMe - ? width - itemRect.value.x - itemRect.value.width - : undefined, - width: ITEM_WIDTH, - transform: [ - { - translateY: transformAnimation(), - }, - ], - }; - }); - - const animatedConversationStyle = useAnimatedStyle(() => { - const animateOpacity = () => - withDelay(HOLD_ITEM_TRANSFORM_DURATION, withTiming(0, { duration: 0 })); - const getTransformValue = () => { - if (itemRect.value.y > AUXILIARY_VIEW_MIN_HEIGHT + safeAreaInsets.top) { - const spacing = 15; - const topTransform = - itemRect.value.y + - itemRect.value.height + - menuHeight + - spacing + - (safeAreaInsets?.bottom || 0); - const ty = topTransform > height ? height - topTransform : 0; - return ty; - } else { - return ( - -1 * - (itemRect.value.y - AUXILIARY_VIEW_MIN_HEIGHT - safeAreaInsets.top) - ); - } - }; - - const tY = getTransformValue(); - const transformAnimation = () => - isVisible - ? withSpring(tY, SPRING_CONFIGURATION) - : withTiming(-0.1, { duration: HOLD_ITEM_TRANSFORM_DURATION }); + const animatedContainerProps = useAnimatedProps(() => { return { - position: "absolute", - top: itemRect.value.y, - left: itemRect.value.x, - height: itemRect.value.height, - width: itemRect.value.width, - opacity: isVisible ? 1 : animateOpacity(), - transform: [ - { - translateY: transformAnimation(), - }, - { - scale: 1.05, - }, - ], + intensity: intensityValue.value, }; }); @@ -233,48 +81,67 @@ const ConversationContextMenuComponent: FC = ({ return ( - - - - - - - {children} - - - {/* Add conversation preview here */} - - - - - - - - - + + + + + + {conversation.name} + + {conversation.lastMessagePreview} + + + + + + + + + ); }; const styles = StyleSheet.create({ - gestureHandlerContainer: { + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + container: { flex: 1, + justifyContent: "flex-end", + }, + previewContainer: { + flex: 1, + backgroundColor: "white", + padding: 20, + justifyContent: "center", + }, + conversationName: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 10, + }, + lastMessagePreview: { + fontSize: 16, + }, + menuContainer: { + maxHeight: 300, }, flex: { flex: 1, diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index 52a054fde..cf3a7dc2a 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -497,11 +497,6 @@ const ConversationListItem = memo(function ConversationListItem({ ); }, [showUnread, styles.leftAction, colorScheme]); - // TODO: Move to context menu - const onLongPress = useCallback(() => { - setPinnedConversations([conversationTopic]); - }, [conversationTopic, setPinnedConversations]); - const rowItem = ( @@ -580,7 +575,6 @@ const ConversationListItem = memo(function ConversationListItem({ isVisible={isContextMenuVisible} onClose={closeContextMenu} items={contextMenuItems} - itemRect={itemRect} conversation={{ name: conversationName, lastMessagePreview }} /> From 9debe2ad4eb78051450daff4e71513e0ae016b5c Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 26 Sep 2024 11:57:02 +0200 Subject: [PATCH 06/20] Add dismissable gesture to ConversationContextMenu - Implement drag-to-dismiss functionality - Wrap component with GestureHandlerRootView - Update to use current Gesture API --- components/ConversationContextMenu.tsx | 98 +++++++++++++++++--------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 57225304e..0eb0b6f4f 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -2,18 +2,23 @@ import TableView, { TableViewItemType } from "@components/TableView/TableView"; import { backgroundColor } from "@styles/colors"; import { HOLD_ITEM_TRANSFORM_DURATION } from "@utils/contextMenu/constants"; import { BlurView } from "expo-blur"; -import React, { FC, memo, useEffect } from "react"; +import React, { FC, memo, useCallback, useEffect } from "react"; import { Text, Platform, StyleSheet, - TouchableWithoutFeedback, useColorScheme, useWindowDimensions, View, } from "react-native"; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, +} from "react-native-gesture-handler"; import { Portal } from "react-native-paper"; import Animated, { + runOnJS, useAnimatedProps, useAnimatedStyle, useSharedValue, @@ -75,44 +80,69 @@ const ConversationContextMenuComponent: FC = ({ }; }); + const closeMenu = useCallback(() => { + translateY.value = withTiming(height, { duration: 300 }, () => { + runOnJS(onClose)(); + }); + }, [height, onClose, translateY]); + + const gesture = Gesture.Pan() + .onStart(() => { + translateY.value = translateY.value; + }) + .onUpdate((event) => { + translateY.value = Math.max(0, event.translationY); + }) + .onEnd((event) => { + if (event.velocityY > 500 || event.translationY > height * 0.2) { + runOnJS(closeMenu)(); + } else { + translateY.value = withTiming(0, { duration: 300 }); + } + }); + if (!isVisible) { return null; } return ( - - - - - - {conversation.name} - - {conversation.lastMessagePreview} - - - - - - - - - + + + + + + + + {conversation.name} + + + {conversation.lastMessagePreview} + + + + + + + + + + ); }; From 83d5f14fa96a7fe53d4bdd1a8e9c721474de6e9a Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 26 Sep 2024 14:00:34 +0200 Subject: [PATCH 07/20] Context menu styling --- components/ConversationContextMenu.tsx | 72 ++++++++++++++++++-------- utils/contextMenu/constants.ts | 2 +- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 0eb0b6f4f..a518e76c7 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -1,6 +1,10 @@ import TableView, { TableViewItemType } from "@components/TableView/TableView"; -import { backgroundColor } from "@styles/colors"; -import { HOLD_ITEM_TRANSFORM_DURATION } from "@utils/contextMenu/constants"; +import { + SIDE_MARGIN, + AUXILIARY_VIEW_MIN_HEIGHT, + HOLD_ITEM_TRANSFORM_DURATION, + contextMenuStyleGuide, +} from "@utils/contextMenu/constants"; import { BlurView } from "expo-blur"; import React, { FC, memo, useCallback, useEffect } from "react"; import { @@ -50,7 +54,7 @@ const ConversationContextMenuComponent: FC = ({ const activeValue = useSharedValue(false); const opacityValue = useSharedValue(0); const intensityValue = useSharedValue(0); - const { height } = useWindowDimensions(); + const { height, width } = useWindowDimensions(); const safeAreaInsets = useSafeAreaInsets(); const colorScheme = useColorScheme(); @@ -67,7 +71,9 @@ const ConversationContextMenuComponent: FC = ({ const translateY = useSharedValue(height); useEffect(() => { - translateY.value = withTiming(isVisible ? 0 : height, { duration: 300 }); + translateY.value = withTiming(isVisible ? 0 : height, { + duration: HOLD_ITEM_TRANSFORM_DURATION, + }); }, [isVisible, translateY, height]); const animatedStyle = useAnimatedStyle(() => ({ @@ -81,9 +87,13 @@ const ConversationContextMenuComponent: FC = ({ }); const closeMenu = useCallback(() => { - translateY.value = withTiming(height, { duration: 300 }, () => { - runOnJS(onClose)(); - }); + translateY.value = withTiming( + height, + { duration: HOLD_ITEM_TRANSFORM_DURATION }, + () => { + runOnJS(onClose)(); + } + ); }, [height, onClose, translateY]); const gesture = Gesture.Pan() @@ -97,7 +107,9 @@ const ConversationContextMenuComponent: FC = ({ if (event.velocityY > 500 || event.translationY > height * 0.2) { runOnJS(closeMenu)(); } else { - translateY.value = withTiming(0, { duration: 300 }); + translateY.value = withTiming(0, { + duration: HOLD_ITEM_TRANSFORM_DURATION, + }); } }); @@ -116,6 +128,7 @@ const ConversationContextMenuComponent: FC = ({ > + {conversation.name} @@ -127,13 +140,7 @@ const ConversationContextMenuComponent: FC = ({ @@ -156,22 +163,41 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "flex-end", }, + handle: { + marginTop: 70, + marginBottom: 10, + width: 36, + height: 5, + backgroundColor: contextMenuStyleGuide.palette.secondary, + alignSelf: "center", + borderRadius: 2.5, + }, previewContainer: { flex: 1, - backgroundColor: "white", - padding: 20, - justifyContent: "center", + margin: SIDE_MARGIN, + padding: contextMenuStyleGuide.spacing, + justifyContent: "flex-start", + minHeight: AUXILIARY_VIEW_MIN_HEIGHT, + backgroundColor: contextMenuStyleGuide.palette.common.white, + borderRadius: 16, }, conversationName: { - fontSize: 24, - fontWeight: "bold", - marginBottom: 10, + ...contextMenuStyleGuide.typography.body, + fontWeight: "600", + marginBottom: contextMenuStyleGuide.spacing, }, lastMessagePreview: { - fontSize: 16, + ...contextMenuStyleGuide.typography.callout, + color: + Platform.OS === "ios" + ? contextMenuStyleGuide.palette.secondary + : contextMenuStyleGuide.palette.common.black, }, menuContainer: { - maxHeight: 300, + marginHorizontal: SIDE_MARGIN, + minHeight: 300, + borderRadius: 16, + overflow: "hidden", }, flex: { flex: 1, diff --git a/utils/contextMenu/constants.ts b/utils/contextMenu/constants.ts index 88d701b08..cc2963775 100644 --- a/utils/contextMenu/constants.ts +++ b/utils/contextMenu/constants.ts @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -const HOLD_ITEM_TRANSFORM_DURATION = 150; +const HOLD_ITEM_TRANSFORM_DURATION = 200; const MENU_ITEM_HEIGHT = 44; const SPRING_CONFIGURATION = { From 95c1cba3de747b18d264580c24b310160063031e Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 26 Sep 2024 18:08:58 +0200 Subject: [PATCH 08/20] Fix gesture conflict --- components/ConversationListItem.tsx | 164 ++++++++++++++++------------ utils/contextMenu/constants.ts | 2 +- 2 files changed, 95 insertions(+), 71 deletions(-) diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index cf3a7dc2a..b1b5a0d45 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -32,11 +32,7 @@ import { TouchableHighlight, View, } from "react-native"; -import { - Gesture, - GestureDetector, - RectButton, -} from "react-native-gesture-handler"; +import { RectButton } from "react-native-gesture-handler"; import Swipeable from "react-native-gesture-handler/Swipeable"; import { TouchableRipple } from "react-native-paper"; import Animated, { @@ -149,50 +145,11 @@ const ConversationListItem = memo(function ConversationListItem({ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); }, []); - const longPressGesture = useMemo(() => { - return Gesture.LongPress() - .onStart(() => { - runOnJS(triggerHapticFeedback)(); - runOnJS(showContextMenu)(); - }) - .minDuration(500); + const onLongPress = useCallback(() => { + runOnJS(triggerHapticFeedback)(); + runOnJS(showContextMenu)(); }, [triggerHapticFeedback, showContextMenu]); - const contextMenuItems = useMemo( - () => [ - { - title: translate("pin"), - action: () => { - setPinnedConversations([conversationTopic]); - closeContextMenu(); - }, - id: "pin", - }, - { - title: translate("mark_as_unread"), - action: () => { - setTopicsData({ - [conversationTopic]: { - status: "unread", - timestamp: new Date().getTime(), - }, - }); - closeContextMenu(); - }, - id: "markAsUnread", - }, - { - title: translate("delete"), - action: () => { - // Implement delete logic here - closeContextMenu(); - }, - id: "delete", - }, - ], - [closeContextMenu, conversationTopic, setPinnedConversations, setTopicsData] - ); - const openConversation = useCallback(async () => { const getUserAction = async () => { const methods = { @@ -356,7 +313,7 @@ const ConversationListItem = memo(function ConversationListItem({ swipeableRef.current?.close(); }, []); - const handleRightPress = useCallback(() => { + const handleDelete = useCallback(() => { if (onRightActionPress) { onRightActionPress(closeSwipeable); return; @@ -462,7 +419,7 @@ const ConversationListItem = memo(function ConversationListItem({ const renderRightActions = useCallback(() => { if (isBlockedChatView) { return ( - + + ); @@ -480,7 +437,7 @@ const ConversationListItem = memo(function ConversationListItem({ }, [ styles.rightAction, styles.rightActionRed, - handleRightPress, + handleDelete, isBlockedChatView, colorScheme, ]); @@ -497,26 +454,92 @@ const ConversationListItem = memo(function ConversationListItem({ ); }, [showUnread, styles.leftAction, colorScheme]); + const contextMenuItems = useMemo( + () => [ + { + title: translate("pin"), + action: () => { + setPinnedConversations([conversationTopic]); + closeContextMenu(); + }, + id: "pin", + }, + { + title: translate("mark_as_unread"), + action: () => { + setTopicsData({ + [conversationTopic]: { + status: "unread", + timestamp: new Date().getTime(), + }, + }); + closeContextMenu(); + }, + id: "markAsUnread", + }, + { + title: translate("delete"), + action: () => { + handleDelete(); + closeContextMenu(); + }, + id: "delete", + }, + ], + [ + conversationTopic, + setPinnedConversations, + setTopicsData, + handleDelete, + closeContextMenu, + ] + ); + const rowItem = ( - - - {Platform.OS === "ios" ? ( - - {listItemContent} - - ) : ( - - {listItemContent} - - )} - - + + {Platform.OS === "ios" || Platform.OS === "web" ? ( + { + if (!isSplitScreen) return; + openConversation(); + }} + onPress={() => { + if (isSplitScreen) return; + openConversation(); + setSelected(true); + }} + style={{ + backgroundColor: + selected || (isSplitScreen && conversationOpened) + ? clickedItemBackgroundColor(colorScheme) + : backgroundColor(colorScheme), + height: 76, + }} + > + {listItemContent} + + ) : ( + { + if (!isSplitScreen) return; + openConversation(); + }} + onPress={() => { + if (isSplitScreen) return; + openConversation(); + }} + onLongPress={onLongPress} + style={styles.rippleRow} + rippleColor={clickedItemBackgroundColor(colorScheme)} + > + {listItemContent} + + )} + ); const toggleUnreadStatusOnClose = useRef(false); @@ -571,6 +594,7 @@ const ConversationListItem = memo(function ConversationListItem({ hitSlop={{ left: isSplitScreen ? 0 : -6 }} > {rowItem} + Date: Thu, 26 Sep 2024 18:14:29 +0200 Subject: [PATCH 09/20] Reusable component --- components/ConversationListItem.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index b1b5a0d45..13fbc73a6 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -545,6 +545,24 @@ const ConversationListItem = memo(function ConversationListItem({ const toggleUnreadStatusOnClose = useRef(false); const [swipeableKey, setSwipeableKey] = useState(0); + const contextMenuComponent = useMemo( + () => ( + + ), + [ + isContextMenuVisible, + closeContextMenu, + contextMenuItems, + conversationName, + lastMessagePreview, + ] + ); + return ( {rowItem} - - + {contextMenuComponent} {Platform.OS === "ios" && } From 1722595dc66b650f392ad9cc342c1cde39de64e8 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 09:21:46 +0200 Subject: [PATCH 10/20] Create a lightweight read-only conversation component for auxiliary display - Remove default export from ConversationReadOnly - Auxiliary display container styling - Hide scrollbar for read-only conversation preview --- components/Chat/Chat.tsx | 34 +++-- components/ConversationContextMenu.tsx | 19 +-- components/ConversationListItem.tsx | 6 +- screens/ConversationReadOnly.tsx | 201 +++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 screens/ConversationReadOnly.tsx diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 4cb6a6ec2..5e2239c9b 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -173,7 +173,7 @@ const getListArray = ( return reverseArray; }; -export default function Chat() { +export default function Chat({ readOnly = false }: { readOnly?: boolean }) { const conversation = useConversationContext("conversation"); const isBlockedPeer = useConversationContext("isBlockedPeer"); const onReadyToFocus = useConversationContext("onReadyToFocus"); @@ -249,12 +249,20 @@ export default function Chat() { const chatContentStyle = useAnimatedStyle( () => ({ ...styles.chatContent, - paddingBottom: showChatInput + paddingBottom: readOnly + ? 0 // Remove bottom padding if readOnly is true + : showChatInput ? chatInputDisplayedHeight.value + Math.max(insets.bottom, keyboardHeight.value) : insets.bottom, }), - [showChatInput, keyboardHeight, chatInputDisplayedHeight, insets.bottom] + [ + showChatInput, + keyboardHeight, + chatInputDisplayedHeight, + insets.bottom, + readOnly, + ] ); const ListFooterComponent = useMemo(() => { @@ -366,6 +374,12 @@ export default function Chat() { [framesStore] ); + const handleOnLayout = useCallback(() => { + setTimeout(() => { + onReadyToFocus(); + }, 50); + }, [onReadyToFocus]); + return ( { - setTimeout(() => { - onReadyToFocus(); - }, 50); - }} + onLayout={handleOnLayout} ref={(r) => { if (r) { messageListRef.current = r; @@ -408,7 +418,8 @@ export default function Chat() { keyboardShouldPersistTaps="handled" estimatedItemSize={80} // Size glitch on Android - showsVerticalScrollIndicator={Platform.OS === "ios"} + showsVerticalScrollIndicator={!readOnly && Platform.OS === "ios"} + pointerEvents={readOnly ? "none" : "auto"} ListFooterComponent={ListFooterComponent} /> )} @@ -418,9 +429,10 @@ export default function Chat() { {showPlaceholder && conversation?.isGroup && ( )} - {conversation?.isGroup ? : } + {!readOnly && + (conversation?.isGroup ? : )} - {showChatInput && ( + {!readOnly && showChatInput && ( <> void; items: TableViewItemType[]; - conversation: { - name: string; - lastMessagePreview?: string; - }; + conversationTopic: string; }; const ConversationContextMenuComponent: FC = ({ isVisible, onClose, items, - conversation, + conversationTopic, }) => { const activeValue = useSharedValue(false); const opacityValue = useSharedValue(0); @@ -130,12 +127,7 @@ const ConversationContextMenuComponent: FC = ({ - - {conversation.name} - - - {conversation.lastMessagePreview} - + ), [ isContextMenuVisible, closeContextMenu, contextMenuItems, - conversationName, - lastMessagePreview, + conversationTopic, ] ); @@ -614,6 +613,7 @@ const ConversationListItem = memo(function ConversationListItem({ {rowItem} {contextMenuComponent} + {/* Hide part of the border to mimic margin*/} {Platform.OS === "ios" && } ); diff --git a/screens/ConversationReadOnly.tsx b/screens/ConversationReadOnly.tsx new file mode 100644 index 000000000..9530b7284 --- /dev/null +++ b/screens/ConversationReadOnly.tsx @@ -0,0 +1,201 @@ +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { backgroundColor, headerTitleStyle } from "@styles/colors"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { StyleSheet, useColorScheme, View } from "react-native"; + +import { NavigationParamList } from "./Navigation/Navigation"; +import ConverseChat from "../components/Chat/Chat"; +import { EmojiPicker } from "../containers/EmojiPicker"; +import { + currentAccount, + useChatStore, + useSettingsStore, +} from "../data/store/accountsStore"; +import { MediaPreview } from "../data/store/chatStore"; +import { useSelect } from "../data/store/storeHelpers"; +import { ConversationContext } from "../utils/conversation"; +import { setTopicToNavigateTo, topicToNavigateTo } from "../utils/navigation"; +import { TextInputWithValue } from "../utils/str"; +import { loadOlderMessages } from "../utils/xmtpRN/messages"; + +export const ConversationReadOnly = ({ + topic, + readOnly, +}: NativeStackScreenProps & { + topic?: string; + readOnly?: boolean; +}) => { + const colorScheme = useColorScheme(); + const peersStatus = useSettingsStore((s) => s.peersStatus); + const [transactionMode, setTransactionMode] = useState(false); + const [frameTextInputFocused, setFrameTextInputFocused] = useState(false); + const tagsFetchedOnceForMessage = useRef<{ [messageId: string]: boolean }>( + {} + ); + + const { + conversations, + conversationsMapping, + setConversationMessageDraft, + setConversationMediaPreview, + } = useChatStore( + useSelect([ + "conversations", + "conversationsMapping", + "setConversationMessageDraft", + "setConversationMediaPreview", + "lastUpdateAt", // Added even if unused to trigger a rerender + ]) + ); + + // Initial conversation topic is be set from the 'topic' prop + const [_conversationTopic, setConversationTopic] = useState(topic); + + // When we set the conversation topic, we check if it has been mapped + // to a new one (for pending conversations) + const conversationTopic = + _conversationTopic && conversationsMapping[_conversationTopic] + ? conversationsMapping[_conversationTopic] + : _conversationTopic; + + // Initial conversation will be set only if topic exists + const [conversation, setConversation] = useState( + conversationTopic ? conversations[conversationTopic] : undefined + ); + + // Initial peer address will be set from the conversation object if it exists + const [peerAddress, setPeerAddress] = useState( + conversation?.peerAddress || "" + ); + + // When we set the conversation, we set the peer address + // and preload the local convo for faster sending + useEffect(() => { + if (conversation && conversation.peerAddress !== peerAddress) { + setPeerAddress(conversation.peerAddress || ""); + } + }, [conversation, peerAddress]); + + // When the conversation topic changes, we set the conversation object + const conversationTopicRef = useRef(conversationTopic); + const currentLastUpdateAt = conversation?.lastUpdateAt; + useEffect(() => { + if ( + conversationTopic && + (conversationTopicRef.current !== conversationTopic || + conversations[conversationTopic]?.lastUpdateAt !== currentLastUpdateAt) + ) { + const foundConversation = conversations[conversationTopic]; + if (foundConversation) { + setConversation(foundConversation); + } + } + conversationTopicRef.current = conversationTopic; + }, [currentLastUpdateAt, conversationTopic, conversations]); + + const isBlockedPeer = useMemo( + () => + conversation?.peerAddress + ? peersStatus[conversation.peerAddress.toLowerCase()] === "blocked" + : false, + [conversation?.peerAddress, peersStatus] + ); + + const textInputRef = useRef(); + const mediaPreviewRef = useRef(); + + const messageToPrefill = ""; + const mediaPreviewToPrefill = null; + const focusOnLayout = useRef(false); + const chatLayoutDone = useRef(false); + const alreadyAutomaticallyFocused = useRef(false); + + const onReadyToFocus = useCallback(() => { + if (alreadyAutomaticallyFocused.current) return; + if (focusOnLayout.current && !chatLayoutDone.current) { + chatLayoutDone.current = true; + alreadyAutomaticallyFocused.current = true; + textInputRef.current?.focus(); + } else { + chatLayoutDone.current = true; + } + }, []); + + const styles = useStyles(); + + useEffect(() => { + if (conversation) { + // On load, we mark the conversation as read and as opened + useChatStore.getState().setOpenedConversationTopic(conversation.topic); + + // On Web this loads them from network, on mobile from local db + loadOlderMessages(currentAccount(), conversation.topic); + + // If we are navigating to a conversation, we reset the topic to navigate to + if (topicToNavigateTo === conversation.topic) { + setTopicToNavigateTo(""); + } + } + }, [conversation]); + + const onLeaveScreen = useCallback(async () => { + if (!conversation) return; + + useChatStore.getState().setOpenedConversationTopic(null); + if (textInputRef.current) { + setConversationMessageDraft( + conversation.topic, + textInputRef.current.currentValue + ); + } + setConversationMediaPreview( + conversation.topic, + mediaPreviewRef.current || null + ); + }, [conversation, setConversationMessageDraft, setConversationMediaPreview]); + + return ( + + {conversationTopic ? ( + + + + ) : ( + + )} + + + ); +}; + +const useStyles = () => { + const colorScheme = useColorScheme(); + return StyleSheet.create({ + container: { + flex: 1, + }, + title: headerTitleStyle(colorScheme), + filler: { flex: 1, backgroundColor: backgroundColor(colorScheme) }, + }); +}; From 239382dcb5e6e0935db150fe9931c1cb3da05116 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 09:59:28 +0200 Subject: [PATCH 11/20] Fix type issue on read-only conversation --- screens/ConversationReadOnly.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/screens/ConversationReadOnly.tsx b/screens/ConversationReadOnly.tsx index 9530b7284..6d07bb29b 100644 --- a/screens/ConversationReadOnly.tsx +++ b/screens/ConversationReadOnly.tsx @@ -1,4 +1,3 @@ -import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { backgroundColor, headerTitleStyle } from "@styles/colors"; import React, { useCallback, @@ -9,7 +8,6 @@ import React, { } from "react"; import { StyleSheet, useColorScheme, View } from "react-native"; -import { NavigationParamList } from "./Navigation/Navigation"; import ConverseChat from "../components/Chat/Chat"; import { EmojiPicker } from "../containers/EmojiPicker"; import { @@ -24,13 +22,10 @@ import { setTopicToNavigateTo, topicToNavigateTo } from "../utils/navigation"; import { TextInputWithValue } from "../utils/str"; import { loadOlderMessages } from "../utils/xmtpRN/messages"; -export const ConversationReadOnly = ({ - topic, - readOnly, -}: NativeStackScreenProps & { +export const ConversationReadOnly: React.FC<{ topic?: string; readOnly?: boolean; -}) => { +}> = ({ topic, readOnly = true }) => { const colorScheme = useColorScheme(); const peersStatus = useSettingsStore((s) => s.peersStatus); const [transactionMode, setTransactionMode] = useState(false); From 3c503f7098477901f69a96fb32619a10226a5a5f Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 10:10:43 +0200 Subject: [PATCH 12/20] Fix dark mode styling for auxiliary preview --- components/ConversationContextMenu.tsx | 108 +++++++++++++------------ 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 6e3e22dbd..718a009a0 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -1,5 +1,6 @@ import TableView, { TableViewItemType } from "@components/TableView/TableView"; import { ConversationReadOnly } from "@screens/ConversationReadOnly"; +import { backgroundColor } from "@styles/colors"; import { SIDE_MARGIN, AUXILIARY_VIEW_MIN_HEIGHT, @@ -28,7 +29,6 @@ import Animated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; const AnimatedBlurView = Platform.OS === "ios" @@ -52,8 +52,7 @@ const ConversationContextMenuComponent: FC = ({ const opacityValue = useSharedValue(0); const intensityValue = useSharedValue(0); const { height, width } = useWindowDimensions(); - const safeAreaInsets = useSafeAreaInsets(); - const colorScheme = useColorScheme(); + const styles = useStyles(); useEffect(() => { activeValue.value = isVisible; @@ -146,55 +145,58 @@ const ConversationContextMenuComponent: FC = ({ ); }; -const styles = StyleSheet.create({ - overlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - container: { - flex: 1, - justifyContent: "flex-end", - }, - handle: { - marginTop: 70, - marginBottom: 10, - width: 36, - height: 5, - backgroundColor: contextMenuStyleGuide.palette.secondary, - alignSelf: "center", - borderRadius: 2.5, - }, - previewContainer: { - flex: 1, - margin: SIDE_MARGIN, - paddingBottom: contextMenuStyleGuide.spacing, - overflow: "hidden", - justifyContent: "flex-start", - minHeight: AUXILIARY_VIEW_MIN_HEIGHT, - backgroundColor: contextMenuStyleGuide.palette.common.white, - borderRadius: 16, - }, - conversationName: { - ...contextMenuStyleGuide.typography.body, - fontWeight: "600", - marginBottom: contextMenuStyleGuide.spacing, - }, - lastMessagePreview: { - ...contextMenuStyleGuide.typography.callout, - color: - Platform.OS === "ios" - ? contextMenuStyleGuide.palette.secondary - : contextMenuStyleGuide.palette.common.black, - }, - menuContainer: { - marginHorizontal: SIDE_MARGIN, - minHeight: 300, - borderRadius: 16, - overflow: "hidden", - }, - flex: { - flex: 1, - }, -}); +const useStyles = () => { + const colorScheme = useColorScheme(); + return StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + container: { + flex: 1, + justifyContent: "flex-end", + }, + handle: { + marginTop: 70, + marginBottom: 10, + width: 36, + height: 5, + backgroundColor: contextMenuStyleGuide.palette.secondary, + alignSelf: "center", + borderRadius: 2.5, + }, + previewContainer: { + flex: 1, + margin: SIDE_MARGIN, + paddingBottom: contextMenuStyleGuide.spacing, + overflow: "hidden", + justifyContent: "flex-start", + minHeight: AUXILIARY_VIEW_MIN_HEIGHT, + backgroundColor: backgroundColor(colorScheme), + borderRadius: 16, + }, + conversationName: { + ...contextMenuStyleGuide.typography.body, + fontWeight: "600", + marginBottom: contextMenuStyleGuide.spacing, + }, + lastMessagePreview: { + ...contextMenuStyleGuide.typography.callout, + color: + Platform.OS === "ios" + ? contextMenuStyleGuide.palette.secondary + : contextMenuStyleGuide.palette.common.black, + }, + menuContainer: { + marginHorizontal: SIDE_MARGIN, + minHeight: 300, + borderRadius: 16, + overflow: "hidden", + }, + flex: { + flex: 1, + }, + }); +}; export const ConversationContextMenu = memo(ConversationContextMenuComponent); From ea3480cdc73ce9779f4fd00328662d88fe2819db Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 10:36:29 +0200 Subject: [PATCH 13/20] Implement `toggleReadStatus` as a callback method --- components/ConversationListItem.tsx | 44 ++++++++++++++++------------- i18n/translations/en.ts | 1 + 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/components/ConversationListItem.tsx b/components/ConversationListItem.tsx index 88a77d4a8..8a76ab18f 100644 --- a/components/ConversationListItem.tsx +++ b/components/ConversationListItem.tsx @@ -454,6 +454,23 @@ const ConversationListItem = memo(function ConversationListItem({ ); }, [showUnread, styles.leftAction, colorScheme]); + const toggleReadStatus = useCallback(() => { + const newStatus = showUnread ? "read" : "unread"; + const timestamp = new Date().getTime(); + setTopicsData({ + [conversationTopic]: { + status: newStatus, + timestamp, + }, + }); + saveTopicsData(currentAccount(), { + [conversationTopic]: { + status: newStatus, + timestamp, + }, + }); + }, [setTopicsData, conversationTopic, showUnread]); + const contextMenuItems = useMemo( () => [ { @@ -465,14 +482,11 @@ const ConversationListItem = memo(function ConversationListItem({ id: "pin", }, { - title: translate("mark_as_unread"), + title: showUnread + ? translate("mark_as_read") + : translate("mark_as_unread"), action: () => { - setTopicsData({ - [conversationTopic]: { - status: "unread", - timestamp: new Date().getTime(), - }, - }); + toggleReadStatus(); closeContextMenu(); }, id: "markAsUnread", @@ -489,9 +503,10 @@ const ConversationListItem = memo(function ConversationListItem({ [ conversationTopic, setPinnedConversations, - setTopicsData, handleDelete, closeContextMenu, + showUnread, + toggleReadStatus, ] ); @@ -591,18 +606,7 @@ const ConversationListItem = memo(function ConversationListItem({ onSwipeableClose={(direction) => { if (direction === "left" && toggleUnreadStatusOnClose.current) { toggleUnreadStatusOnClose.current = false; - setTopicsData({ - [conversationTopic]: { - status: showUnread ? "read" : "unread", - timestamp: new Date().getTime(), - }, - }); - saveTopicsData(currentAccount(), { - [conversationTopic]: { - status: showUnread ? "read" : "unread", - timestamp: new Date().getTime(), - }, - }); + toggleReadStatus(); } if (Platform.OS === "web") { setSwipeableKey(new Date().getTime()); diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts index 752a4f8da..338d2c124 100644 --- a/i18n/translations/en.ts +++ b/i18n/translations/en.ts @@ -205,6 +205,7 @@ const en = { // Conversation Context Menu pin: "Pin", + mark_as_read: "Mark as read", mark_as_unread: "Mark as unread", // NewGroupSummary From 48875d01c296d169c90739bfcaf501563ccf665b Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 10:58:34 +0200 Subject: [PATCH 14/20] Open conversation on tap on auxiliary view --- components/ConversationContextMenu.tsx | 13 +++++---- components/ConversationListItem.tsx | 40 +++++++++++++++----------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 718a009a0..1223945fe 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -37,7 +37,7 @@ const AnimatedBlurView = type ConversationContextMenuProps = { isVisible: boolean; - onClose: () => void; + onClose: (openConversationOnClose?: boolean) => void; items: TableViewItemType[]; conversationTopic: string; }; @@ -93,9 +93,6 @@ const ConversationContextMenuComponent: FC = ({ }, [height, onClose, translateY]); const gesture = Gesture.Pan() - .onStart(() => { - translateY.value = translateY.value; - }) .onUpdate((event) => { translateY.value = Math.max(0, event.translationY); }) @@ -126,7 +123,13 @@ const ConversationContextMenuComponent: FC = ({ - + { + runOnJS(onClose)(true); + })} + > + + { - setIsContextMenuVisible(true); - }, []); - - const closeContextMenu = useCallback(() => { - setIsContextMenuVisible(false); - }, []); - - const triggerHapticFeedback = useCallback(() => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - }, []); - - const onLongPress = useCallback(() => { - runOnJS(triggerHapticFeedback)(); - runOnJS(showContextMenu)(); - }, [triggerHapticFeedback, showContextMenu]); - const openConversation = useCallback(async () => { const getUserAction = async () => { const methods = { @@ -234,6 +217,29 @@ const ConversationListItem = memo(function ConversationListItem({ colorScheme, ]); + const showContextMenu = useCallback(() => { + setIsContextMenuVisible(true); + }, []); + + const closeContextMenu = useCallback( + (openConversationOnClose = false) => { + setIsContextMenuVisible(false); + if (openConversationOnClose) { + openConversation(); + } + }, + [openConversation] + ); + + const triggerHapticFeedback = useCallback(() => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + }, []); + + const onLongPress = useCallback(() => { + runOnJS(triggerHapticFeedback)(); + runOnJS(showContextMenu)(); + }, [triggerHapticFeedback, showContextMenu]); + useEffect(() => { const navRef = navigationRef.current; navRef?.addListener("transitionEnd", resetSelected); From 77275a3d1031010622901815d2c4262e08760fe5 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 11:08:41 +0200 Subject: [PATCH 15/20] iPad and split screen improvements --- components/ConversationContextMenu.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index 1223945fe..d8f0a9f42 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -30,6 +30,8 @@ import Animated, { withTiming, } from "react-native-reanimated"; +import { useIsSplitScreen } from "../screens/Navigation/navHelpers"; + const AnimatedBlurView = Platform.OS === "ios" ? Animated.createAnimatedComponent(BlurView) @@ -51,6 +53,7 @@ const ConversationContextMenuComponent: FC = ({ const activeValue = useSharedValue(false); const opacityValue = useSharedValue(0); const intensityValue = useSharedValue(0); + const isSplitScreen = useIsSplitScreen(); const { height, width } = useWindowDimensions(); const styles = useStyles(); @@ -125,7 +128,12 @@ const ConversationContextMenuComponent: FC = ({ { - runOnJS(onClose)(true); + if (isSplitScreen) { + runOnJS(closeMenu)(); + } else { + // Navigate to conversation + runOnJS(onClose)(true); + } })} > @@ -134,7 +142,8 @@ const ConversationContextMenuComponent: FC = ({ From c98dd73bb9289aa26d7dae6ee8b4b212a07ad450 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 11:42:43 +0200 Subject: [PATCH 16/20] Conversation context menu Android styling --- components/ConversationContextMenu.tsx | 32 +++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index d8f0a9f42..b929c2888 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -6,6 +6,9 @@ import { AUXILIARY_VIEW_MIN_HEIGHT, HOLD_ITEM_TRANSFORM_DURATION, contextMenuStyleGuide, + BACKDROP_DARK_BACKGROUND_COLOR, + BACKDROP_LIGHT_BACKGROUND_COLOR, + ITEM_WIDTH, } from "@utils/contextMenu/constants"; import { BlurView } from "expo-blur"; import React, { FC, memo, useCallback, useEffect } from "react"; @@ -55,6 +58,7 @@ const ConversationContextMenuComponent: FC = ({ const intensityValue = useSharedValue(0); const isSplitScreen = useIsSplitScreen(); const { height, width } = useWindowDimensions(); + const colorScheme = useColorScheme(); const styles = useStyles(); useEffect(() => { @@ -85,6 +89,15 @@ const ConversationContextMenuComponent: FC = ({ }; }); + const backDropContainerStyle = useAnimatedStyle(() => { + const backgroundColor = + colorScheme === "dark" + ? BACKDROP_DARK_BACKGROUND_COLOR + : BACKDROP_LIGHT_BACKGROUND_COLOR; + + return { backgroundColor }; + }, []); + const closeMenu = useCallback(() => { translateY.value = withTiming( height, @@ -119,7 +132,10 @@ const ConversationContextMenuComponent: FC = ({ @@ -140,13 +156,7 @@ const ConversationContextMenuComponent: FC = ({ - + @@ -208,6 +218,12 @@ const useStyles = () => { flex: { flex: 1, }, + table: { + width: ITEM_WIDTH, + backgroundColor: + Platform.OS === "android" ? backgroundColor(colorScheme) : undefined, + borderRadius: Platform.OS === "android" ? 10 : undefined, + }, }); }; From aff36c64e20632ed88227dc87445bd61fb346842 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Fri, 27 Sep 2024 14:04:14 +0200 Subject: [PATCH 17/20] Remove left over prop --- components/ConversationContextMenu.tsx | 2 +- screens/ConversationReadOnly.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index b929c2888..e2aef2c9d 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -152,7 +152,7 @@ const ConversationContextMenuComponent: FC = ({ } })} > - + diff --git a/screens/ConversationReadOnly.tsx b/screens/ConversationReadOnly.tsx index 6d07bb29b..c0f7b4164 100644 --- a/screens/ConversationReadOnly.tsx +++ b/screens/ConversationReadOnly.tsx @@ -24,8 +24,7 @@ import { loadOlderMessages } from "../utils/xmtpRN/messages"; export const ConversationReadOnly: React.FC<{ topic?: string; - readOnly?: boolean; -}> = ({ topic, readOnly = true }) => { +}> = ({ topic }) => { const colorScheme = useColorScheme(); const peersStatus = useSettingsStore((s) => s.peersStatus); const [transactionMode, setTransactionMode] = useState(false); @@ -174,7 +173,7 @@ export const ConversationReadOnly: React.FC<{ tagsFetchedOnceForMessage, }} > - + ) : ( From d90737b6759b164d418c2679e3fd5da7878971fe Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Mon, 30 Sep 2024 11:04:26 +0200 Subject: [PATCH 18/20] Create an `AnimatedBlurView` reusable component --- components/AnimatedBlurView.tsx | 25 +++++++++++++++++++ .../Chat/Message/MessageContextMenu.tsx | 7 +----- components/ConversationContextMenu.tsx | 10 ++------ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 components/AnimatedBlurView.tsx diff --git a/components/AnimatedBlurView.tsx b/components/AnimatedBlurView.tsx new file mode 100644 index 000000000..3e32a7cea --- /dev/null +++ b/components/AnimatedBlurView.tsx @@ -0,0 +1,25 @@ +import { BlurView, BlurViewProps } from "expo-blur"; +import { View, ViewProps, Platform } from "react-native"; +import Animated, { AnimatedProps } from "react-native-reanimated"; + +type AnimatedBlurProps = { + intensity?: number; +}; + +type AnimatedBlurViewProps = ViewProps & + BlurViewProps & { + animatedProps?: AnimatedProps; + }; + +// If BlurView is fixed on Android, +// we only need to update this file to apply the changes across the app +const AnimatedBlurViewComponent = Animated.createAnimatedComponent( + Platform.OS === "ios" ? BlurView : View +); + +export const AnimatedBlurView: React.FC = ({ + animatedProps, + ...props +}) => { + return ; +}; diff --git a/components/Chat/Message/MessageContextMenu.tsx b/components/Chat/Message/MessageContextMenu.tsx index 65ffd1fab..48cf97fae 100644 --- a/components/Chat/Message/MessageContextMenu.tsx +++ b/components/Chat/Message/MessageContextMenu.tsx @@ -1,3 +1,4 @@ +import { AnimatedBlurView } from "@components/AnimatedBlurView"; import TableView, { TableViewItemType } from "@components/TableView/TableView"; import { backgroundColor } from "@styles/colors"; import { calculateMenuHeight } from "@utils/contextMenu/calculateMenuHeight"; @@ -11,7 +12,6 @@ import { SPRING_CONFIGURATION, } from "@utils/contextMenu/constants"; import { ConversationContext } from "@utils/conversation"; -import { BlurView } from "expo-blur"; import React, { FC, memo, useEffect, useMemo } from "react"; import { Platform, @@ -20,7 +20,6 @@ import { TouchableWithoutFeedback, useColorScheme, useWindowDimensions, - View, } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { Portal } from "react-native-paper"; @@ -35,10 +34,6 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useContext } from "use-context-selector"; -const AnimatedBlurView = - Platform.OS === "ios" - ? Animated.createAnimatedComponent(BlurView) - : Animated.createAnimatedComponent(View); const BackdropComponent: FC<{ isActive: boolean; diff --git a/components/ConversationContextMenu.tsx b/components/ConversationContextMenu.tsx index e2aef2c9d..d8ecb4edb 100644 --- a/components/ConversationContextMenu.tsx +++ b/components/ConversationContextMenu.tsx @@ -1,5 +1,7 @@ +import { AnimatedBlurView } from "@components/AnimatedBlurView"; import TableView, { TableViewItemType } from "@components/TableView/TableView"; import { ConversationReadOnly } from "@screens/ConversationReadOnly"; +import { useIsSplitScreen } from "@screens/Navigation/navHelpers"; import { backgroundColor } from "@styles/colors"; import { SIDE_MARGIN, @@ -10,7 +12,6 @@ import { BACKDROP_LIGHT_BACKGROUND_COLOR, ITEM_WIDTH, } from "@utils/contextMenu/constants"; -import { BlurView } from "expo-blur"; import React, { FC, memo, useCallback, useEffect } from "react"; import { Platform, @@ -33,13 +34,6 @@ import Animated, { withTiming, } from "react-native-reanimated"; -import { useIsSplitScreen } from "../screens/Navigation/navHelpers"; - -const AnimatedBlurView = - Platform.OS === "ios" - ? Animated.createAnimatedComponent(BlurView) - : Animated.createAnimatedComponent(View); - type ConversationContextMenuProps = { isVisible: boolean; onClose: (openConversationOnClose?: boolean) => void; From 32789de8805b8076c7ff6523344c2d383f5fad59 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Mon, 30 Sep 2024 11:38:22 +0200 Subject: [PATCH 19/20] Update tsconfig paths to add `containers`, also update legacy babel alias config --- babel.config.js | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/babel.config.js b/babel.config.js index febc99b78..17a7593b6 100644 --- a/babel.config.js +++ b/babel.config.js @@ -33,6 +33,7 @@ module.exports = { // Folder aliases "@components": "./components", + "@containers": "./containers", "@data": "./data", "@hooks": "./hooks", "@i18n": "./i18n", diff --git a/tsconfig.json b/tsconfig.json index 862d424aa..ee2dca960 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "emitDecoratorMetadata": true, "paths": { "@components/*": ["./components/*"], + "@containers/*": ["./containers/*"], "@data/*": ["./data/*"], "@hooks/*": ["./hooks/*"], "@i18n/*": ["./i18n/*"], From 41ef31d1499d8af410191b9aec5d75a69ef20549 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Mon, 30 Sep 2024 12:30:27 +0200 Subject: [PATCH 20/20] Update paths --- screens/ConversationReadOnly.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/screens/ConversationReadOnly.tsx b/screens/ConversationReadOnly.tsx index c0f7b4164..83d89e6c1 100644 --- a/screens/ConversationReadOnly.tsx +++ b/screens/ConversationReadOnly.tsx @@ -1,4 +1,17 @@ +import ConverseChat from "@components/Chat/Chat"; +import { EmojiPicker } from "@containers/EmojiPicker"; +import { + currentAccount, + useChatStore, + useSettingsStore, +} from "@data/store/accountsStore"; +import { MediaPreview } from "@data/store/chatStore"; +import { useSelect } from "@data/store/storeHelpers"; import { backgroundColor, headerTitleStyle } from "@styles/colors"; +import { ConversationContext } from "@utils/conversation"; +import { setTopicToNavigateTo, topicToNavigateTo } from "@utils/navigation"; +import { TextInputWithValue } from "@utils/str"; +import { loadOlderMessages } from "@utils/xmtpRN/messages"; import React, { useCallback, useEffect, @@ -8,20 +21,6 @@ import React, { } from "react"; import { StyleSheet, useColorScheme, View } from "react-native"; -import ConverseChat from "../components/Chat/Chat"; -import { EmojiPicker } from "../containers/EmojiPicker"; -import { - currentAccount, - useChatStore, - useSettingsStore, -} from "../data/store/accountsStore"; -import { MediaPreview } from "../data/store/chatStore"; -import { useSelect } from "../data/store/storeHelpers"; -import { ConversationContext } from "../utils/conversation"; -import { setTopicToNavigateTo, topicToNavigateTo } from "../utils/navigation"; -import { TextInputWithValue } from "../utils/str"; -import { loadOlderMessages } from "../utils/xmtpRN/messages"; - export const ConversationReadOnly: React.FC<{ topic?: string; }> = ({ topic }) => {