/* eslint-disable react-native/no-inline-styles */ import * as React from 'react'; import { View, Animated, Alert, StyleSheet, Text, FlatList, SafeAreaView, type ListRenderItem, KeyboardAvoidingView, TextInput, TouchableOpacity } from 'react-native'; import { Colors, Helpers } from 'react-native-ios-utilities'; import { type AuxiliaryPreviewConfig, ContextMenuView, type MenuElementConfig, type AuxiliaryPreviewConfigBackwardsCompatible } from 'react-native-ios-context-menu'; const SHOULD_USE_NEW_CONFIG = true; const MESSAGE_BUBBLE_BORDER_RADIUS = 10; // Repro for issue: // https://github.com/dominicstop/react-native-ios-context-menu/issues/47 const REACTIONS_DICT = { like : "👍", laugh : "😂", crying: "😭", sad : "😔", heart : "💖", love : "🥰", }; type ReactionKeys = keyof typeof REACTIONS_DICT; const REACTIONS_KEYS = Object.keys(REACTIONS_DICT) as ReactionKeys[]; type Message = { id: number; messageText: string; isSender: boolean; }; let counterID = 0; const MESSAGES_INITIAL: Message[] = (() => { const messages = [ { id: counterID++, messageText: 'Hello World', isSender: false, }, { id: counterID++, messageText: 'Lorum ipsum sit amit dolor aspicing', isSender: true, }, { id: counterID++, messageText: 'Hello, how are you?', isSender: false, }, { id: counterID++, messageText: 'lights, camera, action, pose!', isSender: false, }, { id: counterID++, messageText: 'stan loona, yass kweeeeen', isSender: true, }, { id: counterID++, messageText: 'its giving... taste??? we love to see it', isSender: true, }, { id: counterID++, messageText: 'true lmao', isSender: false, }, { id: counterID++, messageText: "i'm also a indie h0e + bedroom pop btch btw", isSender: false, }, { id: counterID++, messageText: "asdfghjklfghjkl omg same!!", isSender: true, }, { id: counterID++, messageText: "have you listened to emails i can't send tho", isSender: true, }, { id: counterID++, messageText: "its giving olivia rodrigo but w/ a midlife crisis and a failing marriage", isSender: true, }, { id: counterID++, messageText: "love that for her tho", isSender: true, }, { id: counterID++, messageText: "okay but its a bop tho", isSender: false, }, { id: counterID++, messageText: "ik her rent was due and the crops were dying, " + "she really gave it her all lmao", isSender: false, }, { id: counterID++, messageText: "please 😭", isSender: true, }, { id: counterID++, messageText: "asdfghjkskdhaslkfjkl", isSender: true, }, { id: counterID++, messageText: "am i wrong tho", isSender: false, }, { id: counterID++, messageText: "anyways", isSender: false, }, { id: counterID++, messageText: "killer hybs is also pretty good", isSender: false, }, { id: counterID++, messageText: "love the intro", isSender: true, }, { id: counterID++, messageText: "forever a popstan girlie, but bedroom/indie pop hits differenty", isSender: true, }, { id: counterID++, messageText: "how very i'm not like other girls of u", isSender: false, }, { id: counterID++, messageText: "but tbh same", isSender: false, }, { id: counterID++, messageText: "2016-2017 era was basically niki, rex, mxmtoon, and mitski", isSender: false, }, { id: counterID++, messageText: "naurr the fact that they're not considered indie anymore tho", isSender: true, }, { id: counterID++, messageText: "well deserve popularity tho ✨", isSender: true, }, { id: counterID++, messageText: "sdafdghjksdsajlkjkl true", isSender: false, }, { id: counterID++, messageText: "is boy pablo and phum still considered indie?", isSender: false, }, { id: counterID++, messageText: "idk they have alot of streams, but did they chart in the top 100?", isSender: true, }, { id: counterID++, messageText: "asdgfhjkldsfdjkl too lazy to look it up", isSender: true, }, { id: counterID++, messageText: "have u watched the barbie trailer??", isSender: false, }, { id: counterID++, messageText: "asdgfhjkldsfl yes and i'm so excited like", isSender: true, }, { id: counterID++, messageText: "i just know its going to be iconic", isSender: true, }, { id: counterID++, messageText: "i have some thoughts about the casting for the ken dolls tho", isSender: true, }, { id: counterID++, messageText: "same, but after watching the trailer i was convinced!!", isSender: false, }, { id: counterID++, messageText: "pinkpantheress is in the soundtrack!! and it's giving soft bbgirl energy as per usual", isSender: false, }, { id: counterID++, messageText: "did u listen to the soundtrack yet??", isSender: false, }, { id: counterID++, messageText: "i have and ur i have very unpopular opinion", isSender: false, }, { id: counterID++, messageText: "bestie u and ur bad takes adkhakfhdlhfa", isSender: true, }, { id: counterID++, messageText: "but ok go on", isSender: true, }, { id: counterID++, messageText: `item count: ${counterID}`, isSender: counterID % 2 === 0, }, ]; for (let index = 0; index < 100; index++) { messages.push({ id: counterID++, messageText: `item count: ${counterID}`, isSender: counterID % 2 === 0, }); }; return messages; })(); const MENU_ITEM_KEYS = { reply: 'reply', undo_send: 'undo_send', remove_reactions: 'remove_reactions', delete: 'delete', }; type MenuItemKeys = keyof typeof MENU_ITEM_KEYS; const MENU_CONFIGS: Record = { reply: { type: 'action', actionKey: MENU_ITEM_KEYS.reply, actionTitle: 'Reply', icon: { type: 'IMAGE_SYSTEM', imageValue: { systemName: 'arrowshape.turn.up.left.2', }, }, }, undo_send: { actionKey: MENU_ITEM_KEYS.undo_send, actionTitle: 'Undo Send', actionSubtitle: "gurl they might've seen it already tho...", icon: { type: 'IMAGE_SYSTEM', imageValue: { systemName: 'arrow.uturn.left', }, } }, remove_reactions: { actionKey: MENU_ITEM_KEYS.remove_reactions, actionTitle: 'Remove Reactions', icon: { type: 'IMAGE_SYSTEM', imageValue: { systemName: 'heart.slash', }, } }, delete: { actionKey: MENU_ITEM_KEYS.delete, actionTitle: 'Delete Message', menuAttributes: ['destructive'], icon: { type: 'IMAGE_SYSTEM', imageValue: { systemName: 'trash', }, } }, }; const MessageBubble = (props: { message: Message; reactionMap: Record; onPressReaction: (reactionKey: ReactionKeys) => void; setReaction: (reactionKey: ReactionKeys) => void; onPressDeleteMessage: () => void; }) => { const currentReaction = props.reactionMap[props.message.id]; const [currentVisibleReaction, setCurrentVisibleReaction] = React.useState(null); const hasReaction = (currentReaction != null); const menuRef = React.useRef(); const animatedReactionOpacity = React.useRef(new Animated.Value(0)).current; const animatedMessageContainerMarginTop = React.useRef(new Animated.Value(0)).current; const animatedMessageContainerOpacity = React.useRef(new Animated.Value(1)).current; const startFadeIn = () => { return new Promise(resolve => { Animated.timing(animatedReactionOpacity, { toValue: 1, duration: 300, useNativeDriver: true, }).start(resolve); }); }; const startFadeOut = () => { return new Promise(resolve => { Animated.timing(animatedReactionOpacity, { toValue: 0, duration: 300, useNativeDriver: true, }).start(resolve); }); }; const startCollapse = () => { return Promise.all([ new Promise(resolve => { Animated.timing(animatedMessageContainerMarginTop, { toValue: -100, duration: 500, useNativeDriver: false, }).start(resolve); }), new Promise(resolve => { Animated.timing(animatedMessageContainerOpacity, { toValue: 0, duration: 250, useNativeDriver: false, }).start(resolve); }) ]); }; const reactionIndicator = ( {REACTIONS_DICT[currentVisibleReaction]} ); const auxPreviewConfig: AuxiliaryPreviewConfigBackwardsCompatible = (SHOULD_USE_NEW_CONFIG ? { verticalAnchorPosition: 'automatic', horizontalAlignment: (props.message.isSender ? 'targetTrailing' : 'targetLeading' ), transitionConfigEntrance: { mode: 'syncedToMenuEntranceTransition', shouldAnimateSize: true, }, transitionExitPreset: { mode: 'zoomAndSlide', }, } : { alignmentHorizontal: (props.message.isSender ? 'previewTrailing' : 'previewLeading' ), transitionEntranceDelay: 'RECOMMENDED', transitionConfigEntrance: { transition: 'zoomAndSlide', zoomOffset: 0.9, slideOffset: 15, duration: 0.3, options: ['curveEaseInOut'], }, }); return ( {(hasReaction && props.message.isSender) && reactionIndicator} menuRef.current = ref} style={[ styles.messageBubbleContainer ]} lazyPreview={true} menuConfig={{ menuTitle: '', menuItems: [ MENU_CONFIGS.reply, MENU_CONFIGS.undo_send, hasReaction && MENU_CONFIGS.remove_reactions, MENU_CONFIGS.delete, ], }} previewConfig={{ borderRadius: MESSAGE_BUBBLE_BORDER_RADIUS, }} auxiliaryPreviewConfig={auxPreviewConfig} renderAuxiliaryPreview={() => ( {REACTIONS_KEYS.map((reactionKey, index) => ( { await props.onPressReaction(reactionKey); await Helpers.timeout((currentReaction == null)? 550 : 200); menuRef.current.dismissMenu(); }} > {REACTIONS_DICT[reactionKey]} ))} )} onPressMenuItem={async ({nativeEvent}) => { switch(nativeEvent.actionKey){ case MENU_ITEM_KEYS.remove_reactions: await startFadeOut(); props.setReaction(null); setCurrentVisibleReaction(null); break; case MENU_ITEM_KEYS.delete: await startCollapse(); props.onPressDeleteMessage(); break; case MENU_ITEM_KEYS.undo_send: Alert.alert( 'naurrr', `the tea cannot be unspilled babes, sorry xoxo` ); break; default: Alert.alert( 'onPressMenuItem Event', `actionKey: ${nativeEvent.actionKey} - actionTitle: ${nativeEvent.actionTitle}` ); break; }; }} onMenuDidHide={async () => { const didChangeReaction = (currentReaction !== currentVisibleReaction); // guard if(!didChangeReaction) return; (currentVisibleReaction != null) && await startFadeOut(); setCurrentVisibleReaction(currentReaction); (currentReaction != null) && await startFadeIn(); }} > {props.message.messageText} {(hasReaction && !props.message.isSender) && reactionIndicator} ); }; export const Test03Screen = () => { let messageText = ""; const [messages, setMessages] = React.useState(MESSAGES_INITIAL); const messageBoxInputRef = React.useRef(null); const [reactionMap, setReactionMap] = React.useState>({}); const renderItem: ListRenderItem = ({item}) => ( { setReactionMap((prev) => { return ({ ...prev, [item.id]: reactionKey, }); }); }} onPressReaction={(reactionKey) => { setReactionMap((prev) => { const prevReaction = prev[reactionKey]; const didHavePrevReaction = prevReaction != null; return ({ ...prev, [item.id]: didHavePrevReaction? null : reactionKey, }); }); }} onPressDeleteMessage={() => { setMessages(prev => prev.filter(message => item.id !== message.id) ); }} /> ); return ( `${item.id}`} scrollIndicatorInsets={{ bottom: 45 }} /> { messageText = text; }} /> { setMessages(prev => [...prev, { id: counterID++, messageText, isSender: true, }]); messageBoxInputRef.current?.clear(); }} > Send ); }; const styles = StyleSheet.create({ rootContainer: { flex: 1, backgroundColor: 'white', }, list: { flex: 1, }, bottomBarWrapper: { position: 'absolute', width: '100%', bottom: 0, backgroundColor: 'white', borderTopWidth: 0.75, borderTopColor: 'rgb(190,190,190)', shadowColor: 'black', shadowOpacity: 0.1, shadowRadius: 10, shadowOffset: { height: -5, width: 0, }, }, bottomBarContainer: { flexDirection: 'row', backgroundColor: 'white', paddingHorizontal: 15, paddingVertical: 12, // TODO: temp fix, replace with actual safe area bottom height paddingBottom: 30, }, listContentContainer: { paddingTop: 10, paddingHorizontal: 10, paddingBottom: 90, }, messageMenuContainer: { flexDirection: 'row', marginBottom: 10, }, messageBubbleContainer: { maxWidth: '90%', }, messageBubbleContent: { flex: 1, paddingHorizontal: 12, paddingVertical: 8, borderRadius: MESSAGE_BUBBLE_BORDER_RADIUS, }, messageTextLabel: { }, messageBoxInput: { flex: 1, marginRight: 10, }, sendButton: { alignItems: 'center', justifyContent: 'center', }, sendButtonLabel: { color: Colors.PURPLE.A700, fontWeight: '600', }, auxRootContainer: { flexDirection: 'row', backgroundColor: 'white', borderRadius: 10, paddingHorizontal: 15, paddingVertical: 8, minWidth: 100, }, auxPreviewreactionEmojiButton: { }, auxPreviewReactionEmojiButtonSelected: { backgroundColor: Colors.BLUE[100], borderRadius: 7, }, auxPreviewLabelReactionEmoji: { fontSize: 30, }, messageReactionContainer: { alignItems: 'center', justifyContent: 'center', }, messageReactionLabel: { fontSize: 16, }, });