mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

at localize-dates 402 lines 11 kB view raw
1import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' 2import {Pressable, StyleSheet, View} from 'react-native' 3import {useWindowDimensions} from 'react-native' 4import {LinearGradient} from 'expo-linear-gradient' 5import {MaterialIcons} from '@expo/vector-icons' 6import {msg, Trans} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {Slider} from '@miblanchard/react-native-slider' 9import {observer} from 'mobx-react-lite' 10import ImageEditor, {Position} from 'react-avatar-editor' 11 12import {useModalControls} from '#/state/modals' 13import {MAX_ALT_TEXT} from 'lib/constants' 14import {usePalette} from 'lib/hooks/usePalette' 15import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 16import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons' 17import {enforceLen} from 'lib/strings/helpers' 18import {gradients, s} from 'lib/styles' 19import {useTheme} from 'lib/ThemeContext' 20import {getKeys} from 'lib/type-assertions' 21import {GalleryModel} from 'state/models/media/gallery' 22import {ImageModel} from 'state/models/media/image' 23import {Text} from '../util/text/Text' 24import {TextInput} from './util' 25 26export const snapPoints = ['80%'] 27 28const RATIOS = { 29 '4:3': { 30 Icon: RectWideIcon, 31 }, 32 '1:1': { 33 Icon: SquareIcon, 34 }, 35 '3:4': { 36 Icon: RectTallIcon, 37 }, 38 None: { 39 label: 'None', 40 Icon: MaterialIcons, 41 name: 'do-not-disturb-alt', 42 }, 43} as const 44 45type AspectRatio = keyof typeof RATIOS 46 47interface Props { 48 image: ImageModel 49 gallery: GalleryModel 50} 51 52export const Component = observer(function EditImageImpl({ 53 image, 54 gallery, 55}: Props) { 56 const pal = usePalette('default') 57 const theme = useTheme() 58 const {_} = useLingui() 59 const windowDimensions = useWindowDimensions() 60 const {isMobile} = useWebMediaQueries() 61 const {closeModal} = useModalControls() 62 63 const { 64 aspectRatio, 65 // rotate = 0 66 } = image.attributes 67 68 const editorRef = useRef<ImageEditor>(null) 69 const [scale, setScale] = useState<number>(image.attributes.scale ?? 1) 70 const [position, setPosition] = useState<Position | undefined>( 71 image.attributes.position, 72 ) 73 const [altText, setAltText] = useState(image?.altText ?? '') 74 75 const onFlipHorizontal = useCallback(() => { 76 image.flipHorizontal() 77 }, [image]) 78 79 const onFlipVertical = useCallback(() => { 80 image.flipVertical() 81 }, [image]) 82 83 // const onSetRotate = useCallback( 84 // (direction: 'left' | 'right') => { 85 // const rotation = (rotate + 90 * (direction === 'left' ? -1 : 1)) % 360 86 // image.setRotate(rotation) 87 // }, 88 // [rotate, image], 89 // ) 90 91 const onSetRatio = useCallback( 92 (ratio: AspectRatio) => { 93 image.setRatio(ratio) 94 }, 95 [image], 96 ) 97 98 const adjustments = useMemo( 99 () => [ 100 // { 101 // name: 'rotate-left' as const, 102 // label: 'Rotate left', 103 // onPress: () => { 104 // onSetRotate('left') 105 // }, 106 // }, 107 // { 108 // name: 'rotate-right' as const, 109 // label: 'Rotate right', 110 // onPress: () => { 111 // onSetRotate('right') 112 // }, 113 // }, 114 { 115 name: 'flip' as const, 116 label: _(msg`Flip horizontal`), 117 onPress: onFlipHorizontal, 118 }, 119 { 120 name: 'flip' as const, 121 label: _(msg`Flip vertically`), 122 onPress: onFlipVertical, 123 }, 124 ], 125 [onFlipHorizontal, onFlipVertical, _], 126 ) 127 128 useEffect(() => { 129 image.prev = image.cropped 130 image.prevAttributes = image.attributes 131 image.resetCropped() 132 }, [image]) 133 134 const onCloseModal = useCallback(() => { 135 closeModal() 136 }, [closeModal]) 137 138 const onPressCancel = useCallback(async () => { 139 await gallery.previous(image) 140 onCloseModal() 141 }, [onCloseModal, gallery, image]) 142 143 const onPressSave = useCallback(async () => { 144 image.setAltText(altText) 145 146 const crop = editorRef.current?.getCroppingRect() 147 148 await image.manipulate({ 149 ...(crop !== undefined 150 ? { 151 crop: { 152 originX: crop.x, 153 originY: crop.y, 154 width: crop.width, 155 height: crop.height, 156 }, 157 ...(scale !== 1 ? {scale} : {}), 158 ...(position !== undefined ? {position} : {}), 159 } 160 : {}), 161 }) 162 163 image.prev = image.cropped 164 image.prevAttributes = image.attributes 165 onCloseModal() 166 }, [altText, image, position, scale, onCloseModal]) 167 168 const getLabelIconSize = useCallback((as: AspectRatio) => { 169 switch (as) { 170 case 'None': 171 return 22 172 case '1:1': 173 return 32 174 default: 175 return 26 176 } 177 }, []) 178 179 if (image.cropped === undefined) { 180 return null 181 } 182 183 const computedWidth = 184 windowDimensions.width > 500 ? 410 : windowDimensions.width - 80 185 const sideLength = isMobile ? computedWidth : 300 186 187 const dimensions = image.getResizedDimensions(aspectRatio, sideLength) 188 const imgContainerStyles = {width: sideLength, height: sideLength} 189 190 const imgControlStyles = { 191 alignItems: 'center' as const, 192 flexDirection: isMobile ? ('column' as const) : ('row' as const), 193 gap: isMobile ? 0 : 5, 194 } 195 196 return ( 197 <View 198 testID="editImageModal" 199 style={[ 200 pal.view, 201 styles.container, 202 s.flex1, 203 { 204 paddingHorizontal: isMobile ? 16 : undefined, 205 }, 206 ]}> 207 <Text style={[styles.title, pal.text]}> 208 <Trans>Edit image</Trans> 209 </Text> 210 <View style={[styles.gap18, s.flexRow]}> 211 <View> 212 <View 213 style={[styles.imgContainer, pal.borderDark, imgContainerStyles]}> 214 <ImageEditor 215 ref={editorRef} 216 style={styles.imgEditor} 217 image={image.cropped.path} 218 scale={scale} 219 border={0} 220 position={position} 221 onPositionChange={setPosition} 222 {...dimensions} 223 /> 224 </View> 225 <Slider 226 value={scale} 227 onValueChange={(v: number | number[]) => 228 setScale(Array.isArray(v) ? v[0] : v) 229 } 230 minimumValue={1} 231 maximumValue={3} 232 /> 233 </View> 234 <View> 235 {!isMobile ? ( 236 <Text type="sm-bold" style={pal.text}> 237 <Trans>Ratios</Trans> 238 </Text> 239 ) : null} 240 <View style={imgControlStyles}> 241 {getKeys(RATIOS).map(ratio => { 242 const {Icon, ...props} = RATIOS[ratio] 243 const labelIconSize = getLabelIconSize(ratio) 244 const isSelected = aspectRatio === ratio 245 246 return ( 247 <Pressable 248 key={ratio} 249 onPress={() => { 250 onSetRatio(ratio) 251 }} 252 accessibilityLabel={ratio} 253 accessibilityHint=""> 254 <Icon 255 size={labelIconSize} 256 style={[styles.imgControl, isSelected ? s.blue3 : pal.text]} 257 color={(isSelected ? s.blue3 : pal.text).color} 258 {...props} 259 /> 260 261 <Text 262 type={isSelected ? 'xs-bold' : 'xs-medium'} 263 style={[isSelected ? s.blue3 : pal.text, s.textCenter]}> 264 {ratio} 265 </Text> 266 </Pressable> 267 ) 268 })} 269 </View> 270 {!isMobile ? ( 271 <Text type="sm-bold" style={[pal.text, styles.subsection]}> 272 <Trans>Transformations</Trans> 273 </Text> 274 ) : null} 275 <View style={imgControlStyles}> 276 {adjustments.map(({label, name, onPress}) => ( 277 <Pressable 278 key={label} 279 onPress={onPress} 280 accessibilityLabel={label} 281 accessibilityHint="" 282 style={styles.flipBtn}> 283 <MaterialIcons 284 name={name} 285 size={label?.startsWith('Flip') ? 22 : 24} 286 style={[ 287 pal.text, 288 label === _(msg`Flip vertically`) 289 ? styles.flipVertical 290 : undefined, 291 ]} 292 /> 293 </Pressable> 294 ))} 295 </View> 296 </View> 297 </View> 298 <View style={[styles.gap18, styles.bottomSection, pal.border]}> 299 <Text type="sm-bold" style={pal.text} nativeID="alt-text"> 300 <Trans>Accessibility</Trans> 301 </Text> 302 <TextInput 303 testID="altTextImageInput" 304 style={[ 305 styles.textArea, 306 pal.border, 307 pal.text, 308 { 309 maxHeight: isMobile ? 50 : undefined, 310 }, 311 ]} 312 keyboardAppearance={theme.colorScheme} 313 multiline 314 value={altText} 315 onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))} 316 accessibilityLabel={_(msg`Alt text`)} 317 accessibilityHint="" 318 accessibilityLabelledBy="alt-text" 319 /> 320 </View> 321 <View style={styles.btns}> 322 <Pressable onPress={onPressCancel} accessibilityRole="button"> 323 <Text type="xl" style={pal.link}> 324 <Trans>Cancel</Trans> 325 </Text> 326 </Pressable> 327 <Pressable onPress={onPressSave} accessibilityRole="button"> 328 <LinearGradient 329 colors={[gradients.blueLight.start, gradients.blueLight.end]} 330 start={{x: 0, y: 0}} 331 end={{x: 1, y: 1}} 332 style={[styles.btn]}> 333 <Text type="xl-medium" style={s.white}> 334 <Trans context="action">Done</Trans> 335 </Text> 336 </LinearGradient> 337 </Pressable> 338 </View> 339 </View> 340 ) 341}) 342 343const styles = StyleSheet.create({ 344 container: { 345 gap: 18, 346 height: '100%', 347 width: '100%', 348 }, 349 subsection: {marginTop: 12}, 350 gap18: {gap: 18}, 351 title: { 352 fontWeight: 'bold', 353 fontSize: 24, 354 }, 355 btns: { 356 flexDirection: 'row', 357 alignItems: 'center', 358 justifyContent: 'space-between', 359 }, 360 btn: { 361 borderRadius: 4, 362 paddingVertical: 8, 363 paddingHorizontal: 24, 364 }, 365 imgControl: { 366 display: 'flex', 367 alignItems: 'center', 368 justifyContent: 'center', 369 height: 40, 370 }, 371 imgEditor: { 372 maxWidth: '100%', 373 }, 374 imgContainer: { 375 display: 'flex', 376 alignItems: 'center', 377 justifyContent: 'center', 378 borderWidth: 1, 379 borderStyle: 'solid', 380 marginBottom: 4, 381 }, 382 flipVertical: { 383 transform: [{rotate: '90deg'}], 384 }, 385 flipBtn: { 386 paddingHorizontal: 4, 387 paddingVertical: 8, 388 }, 389 textArea: { 390 borderWidth: 1, 391 borderRadius: 6, 392 paddingTop: 10, 393 paddingHorizontal: 12, 394 fontSize: 16, 395 height: 100, 396 textAlignVertical: 'top', 397 }, 398 bottomSection: { 399 borderTopWidth: 1, 400 paddingTop: 18, 401 }, 402})