Done profile editing

This commit is contained in:
Zhiyuan Zheng 2021-05-17 23:09:50 +02:00
parent 5bb77d0114
commit fd1a6b3415
10 changed files with 273 additions and 141 deletions

View File

@ -69,7 +69,7 @@ const apiGeneral = async <T = unknown>({
error.response.status, error.response.status,
error.response.data.error error.response.data.error
) )
return Promise.reject(error.response) return Promise.reject(error.response.data.error)
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of

View File

@ -98,7 +98,7 @@ const apiInstance = async <T = unknown>({
error.response.status, error.response.status,
error.response.data.error error.response.data.error
) )
return Promise.reject(error.response) return Promise.reject(error.response.data.error)
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of

View File

@ -76,84 +76,86 @@ const MenuRow: React.FC<Props> = ({
} }
}} }}
> >
<View style={styles.core}> <View>
<View style={styles.front}> <View style={styles.core}>
{iconFront && ( <View style={styles.front}>
<Icon {iconFront && (
name={iconFront}
size={StyleConstants.Font.Size.L}
color={theme[iconFrontColor]}
style={styles.iconFront}
/>
)}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
) : null}
<View style={styles.main}>
<Text
style={[styles.title, { color: theme.primaryDefault }]}
numberOfLines={1}
>
{title}
</Text>
</View>
</View>
{content || switchValue !== undefined || iconBack ? (
<View style={styles.back}>
{content ? (
typeof content === 'string' ? (
<Text
style={[
styles.content,
{
color: theme.secondary,
opacity: !iconBack && loading ? 0 : 1
}
]}
numberOfLines={1}
>
{content}
</Text>
) : (
content
)
) : null}
{switchValue !== undefined ? (
<Switch
value={switchValue}
onValueChange={switchOnValueChange}
disabled={switchDisabled}
trackColor={{ true: theme.blue, false: theme.disabled }}
style={{ opacity: loading ? 0 : 1 }}
/>
) : null}
{iconBack ? (
<Icon <Icon
name={iconBack} name={iconFront}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]} color={theme[iconFrontColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]} style={styles.iconFront}
/>
)}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/> />
) : null} ) : null}
{loading && loadingSpinkit} <View style={styles.main}>
<Text
style={[styles.title, { color: theme.primaryDefault }]}
numberOfLines={1}
>
{title}
</Text>
</View>
</View> </View>
{content || switchValue !== undefined || iconBack ? (
<View style={styles.back}>
{content ? (
typeof content === 'string' ? (
<Text
style={[
styles.content,
{
color: theme.secondary,
opacity: !iconBack && loading ? 0 : 1
}
]}
numberOfLines={1}
>
{content}
</Text>
) : (
content
)
) : null}
{switchValue !== undefined ? (
<Switch
value={switchValue}
onValueChange={switchOnValueChange}
disabled={switchDisabled}
trackColor={{ true: theme.blue, false: theme.disabled }}
style={{ opacity: loading ? 0 : 1 }}
/>
) : null}
{iconBack ? (
<Icon
name={iconBack}
size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
/>
) : null}
{loading && loadingSpinkit}
</View>
) : null}
</View>
{description ? (
<Text style={[styles.description, { color: theme.secondary }]}>
{description}
</Text>
) : null} ) : null}
</View> </View>
</TapGestureHandler> </TapGestureHandler>
{description ? (
<Text style={[styles.description, { color: theme.secondary }]}>
{description}
</Text>
) : null}
</View> </View>
) )
} }

View File

@ -102,11 +102,11 @@
}, },
"avatar": { "avatar": {
"title": "Avatar", "title": "Avatar",
"description": "Available in next version" "description": "Will be downscaled to 400x400px"
}, },
"banner": { "header": {
"title": "Banner", "title": "Banner",
"description": "Available in next version" "description": "Will be downscaled to 1500x500px"
}, },
"note": { "note": {
"title": "Description" "title": "Description"

View File

@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
> >
<Stack.Screen <Stack.Screen
name='Tab-Me-Profile-Root' name='Tab-Me-Profile-Root'
component={TabMeProfileRoot}
options={{ options={{
headerTitle: t('me.stacks.profile.name'), headerTitle: t('me.stacks.profile.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -45,7 +44,15 @@ const TabMeProfile: React.FC<StackScreenProps<
/> />
) )
}} }}
/> >
{({ route, navigation }) => (
<TabMeProfileRoot
messageRef={messageRef}
route={route}
navigation={navigation}
/>
)}
</Stack.Screen>
<Stack.Screen <Stack.Screen
name='Tab-Me-Profile-Name' name='Tab-Me-Profile-Name'
options={{ options={{

View File

@ -95,12 +95,13 @@ const TabMeProfileFields: React.FC<StackScreenProps<
type: 'success' type: 'success'
}) })
}) })
.catch(() => { .catch(err => {
displayMessage({ displayMessage({
ref: messageRef, ref: messageRef,
message: t('me.profile.feedback.failed', { message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title') type: t('me.profile.root.note.title')
}), }),
...(err && { description: err }),
mode, mode,
type: 'error' type: 'error'
}) })

View File

@ -77,12 +77,13 @@ const TabMeProfileName: React.FC<StackScreenProps<
type: 'success' type: 'success'
}) })
}) })
.catch(() => { .catch(err => {
displayMessage({ displayMessage({
ref: messageRef, ref: messageRef,
message: t('me.profile.feedback.failed', { message: t('me.profile.feedback.failed', {
type: t('me.profile.root.name.title') type: t('me.profile.root.name.title')
}), }),
...(err && { description: err }),
mode, mode,
type: 'error' type: 'error'
}) })

View File

@ -77,12 +77,13 @@ const TabMeProfileNote: React.FC<StackScreenProps<
type: 'success' type: 'success'
}) })
}) })
.catch(() => { .catch(err => {
displayMessage({ displayMessage({
ref: messageRef, ref: messageRef,
message: t('me.profile.feedback.failed', { message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title') type: t('me.profile.root.note.title')
}), }),
...(err && { description: err }),
mode, mode,
type: 'error' type: 'error'
}) })

View File

@ -1,24 +1,26 @@
import GracefullyImage from '@components/GracefullyImage'
import mediaSelector from '@components/mediaSelector'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet' import { useActionSheet } from '@expo/react-native-action-sheet'
import { StackScreenProps } from '@react-navigation/stack' import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile' import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import * as ImagePicker from 'expo-image-picker' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { RefObject, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import ProfileAvatarHeader from './Root/AvatarHeader'
const TabMeProfileRoot: React.FC<StackScreenProps< const TabMeProfileRoot: React.FC<StackScreenProps<
Nav.TabMeProfileStackParamList, Nav.TabMeProfileStackParamList,
'Tab-Me-Profile-Root' 'Tab-Me-Profile-Root'
>> = ({ navigation }) => { > & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { data, isLoading } = useProfileQuery({}) const { data, isLoading } = useProfileQuery({})
const { mutate } = useProfileMutation() const { mutateAsync } = useProfileMutation()
const onPressVisibility = useCallback(() => { const onPressVisibility = useCallback(() => {
showActionSheetWithOptions( showActionSheetWithOptions(
@ -35,13 +37,46 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
async buttonIndex => { async buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
mutate({ type: 'source[privacy]', data: 'public' }) mutateAsync({ type: 'source[privacy]', data: 'public' }).catch(
err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.visibility.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
break break
case 1: case 1:
mutate({ type: 'source[privacy]', data: 'unlisted' }) mutateAsync({ type: 'source[privacy]', data: 'unlisted' }).catch(
err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.visibility.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
break break
case 2: case 2:
mutate({ type: 'source[privacy]', data: 'private' }) mutateAsync({ type: 'source[privacy]', data: 'private' }).catch(
err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.visibility.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
break break
} }
} }
@ -50,25 +85,88 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
const onPressSensitive = useCallback(() => { const onPressSensitive = useCallback(() => {
if (data?.source.sensitive === undefined) { if (data?.source.sensitive === undefined) {
mutate({ type: 'source[sensitive]', data: true }) mutateAsync({ type: 'source[sensitive]', data: true }).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.sensitive.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} else { } else {
mutate({ type: 'source[sensitive]', data: !data.source.sensitive }) mutateAsync({
type: 'source[sensitive]',
data: !data.source.sensitive
}).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.sensitive.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} }
}, [data?.source.sensitive]) }, [data?.source.sensitive])
const onPressLock = useCallback(() => { const onPressLock = useCallback(() => {
if (data?.locked === undefined) { if (data?.locked === undefined) {
mutate({ type: 'locked', data: true }) mutateAsync({ type: 'locked', data: true }).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.lock.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} else { } else {
mutate({ type: 'locked', data: !data.locked }) mutateAsync({ type: 'locked', data: !data.locked }).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.lock.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} }
}, [data?.locked]) }, [data?.locked])
const onPressBot = useCallback(() => { const onPressBot = useCallback(() => {
if (data?.bot === undefined) { if (data?.bot === undefined) {
mutate({ type: 'bot', data: true }) mutateAsync({ type: 'bot', data: true }).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.bot.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} else { } else {
mutate({ type: 'bot', data: !data?.bot }) mutateAsync({ type: 'bot', data: !data?.bot }).catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.bot.title')
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
} }
}, [data?.bot]) }, [data?.bot])
@ -87,52 +185,8 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
}) })
}} }}
/> />
<MenuRow <ProfileAvatarHeader type='avatar' messageRef={messageRef} />
title={t('me.profile.root.avatar.title')} <ProfileAvatarHeader type='header' messageRef={messageRef} />
description={t('me.profile.root.avatar.description')}
content={
<GracefullyImage
key={data?.avatar_static}
style={{ flex: 1 }}
uri={{
original: data?.avatar_static
}}
/>
}
loading={isLoading}
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 400, height: 400 }
})
mutate({ type: 'avatar', data: image.uri })
}}
/>
<MenuRow
title={t('me.profile.root.banner.title')}
description={t('me.profile.root.banner.description')}
content={
<GracefullyImage
key={data?.header_static}
style={{ flex: 1 }}
uri={{
original: data?.header_static
}}
/>
}
loading={isLoading}
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 1500, height: 500 }
})
mutate({ type: 'header', data: image.uri })
}}
/>
<MenuRow <MenuRow
title={t('me.profile.root.note.title')} title={t('me.profile.root.note.title')}
content={data?.source.note} content={data?.source.note}

View File

@ -0,0 +1,66 @@
import mediaSelector from '@components/mediaSelector'
import { MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { useTheme } from '@utils/styles/ThemeManager'
import * as ImagePicker from 'expo-image-picker'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
export interface Props {
type: 'avatar' | 'header'
messageRef: RefObject<FlashMessage>
}
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
const query = useProfileQuery({})
const mutation = useProfileMutation()
return (
<MenuRow
title={t(`me.profile.root.${type}.title`)}
description={t(`me.profile.root.${type}.description`)}
loading={query.isLoading || mutation.isLoading}
iconBack='ChevronRight'
onPress={async () => {
const image = await mediaSelector({
showActionSheetWithOptions,
mediaTypes: ImagePicker.MediaTypeOptions.Images,
resize: { width: 400, height: 400 }
})
mutation
.mutateAsync({ type, data: image.uri })
.then(() =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.succeed', {
type: t(`me.profile.root.${type}.title`)
}),
mode,
type: 'success'
})
)
.catch(err =>
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t(`me.profile.root.${type}.title`)
}),
...(err && { description: err }),
mode,
type: 'error'
})
)
}}
/>
)
}
export default ProfileAvatarHeader