Bluesky app fork with some witchin' additions 💫

Merge branch 'piotrpalek-fix-banner-cropper' into main

+121 -63
+2 -1
src/lib/media/picker.tsx
··· 1 1 import { 2 + Image as RNImage, 2 3 openCamera as openCameraFn, 3 4 openCropper as openCropperFn, 4 - Image as RNImage, 5 5 } from 'react-native-image-crop-picker' 6 + 6 7 import {CameraOpts, CropperOptions} from './types' 7 8 export {openPicker} from './picker.shared' 8 9
+6 -1
src/lib/media/picker.web.tsx
··· 1 1 /// <reference lib="dom" /> 2 2 3 + import {Image as RNImage} from 'react-native-image-crop-picker' 4 + 3 5 import {CameraOpts, CropperOptions} from './types' 4 - import {Image as RNImage} from 'react-native-image-crop-picker' 5 6 export {openPicker} from './picker.shared' 6 7 import {unstable__openModal} from '#/state/modals' 7 8 ··· 16 17 unstable__openModal({ 17 18 name: 'crop-image', 18 19 uri: opts.path, 20 + dimensions: 21 + opts.height && opts.width 22 + ? {width: opts.width, height: opts.height} 23 + : undefined, 19 24 onSelect: (img?: RNImage) => { 20 25 if (img) { 21 26 resolve(img)
+1
src/state/modals/index.tsx
··· 47 47 export interface CropImageModal { 48 48 name: 'crop-image' 49 49 uri: string 50 + dimensions?: {width: number; height: number} 50 51 onSelect: (img?: RNImage) => void 51 52 } 52 53
+59 -35
src/view/com/modals/crop-image/CropImage.web.tsx
··· 14 14 import {getDataUriSize} from 'lib/media/util' 15 15 import {gradients, s} from 'lib/styles' 16 16 import {Text} from 'view/com/util/text/Text' 17 + import {calculateDimensions} from './cropImageUtil' 17 18 18 19 enum AspectRatio { 19 20 Square = 'square', 20 21 Wide = 'wide', 21 22 Tall = 'tall', 23 + Custom = 'custom', 22 24 } 23 25 24 26 const DIMS: Record<string, Dimensions> = { ··· 31 33 32 34 export function Component({ 33 35 uri, 36 + dimensions, 34 37 onSelect, 35 38 }: { 36 39 uri: string 40 + dimensions?: Dimensions 37 41 onSelect: (img?: RNImage) => void 38 42 }) { 39 43 const {closeModal} = useModalControls() 40 44 const pal = usePalette('default') 41 45 const {_} = useLingui() 42 - const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square) 46 + const defaultAspectStyle = dimensions 47 + ? AspectRatio.Custom 48 + : AspectRatio.Square 49 + const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle) 43 50 const [scale, setScale] = React.useState<number>(1) 44 51 const editorRef = React.useRef<ImageEditor>(null) 52 + const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width 53 + const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height 45 54 46 55 const doSetAs = (v: AspectRatio) => () => setAs(v) 47 56 ··· 57 66 path: dataUri, 58 67 mime: 'image/jpeg', 59 68 size: getDataUriSize(dataUri), 60 - width: DIMS[as].width, 61 - height: DIMS[as].height, 69 + width: imageEditorWidth, 70 + height: imageEditorHeight, 62 71 }) 63 72 } else { 64 73 onSelect(undefined) ··· 73 82 cropperStyle = styles.cropperWide 74 83 } else if (as === AspectRatio.Tall) { 75 84 cropperStyle = styles.cropperTall 85 + } else if (as === AspectRatio.Custom) { 86 + const cropperDimensions = calculateDimensions( 87 + 550, 88 + imageEditorHeight, 89 + imageEditorWidth, 90 + ) 91 + cropperStyle = { 92 + width: cropperDimensions.width, 93 + height: cropperDimensions.height, 94 + } 76 95 } 96 + 77 97 return ( 78 98 <View> 79 99 <View style={[styles.cropper, pal.borderDark, cropperStyle]}> ··· 81 101 ref={editorRef} 82 102 style={styles.imageEditor} 83 103 image={uri} 84 - width={DIMS[as].width} 85 - height={DIMS[as].height} 104 + width={imageEditorWidth} 105 + height={imageEditorHeight} 86 106 scale={scale} 87 107 border={0} 88 108 /> ··· 97 117 maximumValue={3} 98 118 containerStyle={styles.slider} 99 119 /> 100 - <TouchableOpacity 101 - onPress={doSetAs(AspectRatio.Wide)} 102 - accessibilityRole="button" 103 - accessibilityLabel={_(msg`Wide`)} 104 - accessibilityHint={_(msg`Sets image aspect ratio to wide`)}> 105 - <RectWideIcon 106 - size={24} 107 - style={as === AspectRatio.Wide ? s.blue3 : pal.text} 108 - /> 109 - </TouchableOpacity> 110 - <TouchableOpacity 111 - onPress={doSetAs(AspectRatio.Tall)} 112 - accessibilityRole="button" 113 - accessibilityLabel={_(msg`Tall`)} 114 - accessibilityHint={_(msg`Sets image aspect ratio to tall`)}> 115 - <RectTallIcon 116 - size={24} 117 - style={as === AspectRatio.Tall ? s.blue3 : pal.text} 118 - /> 119 - </TouchableOpacity> 120 - <TouchableOpacity 121 - onPress={doSetAs(AspectRatio.Square)} 122 - accessibilityRole="button" 123 - accessibilityLabel={_(msg`Square`)} 124 - accessibilityHint={_(msg`Sets image aspect ratio to square`)}> 125 - <SquareIcon 126 - size={24} 127 - style={as === AspectRatio.Square ? s.blue3 : pal.text} 128 - /> 129 - </TouchableOpacity> 120 + {as === AspectRatio.Custom ? null : ( 121 + <> 122 + <TouchableOpacity 123 + onPress={doSetAs(AspectRatio.Wide)} 124 + accessibilityRole="button" 125 + accessibilityLabel={_(msg`Wide`)} 126 + accessibilityHint={_(msg`Sets image aspect ratio to wide`)}> 127 + <RectWideIcon 128 + size={24} 129 + style={as === AspectRatio.Wide ? s.blue3 : pal.text} 130 + /> 131 + </TouchableOpacity> 132 + <TouchableOpacity 133 + onPress={doSetAs(AspectRatio.Tall)} 134 + accessibilityRole="button" 135 + accessibilityLabel={_(msg`Tall`)} 136 + accessibilityHint={_(msg`Sets image aspect ratio to tall`)}> 137 + <RectTallIcon 138 + size={24} 139 + style={as === AspectRatio.Tall ? s.blue3 : pal.text} 140 + /> 141 + </TouchableOpacity> 142 + <TouchableOpacity 143 + onPress={doSetAs(AspectRatio.Square)} 144 + accessibilityRole="button" 145 + accessibilityLabel={_(msg`Square`)} 146 + accessibilityHint={_(msg`Sets image aspect ratio to square`)}> 147 + <SquareIcon 148 + size={24} 149 + style={as === AspectRatio.Square ? s.blue3 : pal.text} 150 + /> 151 + </TouchableOpacity> 152 + </> 153 + )} 130 154 </View> 131 155 <View style={styles.btns}> 132 156 <TouchableOpacity
+13
src/view/com/modals/crop-image/cropImageUtil.ts
··· 1 + export const calculateDimensions = ( 2 + maxWidth: number, 3 + originalHeight: number, 4 + originalWidth: number, 5 + ) => { 6 + const aspectRatio = originalWidth / originalHeight 7 + const newHeight = maxWidth / aspectRatio 8 + const newWidth = maxWidth 9 + return { 10 + width: newWidth, 11 + height: newHeight, 12 + } 13 + }
+15 -8
src/view/com/util/UserAvatar.tsx
··· 8 8 import {useLingui} from '@lingui/react' 9 9 import {useQueryClient} from '@tanstack/react-query' 10 10 11 + import {logger} from '#/logger' 11 12 import {usePalette} from 'lib/hooks/usePalette' 12 13 import { 13 14 useCameraPermission, ··· 282 283 return 283 284 } 284 285 285 - const croppedImage = await openCropper({ 286 - mediaType: 'photo', 287 - cropperCircleOverlay: true, 288 - height: item.height, 289 - width: item.width, 290 - path: item.path, 291 - }) 286 + try { 287 + const croppedImage = await openCropper({ 288 + mediaType: 'photo', 289 + cropperCircleOverlay: true, 290 + height: item.height, 291 + width: item.width, 292 + path: item.path, 293 + }) 292 294 293 - onSelectNewAvatar(croppedImage) 295 + onSelectNewAvatar(croppedImage) 296 + } catch (e: any) { 297 + if (!String(e).includes('Canceled')) { 298 + logger.error('Failed to crop banner', {error: e}) 299 + } 300 + } 294 301 }, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) 295 302 296 303 const onRemoveAvatar = React.useCallback(() => {
+25 -18
src/view/com/util/UserBanner.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {ModerationUI} from '@atproto/api' 3 + import {Image as RNImage} from 'react-native-image-crop-picker' 4 4 import {Image} from 'expo-image' 5 - import {useLingui} from '@lingui/react' 5 + import {ModerationUI} from '@atproto/api' 6 6 import {msg, Trans} from '@lingui/macro' 7 + import {useLingui} from '@lingui/react' 7 8 8 - import {colors} from 'lib/styles' 9 - import {useTheme} from 'lib/ThemeContext' 10 - import {useTheme as useAlfTheme, tokens} from '#/alf' 11 - import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 9 + import {logger} from '#/logger' 10 + import {usePalette} from 'lib/hooks/usePalette' 12 11 import { 13 - usePhotoLibraryPermission, 14 12 useCameraPermission, 13 + usePhotoLibraryPermission, 15 14 } from 'lib/hooks/usePermissions' 16 - import {usePalette} from 'lib/hooks/usePalette' 15 + import {colors} from 'lib/styles' 16 + import {useTheme} from 'lib/ThemeContext' 17 17 import {isAndroid, isNative} from 'platform/detection' 18 - import {Image as RNImage} from 'react-native-image-crop-picker' 19 18 import {EventStopper} from 'view/com/util/EventStopper' 20 - import * as Menu from '#/components/Menu' 19 + import {tokens, useTheme as useAlfTheme} from '#/alf' 21 20 import { 22 21 Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 23 22 Camera_Stroke2_Corner0_Rounded as Camera, 24 23 } from '#/components/icons/Camera' 25 24 import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' 26 25 import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 26 + import * as Menu from '#/components/Menu' 27 + import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 27 28 28 29 export function UserBanner({ 29 30 type, ··· 64 65 return 65 66 } 66 67 67 - onSelectNewBanner?.( 68 - await openCropper({ 69 - mediaType: 'photo', 70 - path: items[0].path, 71 - width: 3000, 72 - height: 1000, 73 - }), 74 - ) 68 + try { 69 + onSelectNewBanner?.( 70 + await openCropper({ 71 + mediaType: 'photo', 72 + path: items[0].path, 73 + width: 3000, 74 + height: 1000, 75 + }), 76 + ) 77 + } catch (e: any) { 78 + if (!String(e).includes('Canceled')) { 79 + logger.error('Failed to crop banner', {error: e}) 80 + } 81 + } 75 82 }, [onSelectNewBanner, requestPhotoAccessIfNeeded]) 76 83 77 84 const onRemoveBanner = React.useCallback(() => {