Edit profile dialog ALF refresh (#5633)
This commit is contained in:
+15
-6
@@ -1,6 +1,7 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewStructure
|
||||
@@ -17,7 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
import java.util.ArrayList
|
||||
|
||||
|
||||
class BottomSheetView(
|
||||
context: Context,
|
||||
@@ -58,17 +59,18 @@ class BottomSheetView(
|
||||
if (value < 0) {
|
||||
0f
|
||||
} else {
|
||||
value
|
||||
dpToPx(value)
|
||||
}
|
||||
}
|
||||
|
||||
var maxHeight = this.screenHeight
|
||||
set(value) {
|
||||
val px = dpToPx(value)
|
||||
field =
|
||||
if (value > this.screenHeight) {
|
||||
this.screenHeight.toFloat()
|
||||
if (px > this.screenHeight) {
|
||||
this.screenHeight
|
||||
} else {
|
||||
value
|
||||
px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ class BottomSheetView(
|
||||
behavior.isDraggable = true
|
||||
behavior.isHideable = true
|
||||
|
||||
if (contentHeight > this.screenHeight) {
|
||||
if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
this.selectedSnapPoint = 2
|
||||
} else {
|
||||
@@ -332,4 +334,11 @@ class BottomSheetView(
|
||||
override fun addChildrenForAccessibility(outChildren: ArrayList<View>?) { }
|
||||
|
||||
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean = false
|
||||
|
||||
// https://stackoverflow.com/questions/11862391/getheight-px-or-dpi
|
||||
fun dpToPx(dp: Float): Float {
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
val px = dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)
|
||||
return px
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -280,7 +280,8 @@
|
||||
"**/zod": "3.23.8",
|
||||
"**/expo-constants": "16.0.1",
|
||||
"**/expo-device": "6.0.2",
|
||||
"@react-native/babel-preset": "0.74.1"
|
||||
"@react-native/babel-preset": "0.74.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/shared'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
// @ts-ignore
|
||||
@@ -169,25 +170,31 @@ export function Outer({
|
||||
)
|
||||
}
|
||||
|
||||
export function Inner({children, style}: DialogInnerProps) {
|
||||
export function Inner({children, style, header}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
{
|
||||
paddingBottom: insets.bottom + insets.top,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
<>
|
||||
{header}
|
||||
<View
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
{
|
||||
paddingBottom: insets.bottom + insets.top,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
function ScrollableInner({children, style, ...props}, ref) {
|
||||
function ScrollableInner(
|
||||
{children, style, contentContainerStyle, header, ...props},
|
||||
ref,
|
||||
) {
|
||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||
const insets = useSafeAreaInsets()
|
||||
const {setEnabled} = useKeyboardController()
|
||||
@@ -232,14 +239,21 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
style={[style]}
|
||||
contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
|
||||
contentContainerStyle={[
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
{paddingBottom},
|
||||
contentContainerStyle,
|
||||
]}
|
||||
ref={ref}
|
||||
{...props}
|
||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||
bottomOffset={30}
|
||||
scrollEventThrottle={50}
|
||||
onScroll={isAndroid ? onScroll : undefined}
|
||||
keyboardShouldPersistTaps="handled">
|
||||
keyboardShouldPersistTaps="handled"
|
||||
stickyHeaderIndices={header ? [0] : undefined}>
|
||||
{header}
|
||||
{children}
|
||||
</KeyboardAwareScrollView>
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||
export * from '#/components/Dialog/shared'
|
||||
export * from '#/components/Dialog/types'
|
||||
export * from '#/components/Dialog/utils'
|
||||
export {Input} from '#/components/forms/TextField'
|
||||
@@ -154,6 +155,8 @@ export function Inner({
|
||||
label,
|
||||
accessibilityLabelledBy,
|
||||
accessibilityDescribedBy,
|
||||
header,
|
||||
contentContainerStyle,
|
||||
}: DialogInnerProps) {
|
||||
const t = useTheme()
|
||||
const {close} = React.useContext(Context)
|
||||
@@ -178,7 +181,6 @@ export function Inner({
|
||||
a.rounded_md,
|
||||
a.w_full,
|
||||
a.border,
|
||||
gtMobile ? a.p_2xl : a.p_xl,
|
||||
t.atoms.bg,
|
||||
{
|
||||
maxWidth: 600,
|
||||
@@ -194,7 +196,10 @@ export function Inner({
|
||||
onFocusOutside={preventDefault}
|
||||
onDismiss={close}
|
||||
style={{display: 'flex', flexDirection: 'column'}}>
|
||||
{children}
|
||||
{header}
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
|
||||
{children}
|
||||
</View>
|
||||
</DismissableLayer>
|
||||
</Animated.View>
|
||||
</FocusScope>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Header({
|
||||
renderLeft,
|
||||
renderRight,
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
renderLeft?: () => React.ReactNode
|
||||
renderRight?: () => React.ReactNode
|
||||
children?: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.py_sm,
|
||||
a.flex_row,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
{minHeight: 50},
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_medium,
|
||||
t.atoms.bg,
|
||||
web([
|
||||
{borderRadiusTopLeft: a.rounded_md.borderRadius},
|
||||
{borderRadiusTopRight: a.rounded_md.borderRadius},
|
||||
]),
|
||||
style,
|
||||
]}>
|
||||
{renderLeft && (
|
||||
<View style={[a.absolute, {left: 6}]}>{renderLeft()}</View>
|
||||
)}
|
||||
{children}
|
||||
{renderRight && (
|
||||
<View style={[a.absolute, {right: 6}]}>{renderRight()}</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function HeaderText({
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
style?: StyleProp<TextStyle>
|
||||
}) {
|
||||
return (
|
||||
<Text style={[a.text_lg, a.text_center, a.font_bold, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import type {
|
||||
GestureResponderEvent,
|
||||
ScrollViewProps,
|
||||
} from 'react-native'
|
||||
import {ViewStyle} from 'react-native'
|
||||
import {StyleProp} from 'react-native'
|
||||
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||
@@ -69,10 +71,14 @@ export type DialogInnerProps =
|
||||
accessibilityLabelledBy: A11yProps['aria-labelledby']
|
||||
accessibilityDescribedBy: string
|
||||
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
header?: React.ReactNode
|
||||
}>
|
||||
| DialogInnerPropsBase<{
|
||||
label: string
|
||||
accessibilityLabelledBy?: undefined
|
||||
accessibilityDescribedBy?: undefined
|
||||
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
header?: React.ReactNode
|
||||
}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {android, atoms as a, useTheme, web} from '#/alf'
|
||||
import {android, atoms as a, TextStyleProp, useTheme, web} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -123,6 +123,11 @@ export function useSharedInputStyles() {
|
||||
|
||||
export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
|
||||
label: string
|
||||
/**
|
||||
* @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible.
|
||||
*
|
||||
* See https://github.com/facebook/react-native-website/pull/4247
|
||||
*/
|
||||
value?: string
|
||||
onChangeText?: (value: string) => void
|
||||
isInvalid?: boolean
|
||||
@@ -308,10 +313,13 @@ export function SuffixText({
|
||||
children,
|
||||
label,
|
||||
accessibilityHint,
|
||||
}: React.PropsWithChildren<{
|
||||
label: string
|
||||
accessibilityHint?: AccessibilityProps['accessibilityHint']
|
||||
}>) {
|
||||
style,
|
||||
}: React.PropsWithChildren<
|
||||
TextStyleProp & {
|
||||
label: string
|
||||
accessibilityHint?: AccessibilityProps['accessibilityHint']
|
||||
}
|
||||
>) {
|
||||
const t = useTheme()
|
||||
const ctx = React.useContext(Context)
|
||||
return (
|
||||
@@ -334,6 +342,7 @@ export function SuffixText({
|
||||
color: t.palette.contrast_800,
|
||||
}
|
||||
: {},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
@@ -41,6 +41,20 @@ export function useEnforceMaxGraphemeCount() {
|
||||
)
|
||||
}
|
||||
|
||||
export function useWarnMaxGraphemeCount({
|
||||
text,
|
||||
maxCount,
|
||||
}: {
|
||||
text: string
|
||||
maxCount: number
|
||||
}) {
|
||||
const splitter = useMemo(() => new Graphemer(), [])
|
||||
|
||||
return useMemo(() => {
|
||||
return splitter.countGraphemes(text) > maxCount
|
||||
}, [splitter, maxCount, text])
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/52171480
|
||||
export function toHashCode(str: string, seed = 0): number {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {Dimensions, View} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {compressIfNeeded} from '#/lib/media/manip'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
const DISPLAY_NAME_MAX_GRAPHEMES = 64
|
||||
const DESCRIPTION_MAX_GRAPHEMES = 256
|
||||
|
||||
const SCREEN_HEIGHT = Dimensions.get('window').height
|
||||
|
||||
export function EditProfileDialog({
|
||||
profile,
|
||||
control,
|
||||
onUpdate,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
control: Dialog.DialogControlProps
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const cancelControl = Dialog.useDialogControl()
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
// 'You might lose unsaved changes' warning
|
||||
useEffect(() => {
|
||||
if (isWeb && dirty) {
|
||||
const abortController = new AbortController()
|
||||
const {signal} = abortController
|
||||
window.addEventListener('beforeunload', evt => evt.preventDefault(), {
|
||||
signal,
|
||||
})
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
}, [dirty])
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
if (dirty) {
|
||||
cancelControl.open()
|
||||
} else {
|
||||
control.close()
|
||||
}
|
||||
}, [dirty, control, cancelControl])
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
nativeOptions={{
|
||||
preventDismiss: dirty,
|
||||
minHeight: SCREEN_HEIGHT,
|
||||
}}>
|
||||
<DialogInner
|
||||
profile={profile}
|
||||
onUpdate={onUpdate}
|
||||
setDirty={setDirty}
|
||||
onPressCancel={onPressCancel}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={cancelControl}
|
||||
title={_(msg`Discard changes?`)}
|
||||
description={_(msg`Are you sure you want to discard your changes?`)}
|
||||
onConfirm={() => control.close()}
|
||||
confirmButtonCta={_(msg`Discard`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
profile,
|
||||
onUpdate,
|
||||
setDirty,
|
||||
onPressCancel,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
setDirty: (dirty: boolean) => void
|
||||
onPressCancel: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {
|
||||
mutateAsync: updateProfileMutation,
|
||||
error: updateProfileError,
|
||||
isError: isUpdateProfileError,
|
||||
isPending: isUpdatingProfile,
|
||||
} = useProfileUpdateMutation()
|
||||
const [imageError, setImageError] = useState('')
|
||||
const initialDisplayName = profile.displayName || ''
|
||||
const [displayName, setDisplayName] = useState(initialDisplayName)
|
||||
const initialDescription = profile.description || ''
|
||||
const [description, setDescription] = useState(initialDescription)
|
||||
const [userBanner, setUserBanner] = useState<string | undefined | null>(
|
||||
profile.banner,
|
||||
)
|
||||
const [userAvatar, setUserAvatar] = useState<string | undefined | null>(
|
||||
profile.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
|
||||
const dirty =
|
||||
displayName !== initialDisplayName ||
|
||||
description !== initialDescription ||
|
||||
userAvatar !== profile.avatar ||
|
||||
userBanner !== profile.banner
|
||||
|
||||
useEffect(() => {
|
||||
setDirty(dirty)
|
||||
}, [dirty, setDirty])
|
||||
|
||||
const onSelectNewAvatar = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (img === null) {
|
||||
setNewUserAvatar(null)
|
||||
setUserAvatar(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserAvatar(finalImg)
|
||||
setUserAvatar(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserAvatar, setUserAvatar, setImageError],
|
||||
)
|
||||
|
||||
const onSelectNewBanner = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (!img) {
|
||||
setNewUserBanner(null)
|
||||
setUserBanner(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserBanner(finalImg)
|
||||
setUserBanner(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserBanner, setUserBanner, setImageError],
|
||||
)
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
setImageError('')
|
||||
try {
|
||||
await updateProfileMutation({
|
||||
profile,
|
||||
updates: {
|
||||
displayName: displayName.trimEnd(),
|
||||
description: description.trimEnd(),
|
||||
},
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
})
|
||||
onUpdate?.()
|
||||
control.close()
|
||||
Toast.show(_(msg`Profile updated`))
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to update user profile', {message: String(e)})
|
||||
}
|
||||
}, [
|
||||
updateProfileMutation,
|
||||
profile,
|
||||
onUpdate,
|
||||
control,
|
||||
displayName,
|
||||
description,
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
setImageError,
|
||||
_,
|
||||
])
|
||||
|
||||
const displayNameTooLong = useWarnMaxGraphemeCount({
|
||||
text: displayName,
|
||||
maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
|
||||
})
|
||||
const descriptionTooLong = useWarnMaxGraphemeCount({
|
||||
text: description,
|
||||
maxCount: DESCRIPTION_MAX_GRAPHEMES,
|
||||
})
|
||||
|
||||
const cancelButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
onPress={onPressCancel}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
),
|
||||
[onPressCancel, _],
|
||||
)
|
||||
|
||||
const saveButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
label={_(msg`Save`)}
|
||||
onPress={onPressSave}
|
||||
disabled={
|
||||
!dirty ||
|
||||
isUpdatingProfile ||
|
||||
displayNameTooLong ||
|
||||
descriptionTooLong
|
||||
}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText style={[a.text_md, !dirty && t.atoms.text_contrast_low]}>
|
||||
<Trans>Save</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
),
|
||||
[
|
||||
_,
|
||||
t,
|
||||
dirty,
|
||||
onPressSave,
|
||||
isUpdatingProfile,
|
||||
displayNameTooLong,
|
||||
descriptionTooLong,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Edit profile`)}
|
||||
style={[a.overflow_hidden]}
|
||||
contentContainerStyle={[a.px_0, a.pt_0]}
|
||||
header={
|
||||
<Dialog.Header renderLeft={cancelButton} renderRight={saveButton}>
|
||||
<Dialog.HeaderText>
|
||||
<Trans>Edit profile</Trans>
|
||||
</Dialog.HeaderText>
|
||||
</Dialog.Header>
|
||||
}>
|
||||
<View style={[a.relative]}>
|
||||
<UserBanner banner={userBanner} onSelectNewBanner={onSelectNewBanner} />
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
top: 80,
|
||||
left: 20,
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderWidth: 2,
|
||||
borderRadius: 42,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
},
|
||||
]}>
|
||||
<EditableUserAvatar
|
||||
size={80}
|
||||
avatar={userAvatar}
|
||||
onSelectNewAvatar={onSelectNewAvatar}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{isUpdateProfileError && (
|
||||
<View style={[a.mt_xl]}>
|
||||
<ErrorMessage message={cleanError(updateProfileError)} />
|
||||
</View>
|
||||
)}
|
||||
{imageError !== '' && (
|
||||
<View style={[a.mt_xl]}>
|
||||
<ErrorMessage message={imageError} />
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.mt_4xl, a.px_xl, a.gap_xl]}>
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<Trans>Display name</Trans>
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={displayNameTooLong}>
|
||||
<Dialog.Input
|
||||
defaultValue={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
label={_(msg`Display name`)}
|
||||
placeholder={_(msg`e.g. Alice Lastname`)}
|
||||
/>
|
||||
</TextField.Root>
|
||||
{displayNameTooLong && (
|
||||
<TextField.SuffixText
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.mt_xs,
|
||||
a.font_bold,
|
||||
{color: t.palette.negative_400},
|
||||
]}
|
||||
label={_(msg`Display name is too long`)}>
|
||||
<Trans>
|
||||
Display name is too long. The maximum number of characters is{' '}
|
||||
{DISPLAY_NAME_MAX_GRAPHEMES}.
|
||||
</Trans>
|
||||
</TextField.SuffixText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<TextField.LabelText>
|
||||
<Trans>Description</Trans>
|
||||
</TextField.LabelText>
|
||||
<TextField.Root isInvalid={descriptionTooLong}>
|
||||
<Dialog.Input
|
||||
defaultValue={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
label={_(msg`Display name`)}
|
||||
placeholder={_(msg`Tell us a bit about yourself`)}
|
||||
/>
|
||||
</TextField.Root>
|
||||
{descriptionTooLong && (
|
||||
<TextField.SuffixText
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.mt_xs,
|
||||
a.font_bold,
|
||||
{color: t.palette.negative_400},
|
||||
]}
|
||||
label={_(msg`Description is too long`)}>
|
||||
<Trans>
|
||||
Description is too long. The maximum number of characters is{' '}
|
||||
{DESCRIPTION_MAX_GRAPHEMES}.
|
||||
</Trans>
|
||||
</TextField.SuffixText>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
|
||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
@@ -27,7 +26,7 @@ import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {DialogOuterProps} from '#/components/Dialog'
|
||||
import {DialogOuterProps, useDialogControl} from '#/components/Dialog'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
|
||||
Heart2_Stroke2_Corner0_Rounded as Heart,
|
||||
@@ -37,6 +36,7 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||
import {EditProfileDialog} from './EditProfileDialog'
|
||||
import {ProfileHeaderHandle} from './Handle'
|
||||
import {ProfileHeaderMetrics} from './Metrics'
|
||||
import {ProfileHeaderShell} from './Shell'
|
||||
@@ -63,7 +63,6 @@ let ProfileHeaderLabeler = ({
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const {openModal} = useModalControls()
|
||||
const requireAuth = useRequireAuth()
|
||||
const playHaptic = useHaptics()
|
||||
const cantSubscribePrompt = Prompt.usePromptControl()
|
||||
@@ -117,12 +116,10 @@ let ProfileHeaderLabeler = ({
|
||||
}
|
||||
}, [labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
|
||||
|
||||
const editProfileControl = useDialogControl()
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'edit-profile',
|
||||
profile,
|
||||
})
|
||||
}, [openModal, profile])
|
||||
editProfileControl.open()
|
||||
}, [editProfileControl])
|
||||
|
||||
const onPressSubscribe = React.useCallback(
|
||||
() =>
|
||||
@@ -169,18 +166,24 @@ let ProfileHeaderLabeler = ({
|
||||
style={[a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
{isMe ? (
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={onPressEditProfile}
|
||||
label={_(msg`Edit profile`)}
|
||||
style={a.rounded_full}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={onPressEditProfile}
|
||||
label={_(msg`Edit profile`)}
|
||||
style={a.rounded_full}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<EditProfileDialog
|
||||
profile={profile}
|
||||
control={editProfileControl}
|
||||
/>
|
||||
</>
|
||||
) : !isAppLabeler(profile.did) ? (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -14,7 +14,6 @@ import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileFollowMutationQueue,
|
||||
@@ -24,6 +23,7 @@ import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||
import {EditProfileDialog} from './EditProfileDialog'
|
||||
import {ProfileHeaderHandle} from './Handle'
|
||||
import {ProfileHeaderMetrics} from './Metrics'
|
||||
import {ProfileHeaderShell} from './Shell'
|
||||
@@ -57,7 +58,6 @@ let ProfileHeaderStandard = ({
|
||||
useProfileShadow(profileUnshadowed)
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const moderation = useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
@@ -74,12 +74,10 @@ let ProfileHeaderStandard = ({
|
||||
profile.viewer?.blockedBy ||
|
||||
profile.viewer?.blockingByList
|
||||
|
||||
const editProfileControl = useDialogControl()
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'edit-profile',
|
||||
profile,
|
||||
})
|
||||
}, [openModal, profile])
|
||||
editProfileControl.open()
|
||||
}, [editProfileControl])
|
||||
|
||||
const onPressFollow = () => {
|
||||
requireAuth(async () => {
|
||||
@@ -161,18 +159,24 @@ let ProfileHeaderStandard = ({
|
||||
]}
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}>
|
||||
{isMe ? (
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={onPressEditProfile}
|
||||
label={_(msg`Edit profile`)}
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
testID="profileHeaderEditProfileButton"
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
onPress={onPressEditProfile}
|
||||
label={_(msg`Edit profile`)}
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Edit Profile</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<EditProfileDialog
|
||||
profile={profile}
|
||||
control={editProfileControl}
|
||||
/>
|
||||
</>
|
||||
) : profile.viewer?.blocking ? (
|
||||
profile.viewer?.blockingByList ? null : (
|
||||
<Button
|
||||
|
||||
@@ -4,12 +4,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
|
||||
export interface EditProfileModal {
|
||||
name: 'edit-profile'
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
export interface CreateOrEditListModal {
|
||||
name: 'create-or-edit-list'
|
||||
purpose?: string
|
||||
@@ -111,7 +105,6 @@ export type Modal =
|
||||
| AddAppPasswordModal
|
||||
| ChangeHandleModal
|
||||
| DeleteAccountModal
|
||||
| EditProfileModal
|
||||
| VerifyEmailModal
|
||||
| ChangeEmailModal
|
||||
| ChangePasswordModal
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import Animated, {FadeOut} from 'react-native-reanimated'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {compressIfNeeded} from '#/lib/media/manip'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {colors, gradients, s} from '#/lib/styles'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export function Component({
|
||||
profile,
|
||||
onUpdate,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const updateMutation = useProfileUpdateMutation()
|
||||
const [imageError, setImageError] = useState<string>('')
|
||||
const [displayName, setDisplayName] = useState<string>(
|
||||
profile.displayName || '',
|
||||
)
|
||||
const [description, setDescription] = useState<string>(
|
||||
profile.description || '',
|
||||
)
|
||||
const [userBanner, setUserBanner] = useState<string | undefined | null>(
|
||||
profile.banner,
|
||||
)
|
||||
const [userAvatar, setUserAvatar] = useState<string | undefined | null>(
|
||||
profile.avatar,
|
||||
)
|
||||
const [newUserBanner, setNewUserBanner] = useState<
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
const [newUserAvatar, setNewUserAvatar] = useState<
|
||||
RNImage | undefined | null
|
||||
>()
|
||||
const onPressCancel = () => {
|
||||
closeModal()
|
||||
}
|
||||
const onSelectNewAvatar = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (img === null) {
|
||||
setNewUserAvatar(null)
|
||||
setUserAvatar(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserAvatar(finalImg)
|
||||
setUserAvatar(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserAvatar, setUserAvatar, setImageError],
|
||||
)
|
||||
|
||||
const onSelectNewBanner = useCallback(
|
||||
async (img: RNImage | null) => {
|
||||
setImageError('')
|
||||
if (!img) {
|
||||
setNewUserBanner(null)
|
||||
setUserBanner(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const finalImg = await compressIfNeeded(img, 1000000)
|
||||
setNewUserBanner(finalImg)
|
||||
setUserBanner(finalImg.path)
|
||||
} catch (e: any) {
|
||||
setImageError(cleanError(e))
|
||||
}
|
||||
},
|
||||
[setNewUserBanner, setUserBanner, setImageError],
|
||||
)
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
setImageError('')
|
||||
try {
|
||||
await updateMutation.mutateAsync({
|
||||
profile,
|
||||
updates: {
|
||||
displayName,
|
||||
description,
|
||||
},
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
})
|
||||
Toast.show(_(msg`Profile updated`))
|
||||
onUpdate?.()
|
||||
closeModal()
|
||||
} catch (e: any) {
|
||||
logger.error('Failed to update user profile', {message: String(e)})
|
||||
}
|
||||
}, [
|
||||
updateMutation,
|
||||
profile,
|
||||
onUpdate,
|
||||
closeModal,
|
||||
displayName,
|
||||
description,
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
setImageError,
|
||||
_,
|
||||
])
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={s.flex1} behavior="height">
|
||||
<ScrollView style={[pal.view]} testID="editProfileModal">
|
||||
<Text style={[styles.title, pal.text]}>
|
||||
<Trans>Edit my profile</Trans>
|
||||
</Text>
|
||||
<View style={styles.photos}>
|
||||
<UserBanner
|
||||
banner={userBanner}
|
||||
onSelectNewBanner={onSelectNewBanner}
|
||||
/>
|
||||
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
|
||||
<EditableUserAvatar
|
||||
size={80}
|
||||
avatar={userAvatar}
|
||||
onSelectNewAvatar={onSelectNewAvatar}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{updateMutation.isError && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={cleanError(updateMutation.error)} />
|
||||
</View>
|
||||
)}
|
||||
{imageError !== '' && (
|
||||
<View style={styles.errorContainer}>
|
||||
<ErrorMessage message={imageError} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Display Name</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDisplayNameInput"
|
||||
style={[styles.textInput, pal.border, pal.text]}
|
||||
placeholder={_(msg`e.g. Alice Roberts`)}
|
||||
placeholderTextColor={colors.gray4}
|
||||
value={displayName}
|
||||
onChangeText={v =>
|
||||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||
}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Display name`)}
|
||||
accessibilityHint={_(msg`Edit your display name`)}
|
||||
/>
|
||||
</View>
|
||||
<View style={s.pb10}>
|
||||
<Text style={[styles.label, pal.text]}>
|
||||
<Trans>Description</Trans>
|
||||
</Text>
|
||||
<TextInput
|
||||
testID="editProfileDescriptionInput"
|
||||
style={[styles.textArea, pal.border, pal.text]}
|
||||
placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)}
|
||||
placeholderTextColor={colors.gray4}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
multiline
|
||||
value={description}
|
||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
||||
accessible={true}
|
||||
accessibilityLabel={_(msg`Description`)}
|
||||
accessibilityHint={_(msg`Edit your profile description`)}
|
||||
/>
|
||||
</View>
|
||||
{updateMutation.isPending ? (
|
||||
<View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="editProfileSaveBtn"
|
||||
style={s.mt10}
|
||||
onPress={onPressSave}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Save`)}
|
||||
accessibilityHint={_(msg`Saves any changes to your profile`)}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold]}>
|
||||
<Trans>Save Changes</Trans>
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!updateMutation.isPending && (
|
||||
<AnimatedTouchableOpacity
|
||||
exiting={!isWeb ? FadeOut : undefined}
|
||||
testID="editProfileCancelBtn"
|
||||
style={s.mt5}
|
||||
onPress={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Cancel profile editing`)}
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}>
|
||||
<View style={[styles.btn]}>
|
||||
<Text style={[s.black, s.bold, pal.text]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</AnimatedTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
fontSize: 24,
|
||||
marginBottom: 18,
|
||||
},
|
||||
label: {
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
marginTop: 20,
|
||||
},
|
||||
form: {
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
textInput: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
textArea: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 10,
|
||||
fontSize: 16,
|
||||
height: 120,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
borderRadius: 32,
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
avi: {
|
||||
position: 'absolute',
|
||||
top: 80,
|
||||
left: 24,
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderWidth: 2,
|
||||
borderRadius: 42,
|
||||
},
|
||||
photos: {
|
||||
marginBottom: 36,
|
||||
marginHorizontal: -14,
|
||||
},
|
||||
errorContainer: {marginTop: 20},
|
||||
})
|
||||
@@ -13,7 +13,6 @@ import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as ChangePasswordModal from './ChangePassword'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as InAppBrowserConsentModal from './InAppBrowserConsent'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
@@ -55,10 +54,7 @@ export function ModalsContainer() {
|
||||
|
||||
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
||||
let element
|
||||
if (activeModal?.name === 'edit-profile') {
|
||||
snapPoints = EditProfileModal.snapPoints
|
||||
element = <EditProfileModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'create-or-edit-list') {
|
||||
if (activeModal?.name === 'create-or-edit-list') {
|
||||
snapPoints = CreateOrEditListModal.snapPoints
|
||||
element = <CreateOrEditListModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'user-add-remove-lists') {
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as ChangePasswordModal from './ChangePassword'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
import * as CropImageModal from './CropImage.web'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||
@@ -63,9 +62,7 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||
}
|
||||
|
||||
let element
|
||||
if (modal.name === 'edit-profile') {
|
||||
element = <EditProfileModal.Component {...modal} />
|
||||
} else if (modal.name === 'create-or-edit-list') {
|
||||
if (modal.name === 'create-or-edit-list') {
|
||||
element = <CreateOrEditListModal.Component {...modal} />
|
||||
} else if (modal.name === 'user-add-remove-lists') {
|
||||
element = <UserAddRemoveLists.Component {...modal} />
|
||||
|
||||
@@ -5197,17 +5197,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
|
||||
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
|
||||
|
||||
"@radix-ui/react-focus-scope@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz#faea8c25f537c5a5c38c50914b63722db0e7f951"
|
||||
integrity sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
|
||||
"@radix-ui/react-focus-scope@1.1.0", "@radix-ui/react-focus-scope@^1.1.0":
|
||||
"@radix-ui/react-focus-scope@1.0.1", "@radix-ui/react-focus-scope@1.1.0", "@radix-ui/react-focus-scope@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2"
|
||||
integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==
|
||||
|
||||
Reference in New Issue
Block a user