diff --git a/assets/icons/camera_filled_stroke2_corner0_rounded.svg b/assets/icons/camera_filled_stroke2_corner0_rounded.svg new file mode 100644 index 00000000..fa0101cf --- /dev/null +++ b/assets/icons/camera_filled_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/camera_stroke2_corner0_rounded.svg b/assets/icons/camera_stroke2_corner0_rounded.svg new file mode 100644 index 00000000..ce0c29ae --- /dev/null +++ b/assets/icons/camera_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/streamingLive_stroke2_corner0_rounded.svg b/assets/icons/streamingLive_stroke2_corner0_rounded.svg new file mode 100644 index 00000000..b6cdd34d --- /dev/null +++ b/assets/icons/streamingLive_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index ee96a566..f9b697ea 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -16,6 +16,10 @@ import { ItemTextProps, ItemIconProps, } from '#/components/Menu/types' +import {Button, ButtonText} from '#/components/Button' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {isNative} from 'platform/detection' export {useDialogControl as useMenuControl} from '#/components/Dialog' @@ -68,7 +72,10 @@ export function Trigger({children, label}: TriggerProps) { }) } -export function Outer({children}: React.PropsWithChildren<{}>) { +export function Outer({ + children, + showCancel, +}: React.PropsWithChildren<{showCancel?: boolean}>) { const context = React.useContext(Context) return ( @@ -78,7 +85,10 @@ export function Outer({children}: React.PropsWithChildren<{}>) { {/* Re-wrap with context since Dialogs are portal-ed to root */} - {children} + + {children} + {isNative && showCancel && } + @@ -185,6 +195,22 @@ export function Group({children, style}: GroupProps) { ) } +function Cancel() { + const {_} = useLingui() + const {control} = React.useContext(Context) + + return ( + + ) +} + export function Divider() { return null } diff --git a/src/components/icons/Camera.tsx b/src/components/icons/Camera.tsx new file mode 100644 index 00000000..ced8e744 --- /dev/null +++ b/src/components/icons/Camera.tsx @@ -0,0 +1,9 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Camera_Stroke2_Corner0_Rounded = createSinglePathSVG({ + 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', +}) + +export const Camera_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ + 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', +}) diff --git a/src/components/icons/StreamingLive.tsx b/src/components/icons/StreamingLive.tsx new file mode 100644 index 00000000..8ab5099d --- /dev/null +++ b/src/components/icons/StreamingLive.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const StreamingLive_Stroke2_Corner0_Rounded = createSinglePathSVG({ + 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', +}) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index f673db1e..41323739 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,9 +1,13 @@ import React, {memo, useMemo} from 'react' -import {Image, StyleSheet, View} from 'react-native' +import {Image, StyleSheet, TouchableOpacity, View} from 'react-native' import Svg, {Circle, Rect, Path} from 'react-native-svg' +import {Image as RNImage} from 'react-native-image-crop-picker' +import {useLingui} from '@lingui/react' +import {msg, Trans} from '@lingui/macro' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {HighPriorityImage} from 'view/com/util/images/Image' import {ModerationUI} from '@atproto/api' + +import {HighPriorityImage} from 'view/com/util/images/Image' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' import { usePhotoLibraryPermission, @@ -11,12 +15,16 @@ import { } from 'lib/hooks/usePermissions' import {colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb, isAndroid} from 'platform/detection' -import {Image as RNImage} from 'react-native-image-crop-picker' +import {isWeb, isAndroid, isNative} from 'platform/detection' import {UserPreviewLink} from './UserPreviewLink' -import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' -import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' +import * as Menu from '#/components/Menu' +import { + Camera_Stroke2_Corner0_Rounded as Camera, + Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, +} 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 {useTheme} from '#/alf' export type UserAvatarType = 'user' | 'algo' | 'list' @@ -196,6 +204,7 @@ let EditableUserAvatar = ({ avatar, onSelectNewAvatar, }: EditableUserAvatarProps): React.ReactNode => { + const t = useTheme() const pal = usePalette('default') const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() @@ -216,118 +225,115 @@ let EditableUserAvatar = ({ } }, [type, size]) - const dropdownItems = useMemo( - () => - [ - !isWeb && { - testID: 'changeAvatarCameraBtn', - label: _(msg`Camera`), - icon: { - ios: { - name: 'camera', - }, - android: 'ic_menu_camera', - web: 'camera', - }, - onPress: async () => { - if (!(await requestCameraAccessIfNeeded())) { - return - } + const onOpenCamera = React.useCallback(async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } - onSelectNewAvatar( - await openCamera({ - width: 1000, - height: 1000, - cropperCircleOverlay: true, - }), - ) - }, - }, - { - testID: 'changeAvatarLibraryBtn', - label: _(msg`Library`), - icon: { - ios: { - name: 'photo.on.rectangle.angled', - }, - android: 'ic_menu_gallery', - web: 'gallery', - }, - onPress: async () => { - if (!(await requestPhotoAccessIfNeeded())) { - return - } + onSelectNewAvatar( + await openCamera({ + width: 1000, + height: 1000, + cropperCircleOverlay: true, + }), + ) + }, [onSelectNewAvatar, requestCameraAccessIfNeeded]) - const items = await openPicker({ - aspect: [1, 1], - }) - const item = items[0] - if (!item) { - return - } + const onOpenLibrary = React.useCallback(async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } - const croppedImage = await openCropper({ - mediaType: 'photo', - cropperCircleOverlay: true, - height: item.height, - width: item.width, - path: item.path, - }) + const items = await openPicker({ + aspect: [1, 1], + }) + const item = items[0] + if (!item) { + return + } - onSelectNewAvatar(croppedImage) - }, - }, - !!avatar && { - label: 'separator', - }, - !!avatar && { - testID: 'changeAvatarRemoveBtn', - label: _(msg`Remove`), - icon: { - ios: { - name: 'trash', - }, - android: 'ic_delete', - web: ['far', 'trash-can'], - }, - onPress: async () => { - onSelectNewAvatar(null) - }, - }, - ].filter(Boolean) as DropdownItem[], - [ - avatar, - onSelectNewAvatar, - requestCameraAccessIfNeeded, - requestPhotoAccessIfNeeded, - _, - ], - ) + const croppedImage = await openCropper({ + mediaType: 'photo', + cropperCircleOverlay: true, + height: item.height, + width: item.width, + path: item.path, + }) + + onSelectNewAvatar(croppedImage) + }, [onSelectNewAvatar, requestPhotoAccessIfNeeded]) + + const onRemoveAvatar = React.useCallback(() => { + onSelectNewAvatar(null) + }, [onSelectNewAvatar]) return ( - - {avatar ? ( - - ) : ( - - )} - - - - + + + {({props}) => ( + + {avatar ? ( + + ) : ( + + )} + + + + + )} + + + + {isNative && ( + + + Upload from Camera + + + + )} + + + + {isNative ? ( + Upload from Library + ) : ( + Upload from Files + )} + + + + + {!!avatar && ( + <> + + + + + Remove Avatar + + + + + + )} + + ) } EditableUserAvatar = memo(EditableUserAvatar) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index cb47b665..a5ddfee8 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -1,21 +1,29 @@ -import React, {useMemo} from 'react' -import {StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {ModerationUI} from '@atproto/api' import {Image} from 'expo-image' import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' + import {colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' +import {useTheme as useAlfTheme} from '#/alf' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' import { usePhotoLibraryPermission, useCameraPermission, } from 'lib/hooks/usePermissions' import {usePalette} from 'lib/hooks/usePalette' -import {isWeb, isAndroid} from 'platform/detection' +import {isAndroid, isNative} from 'platform/detection' import {Image as RNImage} from 'react-native-image-crop-picker' -import {NativeDropdown, DropdownItem} from './forms/NativeDropdown' +import {EventStopper} from 'view/com/util/EventStopper' +import * as Menu from '#/components/Menu' +import { + Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, + Camera_Stroke2_Corner0_Rounded as Camera, +} 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' export function UserBanner({ banner, @@ -28,118 +36,120 @@ export function UserBanner({ }) { const pal = usePalette('default') const theme = useTheme() + const t = useAlfTheme() const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission() - const dropdownItems: DropdownItem[] = useMemo( - () => - [ - !isWeb && { - testID: 'changeBannerCameraBtn', - label: _(msg`Camera`), - icon: { - ios: { - name: 'camera', - }, - android: 'ic_menu_camera', - web: 'camera', - }, - onPress: async () => { - if (!(await requestCameraAccessIfNeeded())) { - return - } - onSelectNewBanner?.( - await openCamera({ - width: 3000, - height: 1000, - }), - ) - }, - }, - { - testID: 'changeBannerLibraryBtn', - label: _(msg`Library`), - icon: { - ios: { - name: 'photo.on.rectangle.angled', - }, - android: 'ic_menu_gallery', - web: 'gallery', - }, - onPress: async () => { - if (!(await requestPhotoAccessIfNeeded())) { - return - } - const items = await openPicker() - if (!items[0]) { - return - } + const onOpenCamera = React.useCallback(async () => { + if (!(await requestCameraAccessIfNeeded())) { + return + } + onSelectNewBanner?.( + await openCamera({ + width: 3000, + height: 1000, + }), + ) + }, [onSelectNewBanner, requestCameraAccessIfNeeded]) - onSelectNewBanner?.( - await openCropper({ - mediaType: 'photo', - path: items[0].path, - width: 3000, - height: 1000, - }), - ) - }, - }, - !!banner && { - testID: 'changeBannerRemoveBtn', - label: _(msg`Remove`), - icon: { - ios: { - name: 'trash', - }, - android: 'ic_delete', - web: ['far', 'trash-can'], - }, - onPress: () => { - onSelectNewBanner?.(null) - }, - }, - ].filter(Boolean) as DropdownItem[], - [ - banner, - onSelectNewBanner, - requestCameraAccessIfNeeded, - requestPhotoAccessIfNeeded, - _, - ], - ) + const onOpenLibrary = React.useCallback(async () => { + if (!(await requestPhotoAccessIfNeeded())) { + return + } + const items = await openPicker() + if (!items[0]) { + return + } + + onSelectNewBanner?.( + await openCropper({ + mediaType: 'photo', + path: items[0].path, + width: 3000, + height: 1000, + }), + ) + }, [onSelectNewBanner, requestPhotoAccessIfNeeded]) + + const onRemoveBanner = React.useCallback(() => { + onSelectNewBanner?.(null) + }, [onSelectNewBanner]) // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( - - {banner ? ( - - ) : ( - - )} - - - - + + + + {({props}) => ( + + {banner ? ( + + ) : ( + + )} + + + + + )} + + + + {isNative && ( + + + Upload from Camera + + + + )} + + + + {isNative ? ( + Upload from Library + ) : ( + Upload from Files + )} + + + + + {!!banner && ( + <> + + + + + Remove Banner + + + + + + )} + + + ) : banner && !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (