From 680c1f991297da8a19de8afb5ea6e3b38451ca03 Mon Sep 17 00:00:00 2001 From: Saul Carlin Date: Tue, 25 Jun 2024 00:12:16 -0700 Subject: [PATCH] context menu wip --- components/Chat/Chat.tsx | 3 + components/Chat/Message/Message.tsx | 3 + components/Chat/Message/MessageActions.tsx | 210 ++++++++++++--------- components/Chat/Message/MessageTail.tsx | 64 +++++++ data/store/appStore.ts | 6 + package.json | 1 + yarn.lock | 5 + 7 files changed, 207 insertions(+), 85 deletions(-) create mode 100644 components/Chat/Message/MessageTail.tsx diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 798c42855..ceb5b4bef 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -371,13 +371,16 @@ const useStyles = () => { flex: 1, justifyContent: "flex-end", backgroundColor: backgroundColor(colorScheme), + overflow: "visible", }, chatContent: { backgroundColor: backgroundColor(colorScheme), flex: 1, + overflow: "visible", }, chat: { backgroundColor: backgroundColor(colorScheme), + overflow: "visible", }, inputBottomFiller: { position: "absolute", diff --git a/components/Chat/Message/Message.tsx b/components/Chat/Message/Message.tsx index 02fd6b143..8901451ad 100644 --- a/components/Chat/Message/Message.tsx +++ b/components/Chat/Message/Message.tsx @@ -469,16 +469,19 @@ const useStyles = () => { messageRow: { flexDirection: "row", flexWrap: "wrap", + overflow: "visible", }, messageSwipeable: { width: "100%", flexDirection: "row", paddingHorizontal: Platform.OS === "android" ? 10 : 20, + overflow: "visible", }, messageSwipeableChildren: { width: "100%", flexDirection: "row", flexWrap: "wrap", + overflow: "visible", }, statusContainer: { marginLeft: "auto", diff --git a/components/Chat/Message/MessageActions.tsx b/components/Chat/Message/MessageActions.tsx index ac3ad0557..5d53f03dd 100644 --- a/components/Chat/Message/MessageActions.tsx +++ b/components/Chat/Message/MessageActions.tsx @@ -8,8 +8,9 @@ import { StyleSheet, DimensionValue, } from "react-native"; +import ContextMenu from "react-native-context-menu-view"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; -import Reanimated, { +import { AnimatedStyle, Easing, ReduceMotion, @@ -18,14 +19,13 @@ import Reanimated, { useSharedValue, withTiming, } from "react-native-reanimated"; -import { SvgProps } from "react-native-svg"; -import _MessageTail from "../../../assets/message-tail.svg"; import { currentAccount, useCurrentAccount, useSettingsStore, } from "../../../data/store/accountsStore"; +import { useAppStore } from "../../../data/store/appStore"; import { XmtpConversation } from "../../../data/store/chatStore"; import { useFramesStore } from "../../../data/store/framesStore"; import { ReanimatedTouchableOpacity } from "../../../utils/animations"; @@ -52,31 +52,7 @@ import { consentToPeersOnProtocol } from "../../../utils/xmtpRN/conversations"; import EmojiPicker from "../../../vendor/rn-emoji-keyboard"; import { showActionSheetWithOptions } from "../../StateHandlers/ActionSheetStateHandler"; import { MessageToDisplay } from "./Message"; - -class MessageTailComponent extends React.Component { - render() { - return <_MessageTail {...this.props} />; - } -} - -const MessageTailAnimated = - Reanimated.createAnimatedComponent(MessageTailComponent); - -const MessageTail = (props: any) => { - return ( - - ); -}; +import MessageTail from "./MessageTail"; type Props = { children: React.ReactNode; @@ -338,6 +314,67 @@ export default function ChatMessageActions({ }; }, [highlightMessage]); + const contextMenuItems = useMemo(() => { + const items = []; + + if (canAddReaction) { + items.push({ title: "Add a reaction", systemIcon: "smiley" }); + } + items.push({ title: "Reply", systemIcon: "arrowshape.turn.up.left" }); + if (!isAttachment && !isTransaction) { + items.push({ title: "Copy message", systemIcon: "doc.on.doc" }); + if (!message.fromMe) { + items.push({ + title: "Report message", + systemIcon: "exclamationmark.triangle", + }); + } + } + + return items; + }, [canAddReaction, isAttachment, isTransaction, message.fromMe]); + + const handleContextMenuAction = useCallback( + (event: { nativeEvent: { index: number } }) => { + const { index } = event.nativeEvent; + switch (contextMenuItems[index].title) { + case "Add a reaction": + showReactionModal(); + break; + case "Reply": + triggerReplyToMessage(); + break; + case "Copy message": + if (message.content) { + Clipboard.setString(message.content); + } else if (message.contentFallback) { + Clipboard.setString(message.contentFallback); + } + break; + case "Report message": + Alert.alert( + "Report this message", + "This message will be forwarded to Converse. The contact will not be informed.", + [ + { text: "Cancel", style: "cancel" }, + { text: "Report", onPress: report }, + { text: "Report and block", onPress: reportAndBlock }, + ] + ); + break; + } + useAppStore.getState().setContextMenuShown(false); + }, + [ + contextMenuItems, + showReactionModal, + triggerReplyToMessage, + message, + report, + reportAndBlock, + ] + ); + // 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 @@ -346,75 +383,78 @@ export default function ChatMessageActions({ return ( <> - { - if (isAttachment) { - // Transfering attachment opening intent to component - converseEventEmitter.emit( - `openAttachmentForMessage-${message.id}` - ); - } - if (isTransaction) { - // Transfering event to component - converseEventEmitter.emit( - `showActionSheetForTxRef-${message.id}` - ); - } - }} - onLongPress={showMessageActionSheet} + useAppStore.getState().setContextMenuShown(false)} + previewBackgroundColor={initialBubbleBackgroundColor} + style={[{ width: "100%" }, { overflow: "visible" }]} > - {children} + { + if (isAttachment) { + // Transfering attachment opening intent to component + converseEventEmitter.emit( + `openAttachmentForMessage-${message.id}` + ); + } + if (isTransaction) { + // Transfering event to component + converseEventEmitter.emit( + `showActionSheetForTxRef-${message.id}` + ); + } + }} + onLongPress={() => useAppStore.getState().setContextMenuShown(true)} + > + {children} + {!message.hasNextMessageInSeries && !isFrame && !isAttachment && !isTransaction && (Platform.OS === "ios" || Platform.OS === "web") && ( )} - + {/* {children} */} diff --git a/components/Chat/Message/MessageTail.tsx b/components/Chat/Message/MessageTail.tsx new file mode 100644 index 000000000..6068be20c --- /dev/null +++ b/components/Chat/Message/MessageTail.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { ColorSchemeName, StyleSheet, ViewStyle } from "react-native"; +import Reanimated, { AnimatedStyleProp } from "react-native-reanimated"; +import { SvgProps } from "react-native-svg"; + +import _MessageTail from "../../../assets/message-tail.svg"; +import { + messageBubbleColor, + myMessageBubbleColor, +} from "../../../utils/colors"; + +class MessageTailComponent extends React.Component { + render() { + return <_MessageTail {...this.props} />; + } +} + +const MessageTailAnimated = + Reanimated.createAnimatedComponent(MessageTailComponent); + +interface MessageTailProps { + fromMe: boolean; + colorScheme: ColorSchemeName; + hideBackground: boolean; + style?: AnimatedStyleProp; +} + +const MessageTail: React.FC = ({ + fromMe, + colorScheme, + hideBackground, + style, +}) => { + return ( + + ); +}; + +const styles = StyleSheet.create({ + messageTail: { + position: "absolute", + left: -5, + bottom: 0, + width: 14, + height: 21, + zIndex: -1, + }, + messageTailMe: { + left: "auto", + right: -5, + transform: [{ scaleX: -1 }], + }, +}); + +export default MessageTail; diff --git a/data/store/appStore.ts b/data/store/appStore.ts index 663024c99..29b2f58fa 100644 --- a/data/store/appStore.ts +++ b/data/store/appStore.ts @@ -45,6 +45,9 @@ type AppStoreType = { actionSheetShown: boolean; setActionSheetShown: (s: boolean) => void; + + contextMenuShown: boolean; + setContextMenuShown: (s: boolean) => void; }; export const useAppStore = create()( @@ -84,6 +87,9 @@ export const useAppStore = create()( actionSheetShown: false, setActionSheetShown: (s: boolean) => set(() => ({ actionSheetShown: s })), + + contextMenuShown: false, + setContextMenuShown: (s: boolean) => set(() => ({ contextMenuShown: s })), }), { name: "store-app", diff --git a/package.json b/package.json index f4d82a721..d2307c208 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "react-native-avoid-softinput": "^4.0.1", "react-native-background-color": "^0.0.8", "react-native-bootsplash": "^4.5.3", + "react-native-context-menu-view": "^1.16.0", "react-native-device-info": "^10.9.0", "react-native-fetch-api": "^3.0.0", "react-native-fs": "^2.20.0", diff --git a/yarn.lock b/yarn.lock index 16eb629e2..6fd905eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23102,6 +23102,11 @@ react-native-camera@^4.2.1: dependencies: prop-types "^15.6.2" +react-native-context-menu-view@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/react-native-context-menu-view/-/react-native-context-menu-view-1.16.0.tgz#50e376ce30bc9b9aa07c0a5b66f1280befcf98f8" + integrity sha512-zqeOAizM7MVV9o6h/quS0REQikBq3J4BkIRLFygY6RiCjr6rwuzSGkif7JRCHpAQQumSKlLqYl4N2h3AdoIHVg== + react-native-country-picker-modal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-native-country-picker-modal/-/react-native-country-picker-modal-2.0.0.tgz#005421303349a81fedf5975e465405bb4b7312d5"