Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

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