Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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