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 */) ? (