Bluesky app fork with some witchin' additions 💫

Convert profile edit avatar/banner dropdown menus to new menu (#3177)

* convert profile edit dropdown menu to new menu

fix banner text

add `showCancel` prop to menu outer

banner dropdown to menu

add Cancel button to menu

replace user avatar dropdown with menu

add StreamingLive icon

add camera icon

* remove export

* use new camera icon

* adjust icon color

authored by hailey.at and committed by

GitHub 81232991 80cc1f18

+284 -225
+1
assets/icons/camera_filled_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" clip-rule="evenodd"/></svg>
+1
assets/icons/camera_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>
+1
assets/icons/streamingLive_stroke2_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>
+28 -2
src/components/Menu/index.tsx
··· 16 16 ItemTextProps, 17 17 ItemIconProps, 18 18 } from '#/components/Menu/types' 19 + import {Button, ButtonText} from '#/components/Button' 20 + import {msg} from '@lingui/macro' 21 + import {useLingui} from '@lingui/react' 22 + import {isNative} from 'platform/detection' 19 23 20 24 export {useDialogControl as useMenuControl} from '#/components/Dialog' 21 25 ··· 68 72 }) 69 73 } 70 74 71 - export function Outer({children}: React.PropsWithChildren<{}>) { 75 + export function Outer({ 76 + children, 77 + showCancel, 78 + }: React.PropsWithChildren<{showCancel?: boolean}>) { 72 79 const context = React.useContext(Context) 73 80 74 81 return ( ··· 78 85 {/* Re-wrap with context since Dialogs are portal-ed to root */} 79 86 <Context.Provider value={context}> 80 87 <Dialog.ScrollableInner label="Menu TODO"> 81 - <View style={[a.gap_lg]}>{children}</View> 88 + <View style={[a.gap_lg]}> 89 + {children} 90 + {isNative && showCancel && <Cancel />} 91 + </View> 82 92 <View style={{height: a.gap_lg.gap}} /> 83 93 </Dialog.ScrollableInner> 84 94 </Context.Provider> ··· 182 192 ) : null 183 193 })} 184 194 </View> 195 + ) 196 + } 197 + 198 + function Cancel() { 199 + const {_} = useLingui() 200 + const {control} = React.useContext(Context) 201 + 202 + return ( 203 + <Button 204 + label={_(msg`Close this dialog`)} 205 + size="small" 206 + variant="ghost" 207 + color="secondary" 208 + onPress={() => control.close()}> 209 + <ButtonText>Cancel</ButtonText> 210 + </Button> 185 211 ) 186 212 } 187 213
+9
src/components/icons/Camera.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Camera_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z', 5 + }) 6 + 7 + export const Camera_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ 8 + path: 'M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z', 9 + })
+5
src/components/icons/StreamingLive.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const StreamingLive_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z', 5 + })
+119 -113
src/view/com/util/UserAvatar.tsx
··· 1 1 import React, {memo, useMemo} from 'react' 2 - import {Image, StyleSheet, View} from 'react-native' 2 + import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import Svg, {Circle, Rect, Path} from 'react-native-svg' 4 + import {Image as RNImage} from 'react-native-image-crop-picker' 5 + import {useLingui} from '@lingui/react' 6 + import {msg, Trans} from '@lingui/macro' 4 7 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 8 + import {ModerationUI} from '@atproto/api' 9 + 5 10 import {HighPriorityImage} from 'view/com/util/images/Image' 6 - import {ModerationUI} from '@atproto/api' 7 11 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 8 12 import { 9 13 usePhotoLibraryPermission, ··· 11 15 } from 'lib/hooks/usePermissions' 12 16 import {colors} from 'lib/styles' 13 17 import {usePalette} from 'lib/hooks/usePalette' 14 - import {isWeb, isAndroid} from 'platform/detection' 15 - import {Image as RNImage} from 'react-native-image-crop-picker' 18 + import {isWeb, isAndroid, isNative} from 'platform/detection' 16 19 import {UserPreviewLink} from './UserPreviewLink' 17 - import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' 18 - import {useLingui} from '@lingui/react' 19 - import {msg} from '@lingui/macro' 20 + import * as Menu from '#/components/Menu' 21 + import { 22 + Camera_Stroke2_Corner0_Rounded as Camera, 23 + Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 24 + } from '#/components/icons/Camera' 25 + import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' 26 + import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 27 + import {useTheme} from '#/alf' 20 28 21 29 export type UserAvatarType = 'user' | 'algo' | 'list' 22 30 ··· 196 204 avatar, 197 205 onSelectNewAvatar, 198 206 }: EditableUserAvatarProps): React.ReactNode => { 207 + const t = useTheme() 199 208 const pal = usePalette('default') 200 209 const {_} = useLingui() 201 210 const {requestCameraAccessIfNeeded} = useCameraPermission() ··· 216 225 } 217 226 }, [type, size]) 218 227 219 - const dropdownItems = useMemo( 220 - () => 221 - [ 222 - !isWeb && { 223 - testID: 'changeAvatarCameraBtn', 224 - label: _(msg`Camera`), 225 - icon: { 226 - ios: { 227 - name: 'camera', 228 - }, 229 - android: 'ic_menu_camera', 230 - web: 'camera', 231 - }, 232 - onPress: async () => { 233 - if (!(await requestCameraAccessIfNeeded())) { 234 - return 235 - } 228 + const onOpenCamera = React.useCallback(async () => { 229 + if (!(await requestCameraAccessIfNeeded())) { 230 + return 231 + } 236 232 237 - onSelectNewAvatar( 238 - await openCamera({ 239 - width: 1000, 240 - height: 1000, 241 - cropperCircleOverlay: true, 242 - }), 243 - ) 244 - }, 245 - }, 246 - { 247 - testID: 'changeAvatarLibraryBtn', 248 - label: _(msg`Library`), 249 - icon: { 250 - ios: { 251 - name: 'photo.on.rectangle.angled', 252 - }, 253 - android: 'ic_menu_gallery', 254 - web: 'gallery', 255 - }, 256 - onPress: async () => { 257 - if (!(await requestPhotoAccessIfNeeded())) { 258 - return 259 - } 233 + onSelectNewAvatar( 234 + await openCamera({ 235 + width: 1000, 236 + height: 1000, 237 + cropperCircleOverlay: true, 238 + }), 239 + ) 240 + }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) 260 241 261 - const items = await openPicker({ 262 - aspect: [1, 1], 263 - }) 264 - const item = items[0] 265 - if (!item) { 266 - return 267 - } 242 + const onOpenLibrary = React.useCallback(async () => { 243 + if (!(await requestPhotoAccessIfNeeded())) { 244 + return 245 + } 268 246 269 - const croppedImage = await openCropper({ 270 - mediaType: 'photo', 271 - cropperCircleOverlay: true, 272 - height: item.height, 273 - width: item.width, 274 - path: item.path, 275 - }) 247 + const items = await openPicker({ 248 + aspect: [1, 1], 249 + }) 250 + const item = items[0] 251 + if (!item) { 252 + return 253 + } 276 254 277 - onSelectNewAvatar(croppedImage) 278 - }, 279 - }, 280 - !!avatar && { 281 - label: 'separator', 282 - }, 283 - !!avatar && { 284 - testID: 'changeAvatarRemoveBtn', 285 - label: _(msg`Remove`), 286 - icon: { 287 - ios: { 288 - name: 'trash', 289 - }, 290 - android: 'ic_delete', 291 - web: ['far', 'trash-can'], 292 - }, 293 - onPress: async () => { 294 - onSelectNewAvatar(null) 295 - }, 296 - }, 297 - ].filter(Boolean) as DropdownItem[], 298 - [ 299 - avatar, 300 - onSelectNewAvatar, 301 - requestCameraAccessIfNeeded, 302 - requestPhotoAccessIfNeeded, 303 - _, 304 - ], 305 - ) 255 + const croppedImage = await openCropper({ 256 + mediaType: 'photo', 257 + cropperCircleOverlay: true, 258 + height: item.height, 259 + width: item.width, 260 + path: item.path, 261 + }) 262 + 263 + onSelectNewAvatar(croppedImage) 264 + }, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) 265 + 266 + const onRemoveAvatar = React.useCallback(() => { 267 + onSelectNewAvatar(null) 268 + }, [onSelectNewAvatar]) 306 269 307 270 return ( 308 - <NativeDropdown 309 - testID="changeAvatarBtn" 310 - items={dropdownItems} 311 - accessibilityLabel={_(msg`Image options`)} 312 - accessibilityHint=""> 313 - {avatar ? ( 314 - <HighPriorityImage 315 - testID="userAvatarImage" 316 - style={aviStyle} 317 - source={{uri: avatar}} 318 - accessibilityRole="image" 319 - /> 320 - ) : ( 321 - <DefaultAvatar type={type} size={size} /> 322 - )} 323 - <View style={[styles.editButtonContainer, pal.btn]}> 324 - <FontAwesomeIcon 325 - icon="camera" 326 - size={12} 327 - color={pal.text.color as string} 328 - /> 329 - </View> 330 - </NativeDropdown> 271 + <Menu.Root> 272 + <Menu.Trigger label={_(msg`Edit avatar`)}> 273 + {({props}) => ( 274 + <TouchableOpacity {...props} activeOpacity={0.8}> 275 + {avatar ? ( 276 + <HighPriorityImage 277 + testID="userAvatarImage" 278 + style={aviStyle} 279 + source={{uri: avatar}} 280 + accessibilityRole="image" 281 + /> 282 + ) : ( 283 + <DefaultAvatar type={type} size={size} /> 284 + )} 285 + <View style={[styles.editButtonContainer, pal.btn]}> 286 + <CameraFilled height={14} width={14} style={t.atoms.text} /> 287 + </View> 288 + </TouchableOpacity> 289 + )} 290 + </Menu.Trigger> 291 + <Menu.Outer showCancel> 292 + <Menu.Group> 293 + {isNative && ( 294 + <Menu.Item 295 + testID="changeAvatarCameraBtn" 296 + label={_(msg`Upload from Camera`)} 297 + onPress={onOpenCamera}> 298 + <Menu.ItemText> 299 + <Trans>Upload from Camera</Trans> 300 + </Menu.ItemText> 301 + <Menu.ItemIcon icon={Camera} /> 302 + </Menu.Item> 303 + )} 304 + 305 + <Menu.Item 306 + testID="changeAvatarLibraryBtn" 307 + label={_(msg`Upload from Library`)} 308 + onPress={onOpenLibrary}> 309 + <Menu.ItemText> 310 + {isNative ? ( 311 + <Trans>Upload from Library</Trans> 312 + ) : ( 313 + <Trans>Upload from Files</Trans> 314 + )} 315 + </Menu.ItemText> 316 + <Menu.ItemIcon icon={Library} /> 317 + </Menu.Item> 318 + </Menu.Group> 319 + {!!avatar && ( 320 + <> 321 + <Menu.Divider /> 322 + <Menu.Group> 323 + <Menu.Item 324 + testID="changeAvatarRemoveBtn" 325 + label={_(`Remove Avatar`)} 326 + onPress={onRemoveAvatar}> 327 + <Menu.ItemText> 328 + <Trans>Remove Avatar</Trans> 329 + </Menu.ItemText> 330 + <Menu.ItemIcon icon={Trash} /> 331 + </Menu.Item> 332 + </Menu.Group> 333 + </> 334 + )} 335 + </Menu.Outer> 336 + </Menu.Root> 331 337 ) 332 338 } 333 339 EditableUserAvatar = memo(EditableUserAvatar)
+120 -110
src/view/com/util/UserBanner.tsx
··· 1 - import React, {useMemo} from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 1 + import React from 'react' 2 + import {StyleSheet, TouchableOpacity, View} from 'react-native' 4 3 import {ModerationUI} from '@atproto/api' 5 4 import {Image} from 'expo-image' 6 5 import {useLingui} from '@lingui/react' 7 - import {msg} from '@lingui/macro' 6 + import {msg, Trans} from '@lingui/macro' 7 + 8 8 import {colors} from 'lib/styles' 9 9 import {useTheme} from 'lib/ThemeContext' 10 + import {useTheme as useAlfTheme} from '#/alf' 10 11 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' 11 12 import { 12 13 usePhotoLibraryPermission, 13 14 useCameraPermission, 14 15 } from 'lib/hooks/usePermissions' 15 16 import {usePalette} from 'lib/hooks/usePalette' 16 - import {isWeb, isAndroid} from 'platform/detection' 17 + import {isAndroid, isNative} from 'platform/detection' 17 18 import {Image as RNImage} from 'react-native-image-crop-picker' 18 - import {NativeDropdown, DropdownItem} from './forms/NativeDropdown' 19 + import {EventStopper} from 'view/com/util/EventStopper' 20 + import * as Menu from '#/components/Menu' 21 + import { 22 + Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, 23 + Camera_Stroke2_Corner0_Rounded as Camera, 24 + } from '#/components/icons/Camera' 25 + import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' 26 + import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 19 27 20 28 export function UserBanner({ 21 29 banner, ··· 28 36 }) { 29 37 const pal = usePalette('default') 30 38 const theme = useTheme() 39 + const t = useAlfTheme() 31 40 const {_} = useLingui() 32 41 const {requestCameraAccessIfNeeded} = useCameraPermission() 33 42 const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() 34 43 35 - const dropdownItems: DropdownItem[] = useMemo( 36 - () => 37 - [ 38 - !isWeb && { 39 - testID: 'changeBannerCameraBtn', 40 - label: _(msg`Camera`), 41 - icon: { 42 - ios: { 43 - name: 'camera', 44 - }, 45 - android: 'ic_menu_camera', 46 - web: 'camera', 47 - }, 48 - onPress: async () => { 49 - if (!(await requestCameraAccessIfNeeded())) { 50 - return 51 - } 52 - onSelectNewBanner?.( 53 - await openCamera({ 54 - width: 3000, 55 - height: 1000, 56 - }), 57 - ) 58 - }, 59 - }, 60 - { 61 - testID: 'changeBannerLibraryBtn', 62 - label: _(msg`Library`), 63 - icon: { 64 - ios: { 65 - name: 'photo.on.rectangle.angled', 66 - }, 67 - android: 'ic_menu_gallery', 68 - web: 'gallery', 69 - }, 70 - onPress: async () => { 71 - if (!(await requestPhotoAccessIfNeeded())) { 72 - return 73 - } 74 - const items = await openPicker() 75 - if (!items[0]) { 76 - return 77 - } 44 + const onOpenCamera = React.useCallback(async () => { 45 + if (!(await requestCameraAccessIfNeeded())) { 46 + return 47 + } 48 + onSelectNewBanner?.( 49 + await openCamera({ 50 + width: 3000, 51 + height: 1000, 52 + }), 53 + ) 54 + }, [onSelectNewBanner, requestCameraAccessIfNeeded]) 78 55 79 - onSelectNewBanner?.( 80 - await openCropper({ 81 - mediaType: 'photo', 82 - path: items[0].path, 83 - width: 3000, 84 - height: 1000, 85 - }), 86 - ) 87 - }, 88 - }, 89 - !!banner && { 90 - testID: 'changeBannerRemoveBtn', 91 - label: _(msg`Remove`), 92 - icon: { 93 - ios: { 94 - name: 'trash', 95 - }, 96 - android: 'ic_delete', 97 - web: ['far', 'trash-can'], 98 - }, 99 - onPress: () => { 100 - onSelectNewBanner?.(null) 101 - }, 102 - }, 103 - ].filter(Boolean) as DropdownItem[], 104 - [ 105 - banner, 106 - onSelectNewBanner, 107 - requestCameraAccessIfNeeded, 108 - requestPhotoAccessIfNeeded, 109 - _, 110 - ], 111 - ) 56 + const onOpenLibrary = React.useCallback(async () => { 57 + if (!(await requestPhotoAccessIfNeeded())) { 58 + return 59 + } 60 + const items = await openPicker() 61 + if (!items[0]) { 62 + return 63 + } 64 + 65 + onSelectNewBanner?.( 66 + await openCropper({ 67 + mediaType: 'photo', 68 + path: items[0].path, 69 + width: 3000, 70 + height: 1000, 71 + }), 72 + ) 73 + }, [onSelectNewBanner, requestPhotoAccessIfNeeded]) 74 + 75 + const onRemoveBanner = React.useCallback(() => { 76 + onSelectNewBanner?.(null) 77 + }, [onSelectNewBanner]) 112 78 113 79 // setUserBanner is only passed as prop on the EditProfile component 114 80 return onSelectNewBanner ? ( 115 - <NativeDropdown 116 - testID="changeBannerBtn" 117 - items={dropdownItems} 118 - accessibilityLabel={_(msg`Image options`)} 119 - accessibilityHint=""> 120 - {banner ? ( 121 - <Image 122 - testID="userBannerImage" 123 - style={styles.bannerImage} 124 - source={{uri: banner}} 125 - accessible={true} 126 - accessibilityIgnoresInvertColors 127 - /> 128 - ) : ( 129 - <View 130 - testID="userBannerFallback" 131 - style={[styles.bannerImage, styles.defaultBanner]} 132 - /> 133 - )} 134 - <View style={[styles.editButtonContainer, pal.btn]}> 135 - <FontAwesomeIcon 136 - icon="camera" 137 - size={12} 138 - style={{color: colors.white}} 139 - color={pal.text.color as string} 140 - /> 141 - </View> 142 - </NativeDropdown> 81 + <EventStopper onKeyDown={false}> 82 + <Menu.Root> 83 + <Menu.Trigger label={_(msg`Edit avatar`)}> 84 + {({props}) => ( 85 + <TouchableOpacity {...props} activeOpacity={0.8}> 86 + {banner ? ( 87 + <Image 88 + testID="userBannerImage" 89 + style={styles.bannerImage} 90 + source={{uri: banner}} 91 + accessible={true} 92 + accessibilityIgnoresInvertColors 93 + /> 94 + ) : ( 95 + <View 96 + testID="userBannerFallback" 97 + style={[styles.bannerImage, styles.defaultBanner]} 98 + /> 99 + )} 100 + <View style={[styles.editButtonContainer, pal.btn]}> 101 + <CameraFilled height={14} width={14} style={t.atoms.text} /> 102 + </View> 103 + </TouchableOpacity> 104 + )} 105 + </Menu.Trigger> 106 + <Menu.Outer showCancel> 107 + <Menu.Group> 108 + {isNative && ( 109 + <Menu.Item 110 + testID="changeBannerCameraBtn" 111 + label={_(msg`Upload from Camera`)} 112 + onPress={onOpenCamera}> 113 + <Menu.ItemText> 114 + <Trans>Upload from Camera</Trans> 115 + </Menu.ItemText> 116 + <Menu.ItemIcon icon={Camera} /> 117 + </Menu.Item> 118 + )} 119 + 120 + <Menu.Item 121 + testID="changeBannerLibraryBtn" 122 + label={_(msg`Upload from Library`)} 123 + onPress={onOpenLibrary}> 124 + <Menu.ItemText> 125 + {isNative ? ( 126 + <Trans>Upload from Library</Trans> 127 + ) : ( 128 + <Trans>Upload from Files</Trans> 129 + )} 130 + </Menu.ItemText> 131 + <Menu.ItemIcon icon={Library} /> 132 + </Menu.Item> 133 + </Menu.Group> 134 + {!!banner && ( 135 + <> 136 + <Menu.Divider /> 137 + <Menu.Group> 138 + <Menu.Item 139 + testID="changeBannerRemoveBtn" 140 + label={_(`Remove Banner`)} 141 + onPress={onRemoveBanner}> 142 + <Menu.ItemText> 143 + <Trans>Remove Banner</Trans> 144 + </Menu.ItemText> 145 + <Menu.ItemIcon icon={Trash} /> 146 + </Menu.Item> 147 + </Menu.Group> 148 + </> 149 + )} 150 + </Menu.Outer> 151 + </Menu.Root> 152 + </EventStopper> 143 153 ) : banner && 144 154 !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( 145 155 <Image