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.data.error
)
return Promise.reject(error.response)
return Promise.reject(error.response.data.error)
} else if (error.request) {
// The request was made but no response was received
// `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.data.error
)
return Promise.reject(error.response)
return Promise.reject(error.response.data.error)
} else if (error.request) {
// The request was made but no response was received
// `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 style={styles.front}>
{iconFront && (
<Icon
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 ? (
<View>
<View style={styles.core}>
<View style={styles.front}>
{iconFront && (
<Icon
name={iconBack}
name={iconFront}
size={StyleConstants.Font.Size.L}
color={theme[iconBackColor]}
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
color={theme[iconFrontColor]}
style={styles.iconFront}
/>
)}
{badge ? (
<View
style={{
width: 8,
height: 8,
backgroundColor: theme.red,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
) : null}
{loading && loadingSpinkit}
<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
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}
</View>
</TapGestureHandler>
{description ? (
<Text style={[styles.description, { color: theme.secondary }]}>
{description}
</Text>
) : null}
</View>
)
}

View File

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

View File

@ -30,7 +30,6 @@ const TabMeProfile: React.FC<StackScreenProps<
>
<Stack.Screen
name='Tab-Me-Profile-Root'
component={TabMeProfileRoot}
options={{
headerTitle: t('me.stacks.profile.name'),
...(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
name='Tab-Me-Profile-Name'
options={{

View File

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

View File

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

View File

@ -77,12 +77,13 @@ const TabMeProfileNote: React.FC<StackScreenProps<
type: 'success'
})
})
.catch(() => {
.catch(err => {
displayMessage({
ref: messageRef,
message: t('me.profile.feedback.failed', {
type: t('me.profile.root.note.title')
}),
...(err && { description: err }),
mode,
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 { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { StackScreenProps } from '@react-navigation/stack'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import * as ImagePicker from 'expo-image-picker'
import React, { useCallback } from 'react'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler'
import ProfileAvatarHeader from './Root/AvatarHeader'
const TabMeProfileRoot: React.FC<StackScreenProps<
Nav.TabMeProfileStackParamList,
'Tab-Me-Profile-Root'
>> = ({ navigation }) => {
> & { messageRef: RefObject<FlashMessage> }> = ({ messageRef, navigation }) => {
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
const { data, isLoading } = useProfileQuery({})
const { mutate } = useProfileMutation()
const { mutateAsync } = useProfileMutation()
const onPressVisibility = useCallback(() => {
showActionSheetWithOptions(
@ -35,13 +37,46 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
async buttonIndex => {
switch (buttonIndex) {
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
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
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
}
}
@ -50,25 +85,88 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
const onPressSensitive = useCallback(() => {
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 {
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])
const onPressLock = useCallback(() => {
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 {
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])
const onPressBot = useCallback(() => {
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 {
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])
@ -87,52 +185,8 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
})
}}
/>
<MenuRow
title={t('me.profile.root.avatar.title')}
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 })
}}
/>
<ProfileAvatarHeader type='avatar' messageRef={messageRef} />
<ProfileAvatarHeader type='header' messageRef={messageRef} />
<MenuRow
title={t('me.profile.root.note.title')}
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