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

Configure Feed

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

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