Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] Empty chat prompt (#4132)

* 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 <me@haileyok.com>

authored by

Eric Bailey
Hailey
and committed by
GitHub
a7b0242c 6dde4875

+141 -10
+98
src/components/dms/ChatEmptyPill.tsx
··· 1 + import React from 'react' 2 + import {Pressable, View} from 'react-native' 3 + import Animated, { 4 + runOnJS, 5 + useAnimatedStyle, 6 + useSharedValue, 7 + withTiming, 8 + } from 'react-native-reanimated' 9 + import {msg} from '@lingui/macro' 10 + import {useLingui} from '@lingui/react' 11 + 12 + import {ScaleAndFadeIn} from 'lib/custom-animations/ScaleAndFade' 13 + import {ShrinkAndPop} from 'lib/custom-animations/ShrinkAndPop' 14 + import {useHaptics} from 'lib/haptics' 15 + import {isWeb} from 'platform/detection' 16 + import {atoms as a, useTheme} from '#/alf' 17 + import {Text} from '#/components/Typography' 18 + 19 + const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 20 + 21 + let lastIndex = 0 22 + 23 + export function ChatEmptyPill() { 24 + const t = useTheme() 25 + const {_} = useLingui() 26 + const playHaptic = useHaptics() 27 + const [promptIndex, setPromptIndex] = React.useState(lastIndex) 28 + 29 + const scale = useSharedValue(1) 30 + 31 + const prompts = React.useMemo(() => { 32 + return [ 33 + _(msg`Say hello!`), 34 + _(msg`Share your favorite feed!`), 35 + _(msg`Tell a joke!`), 36 + _(msg`Share a fun fact!`), 37 + _(msg`Share a cool story!`), 38 + _(msg`Send a neat website!`), 39 + _(msg`Clip ๐Ÿด clop ๐Ÿด`), 40 + ] 41 + }, [_]) 42 + 43 + const onPressIn = React.useCallback(() => { 44 + if (isWeb) return 45 + scale.value = withTiming(1.075, {duration: 100}) 46 + }, [scale]) 47 + 48 + const onPressOut = React.useCallback(() => { 49 + if (isWeb) return 50 + scale.value = withTiming(1, {duration: 100}) 51 + }, [scale]) 52 + 53 + const onPress = React.useCallback(() => { 54 + runOnJS(playHaptic)() 55 + let randomPromptIndex = Math.floor(Math.random() * prompts.length) 56 + while (randomPromptIndex === lastIndex) { 57 + randomPromptIndex = Math.floor(Math.random() * prompts.length) 58 + } 59 + setPromptIndex(randomPromptIndex) 60 + lastIndex = randomPromptIndex 61 + }, [playHaptic, prompts.length]) 62 + 63 + const animatedStyle = useAnimatedStyle(() => ({ 64 + transform: [{scale: scale.value}], 65 + })) 66 + 67 + return ( 68 + <View 69 + style={[ 70 + a.absolute, 71 + a.w_full, 72 + a.z_10, 73 + a.align_center, 74 + { 75 + bottom: 70, 76 + }, 77 + ]}> 78 + <AnimatedPressable 79 + style={[ 80 + a.px_xl, 81 + a.py_md, 82 + a.rounded_full, 83 + t.atoms.bg_contrast_25, 84 + a.align_center, 85 + animatedStyle, 86 + ]} 87 + entering={ScaleAndFadeIn} 88 + exiting={ShrinkAndPop} 89 + onPress={onPress} 90 + onPressIn={onPressIn} 91 + onPressOut={onPressOut}> 92 + <Text style={[a.font_bold, a.pointer_events_none]} selectable={false}> 93 + {prompts[promptIndex]} 94 + </Text> 95 + </AnimatedPressable> 96 + </View> 97 + ) 98 + }
+27
src/lib/custom-animations/ShrinkAndPop.ts
··· 1 + import {withDelay, withSequence, withTiming} from 'react-native-reanimated' 2 + 3 + export function ShrinkAndPop() { 4 + 'worklet' 5 + 6 + const animations = { 7 + opacity: withDelay(125, withTiming(0, {duration: 125})), 8 + transform: [ 9 + { 10 + scale: withSequence( 11 + withTiming(0.7, {duration: 75}), 12 + withTiming(1.1, {duration: 150}), 13 + ), 14 + }, 15 + ], 16 + } 17 + 18 + const initialValues = { 19 + opacity: 1, 20 + transform: [{scale: 1}], 21 + } 22 + 23 + return { 24 + animations, 25 + initialValues, 26 + } 27 + }
+11 -8
src/screens/Messages/Conversation/MessagesList.tsx
··· 17 17 18 18 import {shortenLinks} from '#/lib/strings/rich-text-manip' 19 19 import {isIOS, isNative} from '#/platform/detection' 20 - import {useConvoActive} from '#/state/messages/convo' 20 + import {isConvoActive, useConvoActive} from '#/state/messages/convo' 21 21 import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' 22 22 import {useAgent} from '#/state/session' 23 23 import {ScrollProvider} from 'lib/ScrollContext' ··· 26 26 import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled' 27 27 import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' 28 28 import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' 29 + import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' 29 30 import {MessageItem} from '#/components/dms/MessageItem' 30 31 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 31 32 import {Loader} from '#/components/Loader' ··· 340 341 /> 341 342 </ScrollProvider> 342 343 <KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}> 343 - {!blocked ? ( 344 + {convoState.status === ConvoStatus.Disabled ? ( 345 + <ChatDisabled /> 346 + ) : blocked ? ( 347 + footer 348 + ) : ( 344 349 <> 345 - {convoState.status === ConvoStatus.Disabled ? ( 346 - <ChatDisabled /> 347 - ) : ( 348 - <MessageInput onSendMessage={onSendMessage} /> 350 + {isConvoActive(convoState) && convoState.items.length === 0 && ( 351 + <ChatEmptyPill /> 349 352 )} 353 + <MessageInput onSendMessage={onSendMessage} /> 350 354 </> 351 - ) : ( 352 - footer 353 355 )} 354 356 </KeyboardStickyView> 357 + 355 358 {newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />} 356 359 </> 357 360 )
+5 -2
src/screens/Messages/List/ChatListItem.tsx
··· 13 13 import {useProfileShadow} from '#/state/cache/profile-shadow' 14 14 import {useModerationOpts} from '#/state/preferences/moderation-opts' 15 15 import {useSession} from '#/state/session' 16 + import {useHaptics} from 'lib/haptics' 16 17 import {logEvent} from 'lib/statsig/statsig' 17 18 import {sanitizeDisplayName} from 'lib/strings/display-names' 18 19 import {TimeElapsed} from '#/view/com/util/TimeElapsed' ··· 70 71 () => moderateProfile(profile, moderationOpts), 71 72 [profile, moderationOpts], 72 73 ) 74 + const playHaptic = useHaptics() 73 75 74 76 const blockInfo = React.useMemo(() => { 75 77 const modui = moderation.ui('profileView') ··· 134 136 ) 135 137 136 138 const onLongPress = useCallback(() => { 139 + playHaptic() 137 140 menuControl.open() 138 - }, [menuControl]) 141 + }, [playHaptic, menuControl]) 139 142 140 143 return ( 141 144 <View ··· 162 165 : undefined 163 166 } 164 167 onPress={onPress} 165 - onLongPress={isNative ? menuControl.open : undefined} 168 + onLongPress={isNative ? onLongPress : undefined} 166 169 onAccessibilityAction={onLongPress} 167 170 style={[ 168 171 web({