[🐴] 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>
zio/stable
Eric Bailey 2024-05-20 18:56:44 -05:00 committed by GitHub
parent 6dde487563
commit a7b0242cc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 143 additions and 12 deletions

View File

@ -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 (
<View
style={[
a.absolute,
a.w_full,
a.z_10,
a.align_center,
{
bottom: 70,
},
]}>
<AnimatedPressable
style={[
a.px_xl,
a.py_md,
a.rounded_full,
t.atoms.bg_contrast_25,
a.align_center,
animatedStyle,
]}
entering={ScaleAndFadeIn}
exiting={ShrinkAndPop}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<Text style={[a.font_bold, a.pointer_events_none]} selectable={false}>
{prompts[promptIndex]}
</Text>
</AnimatedPressable>
</View>
)
}

View File

@ -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,
}
}

View File

@ -17,7 +17,7 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {shortenLinks} from '#/lib/strings/rich-text-manip' import {shortenLinks} from '#/lib/strings/rich-text-manip'
import {isIOS, isNative} from '#/platform/detection' 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 {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {ScrollProvider} from 'lib/ScrollContext' import {ScrollProvider} from 'lib/ScrollContext'
@ -26,6 +26,7 @@ import {List} from 'view/com/util/List'
import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled' import {ChatDisabled} from '#/screens/Messages/Conversation/ChatDisabled'
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
import {MessageItem} from '#/components/dms/MessageItem' import {MessageItem} from '#/components/dms/MessageItem'
import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
@ -340,18 +341,20 @@ export function MessagesList({
/> />
</ScrollProvider> </ScrollProvider>
<KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}> <KeyboardStickyView offset={{closed: -bottomOffset, opened: 0}}>
{!blocked ? (
<>
{convoState.status === ConvoStatus.Disabled ? ( {convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled /> <ChatDisabled />
) : ( ) : blocked ? (
<MessageInput onSendMessage={onSendMessage} />
)}
</>
) : (
footer footer
) : (
<>
{isConvoActive(convoState) && convoState.items.length === 0 && (
<ChatEmptyPill />
)}
<MessageInput onSendMessage={onSendMessage} />
</>
)} )}
</KeyboardStickyView> </KeyboardStickyView>
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />} {newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
</> </>
) )

View File

@ -13,6 +13,7 @@ import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useHaptics} from 'lib/haptics'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {TimeElapsed} from '#/view/com/util/TimeElapsed'
@ -70,6 +71,7 @@ function ChatListItemReady({
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
) )
const playHaptic = useHaptics()
const blockInfo = React.useMemo(() => { const blockInfo = React.useMemo(() => {
const modui = moderation.ui('profileView') const modui = moderation.ui('profileView')
@ -134,8 +136,9 @@ function ChatListItemReady({
) )
const onLongPress = useCallback(() => { const onLongPress = useCallback(() => {
playHaptic()
menuControl.open() menuControl.open()
}, [menuControl]) }, [playHaptic, menuControl])
return ( return (
<View <View
@ -162,7 +165,7 @@ function ChatListItemReady({
: undefined : undefined
} }
onPress={onPress} onPress={onPress}
onLongPress={isNative ? menuControl.open : undefined} onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress} onAccessibilityAction={onLongPress}
style={[ style={[
web({ web({