Bluesky app fork with some witchin' additions 馃挮
at readme-update 242 lines 6.8 kB view raw
1import {useCallback, useState} from 'react' 2import {Keyboard, Pressable, View} from 'react-native' 3import {msg, Trans} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5 6import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 7import { 8 useCameraPermission, 9 usePhotoLibraryPermission, 10 useVideoLibraryPermission, 11} from '#/lib/hooks/usePermissions' 12import {openCamera, openUnifiedPicker} from '#/lib/media/picker' 13import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 14import {MAX_IMAGES} from '#/view/com/composer/state/composer' 15import {UserAvatar} from '#/view/com/util/UserAvatar' 16import {atoms as a, native, useTheme, web} from '#/alf' 17import {Button} from '#/components/Button' 18import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 19import {Camera_Stroke2_Corner0_Rounded as CameraIcon} from '#/components/icons/Camera' 20import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 21import {SubtleHover} from '#/components/SubtleHover' 22import {Text} from '#/components/Typography' 23import {useAnalytics} from '#/analytics' 24import {IS_NATIVE} from '#/env' 25 26export function ComposerPrompt() { 27 const t = useTheme() 28 const ax = useAnalytics() 29 const {_} = useLingui() 30 const {openComposer} = useOpenComposer() 31 const profile = useCurrentAccountProfile() 32 const [hover, setHover] = useState(false) 33 const {requestCameraAccessIfNeeded} = useCameraPermission() 34 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 35 const {requestVideoAccessIfNeeded} = useVideoLibraryPermission() 36 const sheetWrapper = useSheetWrapper() 37 38 const onPress = useCallback(() => { 39 ax.metric('composerPrompt:press', {}) 40 openComposer({}) 41 }, [ax, openComposer]) 42 43 const onPressImage = useCallback(async () => { 44 ax.metric('composerPrompt:gallery:press', {}) 45 46 // On web, open the composer with the gallery picker auto-opening 47 if (!IS_NATIVE) { 48 openComposer({openGallery: true}) 49 return 50 } 51 52 try { 53 const [photoAccess, videoAccess] = await Promise.all([ 54 requestPhotoAccessIfNeeded(), 55 requestVideoAccessIfNeeded(), 56 ]) 57 58 if (!photoAccess && !videoAccess) { 59 return 60 } 61 62 if (Keyboard.isVisible()) { 63 Keyboard.dismiss() 64 } 65 66 const selectionCountRemaining = MAX_IMAGES 67 const {assets, canceled} = await sheetWrapper( 68 openUnifiedPicker({selectionCountRemaining}), 69 ) 70 71 if (canceled) { 72 return 73 } 74 75 if (assets.length > 0) { 76 const imageUris = assets 77 .filter(asset => asset.mimeType?.startsWith('image/')) 78 .slice(0, MAX_IMAGES) 79 .map(asset => ({ 80 uri: asset.uri, 81 width: asset.width, 82 height: asset.height, 83 })) 84 85 if (imageUris.length > 0) { 86 openComposer({imageUris}) 87 } 88 } 89 } catch (err: any) { 90 if (!String(err).toLowerCase().includes('cancel')) { 91 ax.logger.error('Error opening image picker', {error: err}) 92 } 93 } 94 }, [ 95 ax, 96 openComposer, 97 requestPhotoAccessIfNeeded, 98 requestVideoAccessIfNeeded, 99 sheetWrapper, 100 ]) 101 102 const onPressCamera = useCallback(async () => { 103 ax.metric('composerPrompt:camera:press', {}) 104 105 try { 106 if (!(await requestCameraAccessIfNeeded())) { 107 return 108 } 109 110 if (IS_NATIVE && Keyboard.isVisible()) { 111 Keyboard.dismiss() 112 } 113 114 const image = await openCamera({ 115 mediaTypes: 'images', 116 }) 117 118 const imageUris = [ 119 { 120 uri: image.path, 121 width: image.width, 122 height: image.height, 123 }, 124 ] 125 126 openComposer({ 127 imageUris: IS_NATIVE ? imageUris : undefined, 128 }) 129 } catch (err: any) { 130 if (!String(err).toLowerCase().includes('cancel')) { 131 ax.logger.error('Error opening camera', {error: err}) 132 } 133 } 134 }, [ax, openComposer, requestCameraAccessIfNeeded]) 135 136 if (!profile) { 137 return null 138 } 139 140 return ( 141 <Pressable 142 onPress={onPress} 143 android_ripple={null} 144 accessibilityRole="button" 145 accessibilityLabel={_(msg`Compose new post`)} 146 accessibilityHint={_(msg`Opens the post composer`)} 147 onPointerEnter={() => setHover(true)} 148 onPointerLeave={() => setHover(false)} 149 style={({pressed}) => [ 150 a.relative, 151 a.flex_row, 152 a.align_start, 153 { 154 paddingLeft: 18, 155 paddingRight: 15, 156 }, 157 a.py_md, 158 native({ 159 paddingTop: 10, 160 paddingBottom: 10, 161 }), 162 web({ 163 cursor: 'pointer', 164 outline: 'none', 165 }), 166 pressed && web({outline: 'none'}), 167 ]}> 168 <SubtleHover hover={hover} /> 169 <UserAvatar 170 avatar={profile.avatar} 171 size={42} 172 type={profile.associated?.labeler ? 'labeler' : 'user'} 173 /> 174 <View 175 style={[ 176 a.flex_1, 177 a.ml_md, 178 a.flex_row, 179 a.align_center, 180 a.justify_between, 181 { 182 height: 40, 183 }, 184 ]}> 185 <Text 186 style={[ 187 t.atoms.text_contrast_medium, 188 a.text_md, 189 {includeFontPadding: false}, 190 ]}> 191 <Trans>What's up?</Trans> 192 </Text> 193 <View style={[a.flex_row, a.gap_md]}> 194 {IS_NATIVE && ( 195 <Button 196 onPress={e => { 197 e.stopPropagation() 198 onPressCamera() 199 }} 200 label={_(msg`Open camera`)} 201 accessibilityHint={_(msg`Opens device camera`)} 202 variant="ghost" 203 shape="round"> 204 {({hovered, pressed, focused}) => ( 205 <CameraIcon 206 size="lg" 207 style={{ 208 color: 209 hovered || pressed || focused 210 ? t.palette.primary_500 211 : t.palette.contrast_300, 212 }} 213 /> 214 )} 215 </Button> 216 )} 217 <Button 218 onPress={e => { 219 e.stopPropagation() 220 onPressImage() 221 }} 222 label={_(msg`Add image`)} 223 accessibilityHint={_(msg`Opens image picker`)} 224 variant="ghost" 225 shape="round"> 226 {({hovered, pressed, focused}) => ( 227 <ImageIcon 228 size="lg" 229 style={{ 230 color: 231 hovered || pressed || focused 232 ? t.palette.primary_500 233 : t.palette.contrast_300, 234 }} 235 /> 236 )} 237 </Button> 238 </View> 239 </View> 240 </Pressable> 241 ) 242}