From a7b0242cc8aaf09273abe20903e88e59a4acd1a7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 20 May 2024 18:56:44 -0500 Subject: [PATCH] =?UTF-8?q?[=F0=9F=90=B4]=20Empty=20chat=20prompt=20(#4132?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add empty chat pill * Tweak padding * move to `components`, place inside `KeyboardStickyView` * cleanup unused vars * add a new animation type * (unrelated) add haptic to long press * adjust shrink and pop --------- Co-authored-by: Hailey --- src/components/dms/ChatEmptyPill.tsx | 98 +++++++++++++++++++ src/lib/custom-animations/ShrinkAndPop.ts | 27 +++++ .../Messages/Conversation/MessagesList.tsx | 23 +++-- src/screens/Messages/List/ChatListItem.tsx | 7 +- 4 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 src/components/dms/ChatEmptyPill.tsx create mode 100644 src/lib/custom-animations/ShrinkAndPop.ts diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx new file mode 100644 index 00000000..a6c4906a --- /dev/null +++ b/src/components/dms/ChatEmptyPill.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import {Pressable, View} from 'react-native' +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {ScaleAndFadeIn} from 'lib/custom-animations/ScaleAndFade' +import {ShrinkAndPop} from 'lib/custom-animations/ShrinkAndPop' +import {useHaptics} from 'lib/haptics' +import {isWeb} from 'platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + +let lastIndex = 0 + +export function ChatEmptyPill() { + const t = useTheme() + const {_} = useLingui() + const playHaptic = useHaptics() + const [promptIndex, setPromptIndex] = React.useState(lastIndex) + + const scale = useSharedValue(1) + + const prompts = React.useMemo(() => { + return [ + _(msg`Say hello!`), + _(msg`Share your favorite feed!`), + _(msg`Tell a joke!`), + _(msg`Share a fun fact!`), + _(msg`Share a cool story!`), + _(msg`Send a neat website!`), + _(msg`Clip 🐴 clop 🐴`), + ] + }, [_]) + + const onPressIn = React.useCallback(() => { + if (isWeb) return + scale.value = withTiming(1.075, {duration: 100}) + }, [scale]) + + const onPressOut = React.useCallback(() => { + if (isWeb) return + scale.value = withTiming(1, {duration: 100}) + }, [scale]) + + const onPress = React.useCallback(() => { + runOnJS(playHaptic)() + let randomPromptIndex = Math.floor(Math.random() * prompts.length) + while (randomPromptIndex === lastIndex) { + randomPromptIndex = Math.floor(Math.random() * prompts.length) + } + setPromptIndex(randomPromptIndex) + lastIndex = randomPromptIndex + }, [playHaptic, prompts.length]) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}], + })) + + return ( + + + + {prompts[promptIndex]} + + + + ) +} diff --git a/src/lib/custom-animations/ShrinkAndPop.ts b/src/lib/custom-animations/ShrinkAndPop.ts new file mode 100644 index 00000000..ea2386c1 --- /dev/null +++ b/src/lib/custom-animations/ShrinkAndPop.ts @@ -0,0 +1,27 @@ +import {withDelay, withSequence, withTiming} from 'react-native-reanimated' + +export function ShrinkAndPop() { + 'worklet' + + const animations = { + opacity: withDelay(125, withTiming(0, {duration: 125})), + transform: [ + { + scale: withSequence( + withTiming(0.7, {duration: 75}), + withTiming(1.1, {duration: 150}), + ), + }, + ], + } + + const initialValues = { + opacity: 1, + transform: [{scale: 1}], + } + + return { + animations, + initialValues, + } +} diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index e3035450..167cc72b 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -17,7 +17,7 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api' import {shortenLinks} from '#/lib/strings/rich-text-manip' import {isIOS, isNative} from '#/platform/detection' -import {useConvoActive} from '#/state/messages/convo' +import {isConvoActive, useConvoActive} from '#/state/messages/convo' import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' import {useAgent} from '#/state/session' import {ScrollProvider} from 'lib/ScrollContext' @@ -26,6 +26,7 @@ import {List} from 'view/com/util/List' import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' +import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' @@ -340,18 +341,20 @@ export function MessagesList({ /> - {!blocked ? ( - <> - {convoState.status === ConvoStatus.Disabled ? ( - - ) : ( - - )} - - ) : ( + {convoState.status === ConvoStatus.Disabled ? ( + + ) : blocked ? ( footer + ) : ( + <> + {isConvoActive(convoState) && convoState.items.length === 0 && ( + + )} + + )} + {newMessagesPill.show && } ) diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index 682a2197..ce0c7eee 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -13,6 +13,7 @@ import {isNative} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' +import {useHaptics} from 'lib/haptics' import {logEvent} from 'lib/statsig/statsig' import {sanitizeDisplayName} from 'lib/strings/display-names' import {TimeElapsed} from '#/view/com/util/TimeElapsed' @@ -70,6 +71,7 @@ function ChatListItemReady({ () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) + const playHaptic = useHaptics() const blockInfo = React.useMemo(() => { const modui = moderation.ui('profileView') @@ -134,8 +136,9 @@ function ChatListItemReady({ ) const onLongPress = useCallback(() => { + playHaptic() menuControl.open() - }, [menuControl]) + }, [playHaptic, menuControl]) return (