diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-05-06 22:50:28 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-06 22:50:28 +0300 |
commit | 4c8fd006f6a994783a43e4744a3167db7aefc159 (patch) | |
tree | 0b8350774438cbbc2b8c6e77b844ce6b677381d3 /src/view/com/util/UserBanner.tsx | |
parent | 3f7dc9a8e5c9225ef20ce996543a1c3cfa991eb7 (diff) | |
download | voidsky-4c8fd006f6a994783a43e4744a3167db7aefc159.tar.zst |
New Edit Profile dialog on web, use new Edit Image dialog everywhere (#8220)
Diffstat (limited to 'src/view/com/util/UserBanner.tsx')
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 240 |
1 files changed, 139 insertions, 101 deletions
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index ab7f25b80..3600f5c24 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -1,35 +1,36 @@ -import React from 'react' +import {useCallback, useState} from 'react' import {Pressable, StyleSheet, View} from 'react-native' import {Image} from 'expo-image' import {type ModerationUI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {usePalette} from '#/lib/hooks/usePalette' import { useCameraPermission, usePhotoLibraryPermission, } from '#/lib/hooks/usePermissions' -import {colors} from '#/lib/styles' -import {useTheme} from '#/lib/ThemeContext' +import {compressIfNeeded} from '#/lib/media/manip' +import {openCamera, openCropper, openPicker} from '#/lib/media/picker' +import {type PickerImage} from '#/lib/media/picker.shared' import {logger} from '#/logger' import {isAndroid, isNative} from '#/platform/detection' +import { + type ComposerImage, + compressImage, + createComposerImage, +} from '#/state/gallery' +import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' import {EventStopper} from '#/view/com/util/EventStopper' -import {tokens, useTheme as useAlfTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' +import {useDialogControl} from '#/components/Dialog' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' import { - Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, - Camera_Stroke2_Corner0_Rounded as Camera, + Camera_Filled_Stroke2_Corner0_Rounded as CameraFilledIcon, + Camera_Stroke2_Corner0_Rounded as CameraIcon, } from '#/components/icons/Camera' -import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' -import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' import * as Menu from '#/components/Menu' -import { - openCamera, - openCropper, - openPicker, - type RNImage, -} from '../../../lib/media/picker' export function UserBanner({ type, @@ -40,28 +41,30 @@ export function UserBanner({ type?: 'labeler' | 'default' banner?: string | null moderation?: ModerationUI - onSelectNewBanner?: (img: RNImage | null) => void + onSelectNewBanner?: (img: PickerImage | null) => void }) { - const pal = usePalette('default') - const theme = useTheme() - const t = useAlfTheme() + const t = useTheme() const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() const sheetWrapper = useSheetWrapper() + const [rawImage, setRawImage] = useState<ComposerImage | undefined>() + const editImageDialogControl = useDialogControl() - const onOpenCamera = React.useCallback(async () => { + const onOpenCamera = useCallback(async () => { if (!(await requestCameraAccessIfNeeded())) { return } onSelectNewBanner?.( - await openCamera({ - aspect: [3, 1], - }), + await compressIfNeeded( + await openCamera({ + aspect: [3, 1], + }), + ), ) }, [onSelectNewBanner, requestCameraAccessIfNeeded]) - const onOpenLibrary = React.useCallback(async () => { + const onOpenLibrary = useCallback(async () => { if (!(await requestPhotoAccessIfNeeded())) { return } @@ -71,105 +74,141 @@ export function UserBanner({ } try { - onSelectNewBanner?.( - await openCropper({ - imageUri: items[0].path, - aspectRatio: 3 / 1, - }), - ) + if (isNative) { + onSelectNewBanner?.( + await compressIfNeeded( + await openCropper({ + imageUri: items[0].path, + aspectRatio: 3 / 1, + }), + ), + ) + } else { + setRawImage(await createComposerImage(items[0])) + editImageDialogControl.open() + } } catch (e: any) { if (!String(e).includes('Canceled')) { logger.error('Failed to crop banner', {error: e}) } } - }, [onSelectNewBanner, requestPhotoAccessIfNeeded, sheetWrapper]) + }, [ + onSelectNewBanner, + requestPhotoAccessIfNeeded, + sheetWrapper, + editImageDialogControl, + ]) - const onRemoveBanner = React.useCallback(() => { + const onRemoveBanner = useCallback(() => { onSelectNewBanner?.(null) }, [onSelectNewBanner]) + const onChangeEditImage = useCallback( + async (image: ComposerImage) => { + const compressed = await compressImage(image) + onSelectNewBanner?.(compressed) + }, + [onSelectNewBanner], + ) + // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( - <EventStopper onKeyDown={true}> - <Menu.Root> - <Menu.Trigger label={_(msg`Edit avatar`)}> - {({props}) => ( - <Pressable {...props} testID="changeBannerBtn"> - {banner ? ( - <Image - testID="userBannerImage" - style={styles.bannerImage} - source={{uri: banner}} - accessible={true} - accessibilityIgnoresInvertColors - /> - ) : ( + <> + <EventStopper onKeyDown={true}> + <Menu.Root> + <Menu.Trigger label={_(msg`Edit avatar`)}> + {({props}) => ( + <Pressable {...props} testID="changeBannerBtn"> + {banner ? ( + <Image + testID="userBannerImage" + style={styles.bannerImage} + source={{uri: banner}} + accessible={true} + accessibilityIgnoresInvertColors + /> + ) : ( + <View + testID="userBannerFallback" + style={[styles.bannerImage, t.atoms.bg_contrast_25]} + /> + )} <View - testID="userBannerFallback" - style={[styles.bannerImage, t.atoms.bg_contrast_25]} - /> + style={[ + styles.editButtonContainer, + t.atoms.bg_contrast_25, + a.border, + t.atoms.border_contrast_low, + ]}> + <CameraFilledIcon + height={14} + width={14} + style={t.atoms.text} + /> + </View> + </Pressable> + )} + </Menu.Trigger> + <Menu.Outer showCancel> + <Menu.Group> + {isNative && ( + <Menu.Item + testID="changeBannerCameraBtn" + label={_(msg`Upload from Camera`)} + onPress={onOpenCamera}> + <Menu.ItemText> + <Trans>Upload from Camera</Trans> + </Menu.ItemText> + <Menu.ItemIcon icon={CameraIcon} /> + </Menu.Item> )} - <View style={[styles.editButtonContainer, pal.btn]}> - <CameraFilled height={14} width={14} style={t.atoms.text} /> - </View> - </Pressable> - )} - </Menu.Trigger> - <Menu.Outer showCancel> - <Menu.Group> - {isNative && ( + <Menu.Item - testID="changeBannerCameraBtn" - label={_(msg`Upload from Camera`)} - onPress={onOpenCamera}> + testID="changeBannerLibraryBtn" + label={_(msg`Upload from Library`)} + onPress={onOpenLibrary}> <Menu.ItemText> - <Trans>Upload from Camera</Trans> + {isNative ? ( + <Trans>Upload from Library</Trans> + ) : ( + <Trans>Upload from Files</Trans> + )} </Menu.ItemText> - <Menu.ItemIcon icon={Camera} /> + <Menu.ItemIcon icon={LibraryIcon} /> </Menu.Item> + </Menu.Group> + {!!banner && ( + <> + <Menu.Divider /> + <Menu.Group> + <Menu.Item + testID="changeBannerRemoveBtn" + label={_(msg`Remove Banner`)} + onPress={onRemoveBanner}> + <Menu.ItemText> + <Trans>Remove Banner</Trans> + </Menu.ItemText> + <Menu.ItemIcon icon={TrashIcon} /> + </Menu.Item> + </Menu.Group> + </> )} + </Menu.Outer> + </Menu.Root> + </EventStopper> - <Menu.Item - testID="changeBannerLibraryBtn" - label={_(msg`Upload from Library`)} - onPress={onOpenLibrary}> - <Menu.ItemText> - {isNative ? ( - <Trans>Upload from Library</Trans> - ) : ( - <Trans>Upload from Files</Trans> - )} - </Menu.ItemText> - <Menu.ItemIcon icon={Library} /> - </Menu.Item> - </Menu.Group> - {!!banner && ( - <> - <Menu.Divider /> - <Menu.Group> - <Menu.Item - testID="changeBannerRemoveBtn" - label={_(msg`Remove Banner`)} - onPress={onRemoveBanner}> - <Menu.ItemText> - <Trans>Remove Banner</Trans> - </Menu.ItemText> - <Menu.ItemIcon icon={Trash} /> - </Menu.Item> - </Menu.Group> - </> - )} - </Menu.Outer> - </Menu.Root> - </EventStopper> + <EditImageDialog + control={editImageDialogControl} + image={rawImage} + onChange={onChangeEditImage} + aspectRatio={3} + /> + </> ) : banner && !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( <Image testID="userBannerImage" - style={[ - styles.bannerImage, - {backgroundColor: theme.palette.default.backgroundLight}, - ]} + style={[styles.bannerImage, t.atoms.bg_contrast_25]} contentFit="cover" source={{uri: banner}} blurRadius={moderation?.blur ? 100 : 0} @@ -197,7 +236,6 @@ const styles = StyleSheet.create({ borderRadius: 12, alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.gray5, }, bannerImage: { width: '100%', |