Files
bsky-social-app/src/view/com/feeds/ComposerPrompt.tsx
T
2026-08-03 06:25:42 -07:00

247 lines
6.8 KiB
TypeScript

import {useCallback, useState} from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {
useCameraPermission,
usePhotoLibraryPermission,
useVideoLibraryPermission,
} from '#/lib/hooks/usePermissions'
import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
import {MAX_GALLERY_IMAGES} from '#/view/com/composer/state/composer'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
export function ComposerPrompt() {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const {openComposer} = useOpenComposer()
const profile = useCurrentAccountProfile()
const [hover, setHover] = useState(false)
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
const sheetWrapper = useSheetWrapper()
const onPress = useCallback(() => {
ax.metric('composerPrompt:press', {})
openComposer({logContext: 'Fab'})
}, [ax, openComposer])
const onPressImage = useCallback(async () => {
ax.metric('composerPrompt:gallery:press', {})
// On web, open the composer with the gallery picker auto-opening
if (!IS_NATIVE) {
openComposer({openGallery: true, logContext: 'Fab'})
return
}
try {
const [photoAccess, videoAccess] = await Promise.all([
requestPhotoAccessIfNeeded(),
requestVideoAccessIfNeeded(),
])
if (!photoAccess && !videoAccess) {
return
}
if (Keyboard.isVisible()) {
Keyboard.dismiss()
}
const selectionCountRemaining = MAX_GALLERY_IMAGES
const {assets, canceled} = await sheetWrapper(
openUnifiedPicker({selectionCountRemaining}),
)
if (canceled) {
return
}
if (assets.length > 0) {
const imageUris = assets
.filter(asset => asset.mimeType?.startsWith('image/'))
.slice(0, MAX_GALLERY_IMAGES)
.map(asset => ({
uri: asset.uri,
width: asset.width,
height: asset.height,
}))
if (imageUris.length > 0) {
openComposer({imageUris, logContext: 'Fab'})
}
}
} catch (err: any) {
if (!String(err).toLowerCase().includes('cancel')) {
ax.logger.error('Error opening image picker', {error: err})
}
}
}, [
ax,
openComposer,
requestPhotoAccessIfNeeded,
requestVideoAccessIfNeeded,
sheetWrapper,
])
const onPressCamera = useCallback(async () => {
ax.metric('composerPrompt:camera:press', {})
try {
if (!(await requestCameraAccessIfNeeded())) {
return
}
if (IS_NATIVE && Keyboard.isVisible()) {
Keyboard.dismiss()
}
const image = await openCamera({
mediaTypes: 'images',
})
if (!image) {
return
}
const imageUris = [
{
uri: image.path,
width: image.width,
height: image.height,
},
]
openComposer({
imageUris: IS_NATIVE ? imageUris : undefined,
logContext: 'Fab',
})
} catch (err: any) {
if (!String(err).toLowerCase().includes('cancel')) {
ax.logger.error('Error opening camera', {error: err})
}
}
}, [ax, openComposer, requestCameraAccessIfNeeded])
if (!profile) {
return null
}
return (
<Pressable
onPress={onPress}
android_ripple={null}
accessibilityRole="button"
accessibilityLabel={_(msg`Compose new post`)}
accessibilityHint={_(msg`Opens the post composer`)}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
style={({pressed}) => [
a.relative,
a.flex_row,
a.align_start,
{
paddingLeft: 18,
paddingRight: 15,
},
a.py_md,
native({
paddingTop: 10,
paddingBottom: 10,
}),
web({
cursor: 'pointer',
}),
pressed && web({outline: 'none'}),
]}>
<SubtleHover hover={hover} />
<UserAvatar
avatar={profile.avatar}
size={42}
type={profile.associated?.labeler ? 'labeler' : 'user'}
/>
<View
style={[
a.flex_1,
a.ml_md,
a.flex_row,
a.align_center,
a.justify_between,
{
height: 40,
},
]}>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_md,
{includeFontPadding: false},
]}>
<Trans>What's up?</Trans>
</Text>
<View style={[a.flex_row, a.gap_md]}>
{IS_NATIVE && (
<Button
onPress={e => {
e.stopPropagation()
onPressCamera()
}}
label={_(msg`Open camera`)}
accessibilityHint={_(msg`Opens device camera`)}
variant="ghost"
shape="round">
{({hovered, pressed, focused}) => (
<CameraIcon
size="lg"
style={{
color:
hovered || pressed || focused
? t.palette.primary_500
: t.palette.contrast_300,
}}
/>
)}
</Button>
)}
<Button
onPress={e => {
e.stopPropagation()
onPressImage()
}}
label={_(msg`Add image`)}
accessibilityHint={_(msg`Opens image picker`)}
variant="ghost"
shape="round">
{({hovered, pressed, focused}) => (
<ImageIcon
size="lg"
style={{
color:
hovered || pressed || focused
? t.palette.primary_500
: t.palette.contrast_300,
}}
/>
)}
</Button>
</View>
</View>
</Pressable>
)
}