diff --git a/native/chat/multimedia-message-tooltip-button.react.js b/native/chat/multimedia-message-tooltip-button.react.js index b303feba2..eafc0c280 100644 --- a/native/chat/multimedia-message-tooltip-button.react.js +++ b/native/chat/multimedia-message-tooltip-button.react.js @@ -1,186 +1,187 @@ // @flow import * as React from 'react'; import Animated from 'react-native-reanimated'; import { localIDPrefix } from 'lib/shared/message-utils.js'; import { useViewerAlreadySelectedMessageReactions, useCanCreateReactionFromMessage, } from 'lib/shared/reaction-utils.js'; import { TooltipInlineEngagement } from './inline-engagement.react.js'; import { InnerMultimediaMessage } from './inner-multimedia-message.react.js'; import { MessageHeader } from './message-header.react.js'; import MessageTooltipButtonAvatar from './message-tooltip-button-avatar.react.js'; import { useSendReaction } from './reaction-message-utils.js'; import ReactionSelectionPopover from './reaction-selection-popover.react.js'; import SidebarInputBarHeightMeasurer from './sidebar-input-bar-height-measurer.react.js'; import { useAnimatedMessageTooltipButton } from './utils.js'; import EmojiKeyboard from '../components/emoji-keyboard.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useTooltipActions } from '../tooltip/tooltip-hooks.js'; import type { TooltipRoute } from '../tooltip/tooltip.react.js'; /* eslint-disable import/no-named-as-default-member */ const { Node, Extrapolate, interpolateNode } = Animated; /* eslint-enable import/no-named-as-default-member */ function noop() {} type Props = { +navigation: AppNavigationProp<'MultimediaMessageTooltipModal'>, +route: TooltipRoute<'MultimediaMessageTooltipModal'>, +progress: Node, +isOpeningSidebar: boolean, }; function MultimediaMessageTooltipButton(props: Props): React.Node { const { navigation, route, progress, isOpeningSidebar } = props; const windowWidth = useSelector(state => state.dimensions.width); const [sidebarInputBarHeight, setSidebarInputBarHeight] = React.useState(null); const onInputBarMeasured = React.useCallback((height: number) => { setSidebarInputBarHeight(height); }, []); const { item, verticalBounds, initialCoordinates } = route.params; const { style: messageContainerStyle } = useAnimatedMessageTooltipButton({ sourceMessage: item, initialCoordinates, messageListVerticalBounds: verticalBounds, progress, targetInputBarHeight: sidebarInputBarHeight, }); const headerStyle = React.useMemo(() => { const bottom = initialCoordinates.height; const opacity = interpolateNode(progress, { inputRange: [0, 0.05], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); return { opacity, position: 'absolute', left: -initialCoordinates.x, width: windowWidth, bottom, }; }, [initialCoordinates.height, initialCoordinates.x, progress, windowWidth]); const inlineEngagement = React.useMemo(() => { if (!item.threadCreatedFromMessage) { return null; } return ( ); }, [initialCoordinates, isOpeningSidebar, item, progress, windowWidth]); const innerMultimediaMessage = React.useMemo( () => ( ), [item, navigation.goBackOnce, verticalBounds], ); const { messageInfo, threadInfo, reactions } = item; const nextLocalID = useSelector(state => state.nextLocalID); const localID = `${localIDPrefix}${nextLocalID}`; const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, messageInfo, ); const sendReaction = useSendReaction( messageInfo.id, localID, threadInfo.id, reactions, ); const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); const openEmojiPicker = React.useCallback(() => { setEmojiPickerOpen(true); }, []); const reactionSelectionPopover = React.useMemo(() => { if (!canCreateReactionFromMessage) { return null; } return ( ); }, [ navigation, route, openEmojiPicker, canCreateReactionFromMessage, sendReaction, ]); const tooltipRouteKey = route.key; const { dismissTooltip } = useTooltipActions(navigation, tooltipRouteKey); const onEmojiSelected = React.useCallback( emoji => { sendReaction(emoji.emoji); dismissTooltip(); }, [sendReaction, dismissTooltip], ); const alreadySelectedEmojis = useViewerAlreadySelectedMessageReactions(reactions); return ( <> {reactionSelectionPopover} {innerMultimediaMessage} {inlineEngagement} ); } export default MultimediaMessageTooltipButton; diff --git a/native/chat/robotext-message-tooltip-button.react.js b/native/chat/robotext-message-tooltip-button.react.js index 3e63c1aac..541ec5bec 100644 --- a/native/chat/robotext-message-tooltip-button.react.js +++ b/native/chat/robotext-message-tooltip-button.react.js @@ -1,168 +1,169 @@ // @flow import * as React from 'react'; import Animated from 'react-native-reanimated'; import { localIDPrefix } from 'lib/shared/message-utils.js'; import { useViewerAlreadySelectedMessageReactions, useCanCreateReactionFromMessage, } from 'lib/shared/reaction-utils.js'; import { TooltipInlineEngagement } from './inline-engagement.react.js'; import { InnerRobotextMessage } from './inner-robotext-message.react.js'; import { useSendReaction } from './reaction-message-utils.js'; import ReactionSelectionPopover from './reaction-selection-popover.react.js'; import SidebarInputBarHeightMeasurer from './sidebar-input-bar-height-measurer.react.js'; import { Timestamp } from './timestamp.react.js'; import { useAnimatedMessageTooltipButton } from './utils.js'; import EmojiKeyboard from '../components/emoji-keyboard.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useTooltipActions } from '../tooltip/tooltip-hooks.js'; import type { TooltipRoute } from '../tooltip/tooltip.react.js'; /* eslint-disable import/no-named-as-default-member */ const { Node, interpolateNode, Extrapolate } = Animated; /* eslint-enable import/no-named-as-default-member */ type Props = { +navigation: AppNavigationProp<'RobotextMessageTooltipModal'>, +route: TooltipRoute<'RobotextMessageTooltipModal'>, +progress: Node, +isOpeningSidebar: boolean, }; function RobotextMessageTooltipButton(props: Props): React.Node { const { navigation, route, progress, isOpeningSidebar } = props; const windowWidth = useSelector(state => state.dimensions.width); const [sidebarInputBarHeight, setSidebarInputBarHeight] = React.useState(null); const onInputBarMeasured = React.useCallback((height: number) => { setSidebarInputBarHeight(height); }, []); const { item, verticalBounds, initialCoordinates } = route.params; const { style: messageContainerStyle } = useAnimatedMessageTooltipButton({ sourceMessage: item, initialCoordinates, messageListVerticalBounds: verticalBounds, progress, targetInputBarHeight: sidebarInputBarHeight, }); const headerStyle = React.useMemo(() => { const bottom = initialCoordinates.height; const opacity = interpolateNode(progress, { inputRange: [0, 0.05], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); return { opacity, position: 'absolute', left: -initialCoordinates.x, width: windowWidth, bottom, }; }, [initialCoordinates.height, initialCoordinates.x, progress, windowWidth]); const inlineEngagement = React.useMemo(() => { if (!item.threadCreatedFromMessage) { return null; } return ( ); }, [initialCoordinates, isOpeningSidebar, item, progress, windowWidth]); const { messageInfo, threadInfo, reactions } = item; const nextLocalID = useSelector(state => state.nextLocalID); const localID = `${localIDPrefix}${nextLocalID}`; const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, messageInfo, ); const sendReaction = useSendReaction( messageInfo.id, localID, threadInfo.id, reactions, ); const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); const openEmojiPicker = React.useCallback(() => { setEmojiPickerOpen(true); }, []); const reactionSelectionPopover = React.useMemo(() => { if (!canCreateReactionFromMessage) { return null; } return ( ); }, [ navigation, route, openEmojiPicker, canCreateReactionFromMessage, sendReaction, ]); const tooltipRouteKey = route.key; const { dismissTooltip } = useTooltipActions(navigation, tooltipRouteKey); const onEmojiSelected = React.useCallback( emoji => { sendReaction(emoji.emoji); dismissTooltip(); }, [sendReaction, dismissTooltip], ); const alreadySelectedEmojis = useViewerAlreadySelectedMessageReactions(reactions); return ( <> {reactionSelectionPopover} {inlineEngagement} ); } export default RobotextMessageTooltipButton; diff --git a/native/chat/text-message-tooltip-button.react.js b/native/chat/text-message-tooltip-button.react.js index 37cbfd496..dd8e43555 100644 --- a/native/chat/text-message-tooltip-button.react.js +++ b/native/chat/text-message-tooltip-button.react.js @@ -1,192 +1,193 @@ // @flow import * as React from 'react'; import Animated from 'react-native-reanimated'; import { localIDPrefix } from 'lib/shared/message-utils.js'; import { useViewerAlreadySelectedMessageReactions, useCanCreateReactionFromMessage, } from 'lib/shared/reaction-utils.js'; import { TooltipInlineEngagement } from './inline-engagement.react.js'; import { InnerTextMessage } from './inner-text-message.react.js'; import { MessageHeader } from './message-header.react.js'; import { MessageListContextProvider } from './message-list-types.js'; import { MessagePressResponderContext } from './message-press-responder-context.js'; import MessageTooltipButtonAvatar from './message-tooltip-button-avatar.react.js'; import { useSendReaction } from './reaction-message-utils.js'; import ReactionSelectionPopover from './reaction-selection-popover.react.js'; import SidebarInputBarHeightMeasurer from './sidebar-input-bar-height-measurer.react.js'; import { useAnimatedMessageTooltipButton } from './utils.js'; import EmojiKeyboard from '../components/emoji-keyboard.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useTooltipActions } from '../tooltip/tooltip-hooks.js'; import type { TooltipRoute } from '../tooltip/tooltip.react.js'; /* eslint-disable import/no-named-as-default-member */ const { Node, interpolateNode, Extrapolate } = Animated; /* eslint-enable import/no-named-as-default-member */ type Props = { +navigation: AppNavigationProp<'TextMessageTooltipModal'>, +route: TooltipRoute<'TextMessageTooltipModal'>, +progress: Node, +isOpeningSidebar: boolean, }; function TextMessageTooltipButton(props: Props): React.Node { const { navigation, route, progress, isOpeningSidebar } = props; const windowWidth = useSelector(state => state.dimensions.width); const [sidebarInputBarHeight, setSidebarInputBarHeight] = React.useState(null); const onInputBarMeasured = React.useCallback((height: number) => { setSidebarInputBarHeight(height); }, []); const { item, verticalBounds, initialCoordinates } = route.params; const { style: messageContainerStyle, threadColorOverride, isThreadColorDarkOverride, } = useAnimatedMessageTooltipButton({ sourceMessage: item, initialCoordinates, messageListVerticalBounds: verticalBounds, progress, targetInputBarHeight: sidebarInputBarHeight, }); const headerStyle = React.useMemo(() => { const bottom = initialCoordinates.height; const opacity = interpolateNode(progress, { inputRange: [0, 0.05], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); return { opacity, position: 'absolute', left: -initialCoordinates.x, width: windowWidth, bottom, }; }, [initialCoordinates.height, initialCoordinates.x, progress, windowWidth]); const messagePressResponderContext = React.useMemo( () => ({ onPressMessage: navigation.goBackOnce, }), [navigation.goBackOnce], ); const inlineEngagement = React.useMemo(() => { if (!item.threadCreatedFromMessage) { return null; } return ( ); }, [initialCoordinates, isOpeningSidebar, item, progress, windowWidth]); const { messageInfo, threadInfo, reactions } = item; const nextLocalID = useSelector(state => state.nextLocalID); const localID = `${localIDPrefix}${nextLocalID}`; const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, messageInfo, ); const sendReaction = useSendReaction( messageInfo.id, localID, threadInfo.id, reactions, ); const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); const openEmojiPicker = React.useCallback(() => { setEmojiPickerOpen(true); }, []); const reactionSelectionPopover = React.useMemo(() => { if (!canCreateReactionFromMessage) { return null; } return ( ); }, [ navigation, route, openEmojiPicker, canCreateReactionFromMessage, sendReaction, ]); const tooltipRouteKey = route.key; const { dismissTooltip } = useTooltipActions(navigation, tooltipRouteKey); const onEmojiSelected = React.useCallback( emoji => { sendReaction(emoji.emoji); dismissTooltip(); }, [sendReaction, dismissTooltip], ); const alreadySelectedEmojis = useViewerAlreadySelectedMessageReactions(reactions); return ( {reactionSelectionPopover} {inlineEngagement} ); } export default TextMessageTooltipButton; diff --git a/native/components/emoji-keyboard.react.js b/native/components/emoji-keyboard.react.js index 71758960c..d1466260b 100644 --- a/native/components/emoji-keyboard.react.js +++ b/native/components/emoji-keyboard.react.js @@ -1,152 +1,152 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import _flatMap from 'lodash/fp/flatMap.js'; import _flow from 'lodash/fp/flow.js'; import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; import EmojiPicker, { useRecentPicksPersistence } from 'rn-emoji-keyboard'; import emojisData from 'rn-emoji-keyboard/src/assets/emojis.json'; import { useColors } from '../themes/colors.js'; const STORAGE_KEY = 'EMOJI_KEYBOARD_RECENT'; const categoryOrder = [ 'recently_used', 'smileys_emotion', 'people_body', 'animals_nature', 'food_drink', 'travel_places', 'activities', 'objects', 'symbols', 'flags', 'search', ]; const initializationCallback = async () => { const recentlyUsedEmojis = await AsyncStorage.getItem(STORAGE_KEY); return JSON.parse(recentlyUsedEmojis ?? '[]'); }; const onStateChangeCallback = nextRecentlyUsedEmojis => AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(nextRecentlyUsedEmojis)); const useRecentPicksPersistenceArgs = { initialization: initializationCallback, onStateChange: onStateChangeCallback, }; const keyedEmojiData = _flow(_flatMap('data'), _keyBy('emoji'))(emojisData); export type EmojiSelection = { +emoji: string, +name: string, +slug: string, +unicode_version: string, +toneEnabled: string, +alreadySelected?: boolean, }; type Props = { +onEmojiSelected: (emoji: EmojiSelection) => mixed, +emojiKeyboardOpen: boolean, +onEmojiKeyboardClose: () => mixed, +selectMultipleEmojis?: boolean, +alreadySelectedEmojis: $ReadOnlyArray, }; function EmojiKeyboard(props: Props): React.Node { const { onEmojiSelected, emojiKeyboardOpen, onEmojiKeyboardClose, - selectMultipleEmojis, + selectMultipleEmojis = false, alreadySelectedEmojis, } = props; const [currentlySelected, setCurrentlySelected] = React.useState< $ReadOnlyArray, >(() => alreadySelectedEmojis.map(emoji => keyedEmojiData[emoji]?.name)); const handleOnEmojiSelected = React.useCallback( (emoji: EmojiSelection) => { if (!selectMultipleEmojis) { setCurrentlySelected([emoji.name]); } else if (emoji.alreadySelected) { setCurrentlySelected(prev => prev.filter(emojiName => emojiName !== emoji.name), ); } else { setCurrentlySelected(prev => [...prev, emoji.name]); } onEmojiSelected(emoji); }, [onEmojiSelected, setCurrentlySelected, selectMultipleEmojis], ); useRecentPicksPersistence(useRecentPicksPersistenceArgs); const colors = useColors(); const theme = React.useMemo( () => ({ knob: colors.emojiKeyboardKnob, container: colors.modalForeground, header: colors.modalBackgroundLabel, category: { icon: colors.emojiKeyboardCategoryIcon, iconActive: colors.panelForegroundLabel, container: colors.panelInputBackground, containerActive: colors.emojiKeyboardContainerActive, }, search: { text: colors.panelForegroundLabel, placeholder: colors.panelInputSecondaryForeground, icon: colors.panelInputSecondaryForeground, background: colors.panelInputBackground, }, emoji: { selected: colors.panelSecondaryForegroundBorder, }, }), [ colors.emojiKeyboardCategoryIcon, colors.emojiKeyboardKnob, colors.emojiKeyboardContainerActive, colors.modalBackgroundLabel, colors.modalForeground, colors.panelForegroundLabel, colors.panelInputBackground, colors.panelInputSecondaryForeground, colors.panelSecondaryForegroundBorder, ], ); return ( ); } const emojiKeyboardStyles = { searchBar: { container: { borderWidth: 0, borderRadius: 8, }, }, }; export default EmojiKeyboard;