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

Configure Feed

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

at 82f42e734c50b34de31e8aff1e7ced248ab6e96f 249 lines 7.6 kB view raw
1import {useCallback, useState} from 'react' 2import {Pressable, StyleSheet, View} from 'react-native' 3import {Image} from 'expo-image' 4import {type ModerationUI} from '@atproto/api' 5import {msg, Trans} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7 8import { 9 useCameraPermission, 10 usePhotoLibraryPermission, 11} from '#/lib/hooks/usePermissions' 12import {compressIfNeeded} from '#/lib/media/manip' 13import {openCamera, openCropper, openPicker} from '#/lib/media/picker' 14import {type PickerImage} from '#/lib/media/picker.shared' 15import {isCancelledError} from '#/lib/strings/errors' 16import {logger} from '#/logger' 17import { 18 type ComposerImage, 19 compressImage, 20 createComposerImage, 21} from '#/state/gallery' 22import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' 23import {EventStopper} from '#/view/com/util/EventStopper' 24import {atoms as a, tokens, useTheme} from '#/alf' 25import {useDialogControl} from '#/components/Dialog' 26import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 27import { 28 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilledIcon, 29 Camera_Stroke2_Corner0_Rounded as CameraIcon, 30} from '#/components/icons/Camera' 31import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' 32import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 33import * as Menu from '#/components/Menu' 34import {IS_ANDROID, IS_NATIVE} from '#/env' 35 36export function UserBanner({ 37 type, 38 banner, 39 moderation, 40 onSelectNewBanner, 41}: { 42 type?: 'labeler' | 'default' 43 banner?: string | null 44 moderation?: ModerationUI 45 onSelectNewBanner?: (img: PickerImage | null) => void 46}) { 47 const t = useTheme() 48 const {_} = useLingui() 49 const {requestCameraAccessIfNeeded} = useCameraPermission() 50 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 51 const sheetWrapper = useSheetWrapper() 52 const [rawImage, setRawImage] = useState<ComposerImage | undefined>() 53 const editImageDialogControl = useDialogControl() 54 55 const onOpenCamera = useCallback(async () => { 56 if (!(await requestCameraAccessIfNeeded())) { 57 return 58 } 59 onSelectNewBanner?.( 60 await compressIfNeeded( 61 await openCamera({ 62 aspect: [3, 1], 63 }), 64 ), 65 ) 66 }, [onSelectNewBanner, requestCameraAccessIfNeeded]) 67 68 const onOpenLibrary = useCallback(async () => { 69 if (!(await requestPhotoAccessIfNeeded())) { 70 return 71 } 72 const items = await sheetWrapper(openPicker()) 73 if (!items[0]) { 74 return 75 } 76 77 try { 78 if (IS_NATIVE) { 79 onSelectNewBanner?.( 80 await compressIfNeeded( 81 await openCropper({ 82 imageUri: items[0].path, 83 aspectRatio: 3 / 1, 84 }), 85 ), 86 ) 87 } else { 88 setRawImage(await createComposerImage(items[0])) 89 editImageDialogControl.open() 90 } 91 } catch (e) { 92 // Don't log errors for cancelling selection to sentry on ios or android 93 if (!isCancelledError(e)) { 94 logger.error('Failed to crop banner', {error: e}) 95 } 96 } 97 }, [ 98 onSelectNewBanner, 99 requestPhotoAccessIfNeeded, 100 sheetWrapper, 101 editImageDialogControl, 102 ]) 103 104 const onRemoveBanner = useCallback(() => { 105 onSelectNewBanner?.(null) 106 }, [onSelectNewBanner]) 107 108 const onChangeEditImage = useCallback( 109 async (image: ComposerImage) => { 110 const compressed = await compressImage(image) 111 onSelectNewBanner?.(compressed) 112 }, 113 [onSelectNewBanner], 114 ) 115 116 // setUserBanner is only passed as prop on the EditProfile component 117 return onSelectNewBanner ? ( 118 <> 119 <EventStopper onKeyDown={true}> 120 <Menu.Root> 121 <Menu.Trigger label={_(msg`Edit avatar`)}> 122 {({props}) => ( 123 <Pressable {...props} testID="changeBannerBtn"> 124 {banner ? ( 125 <Image 126 testID="userBannerImage" 127 style={styles.bannerImage} 128 source={{uri: banner}} 129 accessible={true} 130 accessibilityIgnoresInvertColors 131 /> 132 ) : ( 133 <View 134 testID="userBannerFallback" 135 style={[styles.bannerImage, t.atoms.bg_contrast_25]} 136 /> 137 )} 138 <View 139 style={[ 140 styles.editButtonContainer, 141 t.atoms.bg_contrast_25, 142 a.border, 143 t.atoms.border_contrast_low, 144 ]}> 145 <CameraFilledIcon 146 height={14} 147 width={14} 148 style={t.atoms.text} 149 /> 150 </View> 151 </Pressable> 152 )} 153 </Menu.Trigger> 154 <Menu.Outer showCancel> 155 <Menu.Group> 156 {IS_NATIVE && ( 157 <Menu.Item 158 testID="changeBannerCameraBtn" 159 label={_(msg`Upload from Camera`)} 160 onPress={onOpenCamera}> 161 <Menu.ItemText> 162 <Trans>Upload from Camera</Trans> 163 </Menu.ItemText> 164 <Menu.ItemIcon icon={CameraIcon} /> 165 </Menu.Item> 166 )} 167 168 <Menu.Item 169 testID="changeBannerLibraryBtn" 170 label={_(msg`Upload from Library`)} 171 onPress={onOpenLibrary}> 172 <Menu.ItemText> 173 {IS_NATIVE ? ( 174 <Trans>Upload from Library</Trans> 175 ) : ( 176 <Trans>Upload from Files</Trans> 177 )} 178 </Menu.ItemText> 179 <Menu.ItemIcon icon={LibraryIcon} /> 180 </Menu.Item> 181 </Menu.Group> 182 {!!banner && ( 183 <> 184 <Menu.Divider /> 185 <Menu.Group> 186 <Menu.Item 187 testID="changeBannerRemoveBtn" 188 label={_(msg`Remove Banner`)} 189 onPress={onRemoveBanner}> 190 <Menu.ItemText> 191 <Trans>Remove Banner</Trans> 192 </Menu.ItemText> 193 <Menu.ItemIcon icon={TrashIcon} /> 194 </Menu.Item> 195 </Menu.Group> 196 </> 197 )} 198 </Menu.Outer> 199 </Menu.Root> 200 </EventStopper> 201 202 <EditImageDialog 203 control={editImageDialogControl} 204 image={rawImage} 205 onChange={onChangeEditImage} 206 aspectRatio={3} 207 /> 208 </> 209 ) : banner && 210 !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? ( 211 <Image 212 testID="userBannerImage" 213 style={[styles.bannerImage, t.atoms.bg_contrast_25]} 214 contentFit="cover" 215 source={{uri: banner}} 216 blurRadius={moderation?.blur ? 100 : 0} 217 accessible={true} 218 accessibilityIgnoresInvertColors 219 /> 220 ) : ( 221 <View 222 testID="userBannerFallback" 223 style={[ 224 styles.bannerImage, 225 type === 'labeler' ? styles.labelerBanner : t.atoms.bg_contrast_25, 226 ]} 227 /> 228 ) 229} 230 231const styles = StyleSheet.create({ 232 editButtonContainer: { 233 position: 'absolute', 234 width: 24, 235 height: 24, 236 bottom: 8, 237 right: 24, 238 borderRadius: 12, 239 alignItems: 'center', 240 justifyContent: 'center', 241 }, 242 bannerImage: { 243 width: '100%', 244 height: 150, 245 }, 246 labelerBanner: { 247 backgroundColor: tokens.color.temp_purple, 248 }, 249})