mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Rewrite emoji component logic to be more generic
This commit is contained in:
@@ -1,27 +1,69 @@
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import Input from '@components/Input'
|
||||
import ComponentInput from '@components/Input'
|
||||
import CustomText from '@components/Text'
|
||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useProfileMutation } from '@utils/queryHooks/profile'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { isEqual } from 'lodash'
|
||||
import React, { RefObject, useEffect, useState } from 'react'
|
||||
import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, View } from 'react-native'
|
||||
import { Alert, TextInput, View } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
||||
const prepareFields = (
|
||||
fields: Mastodon.Field[] | undefined
|
||||
): Mastodon.Field[] => {
|
||||
return Array.from(Array(4).keys()).map(index => {
|
||||
if (fields && fields[index]) {
|
||||
return fields[index]
|
||||
} else {
|
||||
return { name: '', value: '', verified_at: null }
|
||||
}
|
||||
})
|
||||
const Field: React.FC<{
|
||||
allProps: EmojisState['inputProps']
|
||||
setDirty: Dispatch<SetStateAction<boolean>>
|
||||
index: number
|
||||
field?: Mastodon.Field
|
||||
}> = ({ allProps, setDirty, index, field }) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const [name, setName] = useState(field?.name || '')
|
||||
const [value, setValue] = useState(field?.value || '')
|
||||
allProps[index * 2] = {
|
||||
ref: useRef<TextInput>(null),
|
||||
value: name,
|
||||
setValue: setName,
|
||||
selectionRange: name ? { start: name.length, end: name.length } : { start: 0, end: 0 },
|
||||
maxLength: 255
|
||||
}
|
||||
allProps[index * 2 + 1] = {
|
||||
ref: useRef<TextInput>(null),
|
||||
value,
|
||||
setValue,
|
||||
selectionRange: value ? { start: value.length, end: value.length } : { start: 0, end: 0 },
|
||||
maxLength: 255
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDirty(dirty =>
|
||||
dirty ? dirty : !isEqual(field?.name, name) || !isEqual(field?.value, value)
|
||||
)
|
||||
}, [name, value])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.XS,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('me.profile.fields.group', { index: index + 1 })}
|
||||
</CustomText>
|
||||
<ComponentInput title={t('me.profile.fields.label')} {...allProps[index * 2]} />
|
||||
<ComponentInput
|
||||
title={t('me.profile.fields.content')}
|
||||
{...allProps[index * 2 + 1]}
|
||||
multiline
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeProfileFields: React.FC<
|
||||
@@ -35,16 +77,13 @@ const TabMeProfileFields: React.FC<
|
||||
},
|
||||
navigation
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('screenTabs')
|
||||
const { mutateAsync, status } = useProfileMutation()
|
||||
|
||||
const [newFields, setNewFields] = useState(prepareFields(fields))
|
||||
const allProps: EmojisState['inputProps'] = []
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useEffect(() => {
|
||||
setDirty(!isEqual(prepareFields(fields), newFields))
|
||||
}, [newFields])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
@@ -88,9 +127,15 @@ const TabMeProfileFields: React.FC<
|
||||
failed: true
|
||||
},
|
||||
type: 'fields_attributes',
|
||||
data: newFields
|
||||
.filter(field => field.name.length && field.value.length)
|
||||
.map(field => ({ name: field.name, value: field.value }))
|
||||
data: Array.from(Array(4).keys())
|
||||
.filter(
|
||||
index =>
|
||||
allProps[index * 2]?.value.length || allProps[index * 2 + 1]?.value.length
|
||||
)
|
||||
.map(index => ({
|
||||
name: allProps[index * 2].value,
|
||||
value: allProps[index * 2 + 1].value
|
||||
}))
|
||||
}).then(() => {
|
||||
navigation.navigate('Tab-Me-Profile-Root')
|
||||
})
|
||||
@@ -98,60 +143,22 @@ const TabMeProfileFields: React.FC<
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [theme, i18n.language, dirty, status, newFields])
|
||||
}, [theme, i18n.language, dirty, status, allProps.map(p => p.value)])
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
}}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View style={{ marginBottom: StyleConstants.Spacing.L * 2 }}>
|
||||
<ComponentEmojis inputProps={allProps}>
|
||||
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
||||
{Array.from(Array(4).keys()).map(index => (
|
||||
<View key={index} style={{ marginBottom: StyleConstants.Spacing.M }}>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.XS,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('me.profile.fields.group', { index: index + 1 })}
|
||||
</CustomText>
|
||||
<Input
|
||||
title={t('me.profile.fields.label')}
|
||||
autoFocus={false}
|
||||
options={{ maxLength: 255 }}
|
||||
value={newFields[index].name}
|
||||
setValue={(v: any) =>
|
||||
setNewFields(
|
||||
newFields.map((field, i) =>
|
||||
i === index ? { ...field, name: v } : field
|
||||
)
|
||||
)
|
||||
}
|
||||
emoji
|
||||
/>
|
||||
<Input
|
||||
title={t('me.profile.fields.content')}
|
||||
autoFocus={false}
|
||||
options={{ maxLength: 255 }}
|
||||
value={newFields[index].value}
|
||||
setValue={(v: any) =>
|
||||
setNewFields(
|
||||
newFields.map((field, i) =>
|
||||
i === index ? { ...field, value: v } : field
|
||||
)
|
||||
)
|
||||
}
|
||||
emoji
|
||||
/>
|
||||
</View>
|
||||
<Field
|
||||
key={index}
|
||||
allProps={allProps}
|
||||
setDirty={setDirty}
|
||||
index={index}
|
||||
field={fields?.[index]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ComponentEmojis>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import Input from '@components/Input'
|
||||
import ComponentInput from '@components/Input'
|
||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useProfileMutation } from '@utils/queryHooks/profile'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useEffect, useState } from 'react'
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, StyleSheet } from 'react-native'
|
||||
import { Alert, TextInput, View } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
||||
const TabMeProfileName: React.FC<
|
||||
TabMeProfileStackScreenProps<'Tab-Me-Profile-Name'> & {
|
||||
@@ -26,6 +27,15 @@ const TabMeProfileName: React.FC<
|
||||
const { mutateAsync, status } = useProfileMutation()
|
||||
|
||||
const [displayName, setDisplayName] = useState(display_name)
|
||||
const displayNameProps: NonNullable<EmojisState['targetProps']> = {
|
||||
ref: useRef<TextInput>(null),
|
||||
value: displayName,
|
||||
setValue: setDisplayName,
|
||||
selectionRange: displayName
|
||||
? { start: displayName.length, end: displayName.length }
|
||||
: { start: 0, end: 0 },
|
||||
maxLength: 30
|
||||
}
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useEffect(() => {
|
||||
@@ -85,27 +95,18 @@ const TabMeProfileName: React.FC<
|
||||
}, [theme, i18n.language, dirty, status, displayName])
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.base} keyboardShouldPersistTaps='always'>
|
||||
<Input
|
||||
value={displayName}
|
||||
setValue={setDisplayName}
|
||||
emoji
|
||||
options={{
|
||||
maxLength: 30,
|
||||
autoCapitalize: 'none',
|
||||
autoComplete: 'username',
|
||||
textContentType: 'username',
|
||||
autoCorrect: false
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
<ComponentEmojis inputProps={[displayNameProps]} focusRef={displayNameProps.ref}>
|
||||
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
||||
<ComponentInput
|
||||
{...displayNameProps}
|
||||
autoCapitalize='none'
|
||||
autoComplete='username'
|
||||
textContentType='username'
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
</ComponentEmojis>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeProfileName
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import Input from '@components/Input'
|
||||
import ComponentInput from '@components/Input'
|
||||
import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useProfileMutation } from '@utils/queryHooks/profile'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useEffect, useState } from 'react'
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, StyleSheet, View } from 'react-native'
|
||||
import { Alert, TextInput, View } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
||||
const TabMeProfileNote: React.FC<
|
||||
TabMeProfileStackScreenProps<'Tab-Me-Profile-Note'> & {
|
||||
@@ -25,12 +26,19 @@ const TabMeProfileNote: React.FC<
|
||||
const { t, i18n } = useTranslation('screenTabs')
|
||||
const { mutateAsync, status } = useProfileMutation()
|
||||
|
||||
const [newNote, setNewNote] = useState(note)
|
||||
const [notes, setNotes] = useState(note)
|
||||
const notesProps: NonNullable<EmojisState['targetProps']> = {
|
||||
ref: useRef<TextInput>(null),
|
||||
value: notes,
|
||||
setValue: setNotes,
|
||||
selectionRange: notes ? { start: notes.length, end: notes.length } : { start: 0, end: 0 },
|
||||
maxLength: 500
|
||||
}
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useEffect(() => {
|
||||
setDirty(note !== newNote)
|
||||
}, [newNote])
|
||||
setDirty(note !== notes)
|
||||
}, [notes])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
@@ -74,7 +82,7 @@ const TabMeProfileNote: React.FC<
|
||||
failed: true
|
||||
},
|
||||
type: 'note',
|
||||
data: newNote
|
||||
data: notes
|
||||
}).then(() => {
|
||||
navigation.navigate('Tab-Me-Profile-Root')
|
||||
})
|
||||
@@ -82,27 +90,15 @@ const TabMeProfileNote: React.FC<
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [theme, i18n.language, dirty, status, newNote])
|
||||
}, [theme, i18n.language, dirty, status, notes])
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.base} keyboardShouldPersistTaps='always'>
|
||||
<View style={{ marginBottom: StyleConstants.Spacing.XL * 2 }}>
|
||||
<Input
|
||||
value={newNote}
|
||||
setValue={setNewNote}
|
||||
multiline
|
||||
emoji
|
||||
options={{ maxLength: 500 }}
|
||||
/>
|
||||
<ComponentEmojis inputProps={[notesProps]} focusRef={notesProps.ref}>
|
||||
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
||||
<ComponentInput {...notesProps} multiline />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ComponentEmojis>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
||||
export default TabMeProfileNote
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import Input from '@components/Input'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { PlaceholderLine } from 'rn-placeholder'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
edit?: boolean // Editing mode
|
||||
}
|
||||
|
||||
const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
|
||||
const AccountInformationName: React.FC<Props> = ({ account }) => {
|
||||
const { colors } = useTheme()
|
||||
|
||||
const movedContent = useMemo(() => {
|
||||
@@ -30,8 +28,6 @@ const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
|
||||
}
|
||||
}, [account?.moved])
|
||||
|
||||
const [displatName, setDisplayName] = useState(account?.display_name)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -42,25 +38,21 @@ const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
|
||||
}}
|
||||
>
|
||||
{account ? (
|
||||
edit ? (
|
||||
<Input title='昵称' value={displatName} setValue={setDisplayName} />
|
||||
) : (
|
||||
<>
|
||||
<CustomText
|
||||
style={{
|
||||
textDecorationLine: account?.moved ? 'line-through' : undefined
|
||||
}}
|
||||
>
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
size='L'
|
||||
fontBold
|
||||
/>
|
||||
</CustomText>
|
||||
{movedContent}
|
||||
</>
|
||||
)
|
||||
<>
|
||||
<CustomText
|
||||
style={{
|
||||
textDecorationLine: account?.moved ? 'line-through' : undefined
|
||||
}}
|
||||
>
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
size='L'
|
||||
fontBold
|
||||
/>
|
||||
</CustomText>
|
||||
{movedContent}
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderLine
|
||||
width={StyleConstants.Font.Size.L * 2}
|
||||
@@ -74,7 +66,4 @@ const AccountInformationName: React.FC<Props> = ({ account, edit }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(
|
||||
AccountInformationName,
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
export default React.memo(AccountInformationName, (_, next) => next.account === undefined)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Input from '@components/Input'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useState } from 'react'
|
||||
@@ -7,16 +6,11 @@ import { StyleSheet, View } from 'react-native'
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
myInfo?: boolean
|
||||
edit?: boolean
|
||||
}
|
||||
|
||||
const AccountInformationNote = React.memo(
|
||||
({ account, myInfo, edit }: Props) => {
|
||||
({ account, myInfo }: Props) => {
|
||||
const [note, setNote] = useState(account?.source?.note)
|
||||
if (edit) {
|
||||
return <Input title='简介' value={note} setValue={setNote} multiline />
|
||||
}
|
||||
|
||||
if (
|
||||
myInfo ||
|
||||
!account?.note ||
|
||||
|
||||
Reference in New Issue
Block a user