Preparing for upgrading expo SDK

This commit is contained in:
Zhiyuan Zheng 2020-12-10 19:19:56 +01:00
parent 2272ea3841
commit fb123b6a26
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
16 changed files with 507 additions and 325 deletions

16
App.tsx
View File

@ -16,14 +16,14 @@ setConsole({
error: console.warn
})
// if (__DEV__) {
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
// whyDidYouRender(React, {
// trackAllPureComponents: true,
// trackHooks: true,
// hotReloadBufferMs: 1000
// })
// }
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
trackHooks: true,
hotReloadBufferMs: 1000
})
}
const App: React.FC = () => {
return (

View File

@ -43,7 +43,7 @@ declare namespace Mastodon {
| AttachmentVideo
| AttachmentGifv
| AttachmentAudio
// | AttachmentUnknown
// | AttachmentUnknown
type AttachmentImage = {
// Base
@ -267,6 +267,12 @@ declare namespace Mastodon {
'reading:expand:spoilers'?: boolean
}
type Results = {
accounts?: Account[]
statuses?: Status[]
hashtags?: Tag[]
}
type Status = {
// Base
id: string

View File

@ -79,4 +79,8 @@ const styles = StyleSheet.create({
}
})
export default ButtonRow
export default React.memo(ButtonRow, (prev, next) => {
let skipUpdate = true
skipUpdate = prev.disabled === next.disabled
return skipUpdate
})

View File

@ -38,4 +38,9 @@ const styles = StyleSheet.create({
}
})
export default HeaderLeft
export default React.memo(HeaderLeft, (prev, next) => {
let skipUpdate = true
skipUpdate = prev.text === next.text
skipUpdate = prev.icon === next.icon
return skipUpdate
})

View File

@ -60,4 +60,10 @@ const styles = StyleSheet.create({
}
})
export default HeaderRight
export default React.memo(HeaderRight, (prev, next) => {
let skipUpdate = true
skipUpdate = prev.disabled === next.disabled
skipUpdate = prev.text === next.text
skipUpdate = prev.icon === next.icon
return skipUpdate
})

View File

@ -123,4 +123,8 @@ const styles = StyleSheet.create({
}
})
export default MenuRow
export default React.memo(MenuRow, (prev, next) => {
let skipUpdate = true
skipUpdate = prev.content === next.content
return skipUpdate
})

View File

@ -15,7 +15,7 @@ import HeaderDefaultActionsStatus from './HeaderDefault/ActionsStatus'
import HeaderDefaultActionsDomain from './HeaderDefault/ActionsDomain'
export interface Props {
queryKey: App.QueryKey
queryKey?: App.QueryKey
status: Mastodon.Status
}
@ -83,11 +83,13 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
@{account}
</Text>
</View>
<Pressable
style={styles.action}
onPress={onPressAction}
children={pressableAction}
/>
{queryKey && (
<Pressable
style={styles.action}
onPress={onPressAction}
children={pressableAction}
/>
)}
</View>
<View style={styles.meta}>
@ -116,35 +118,37 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
)}
</View>
<BottomSheet
visible={modalVisible}
handleDismiss={() => setBottomSheetVisible(false)}
>
{status.account.id !== localAccountId && (
<HeaderDefaultActionsAccount
queryKey={queryKey}
accountId={status.account.id}
account={status.account.acct}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
{queryKey && (
<BottomSheet
visible={modalVisible}
handleDismiss={() => setBottomSheetVisible(false)}
>
{status.account.id !== localAccountId && (
<HeaderDefaultActionsAccount
queryKey={queryKey}
accountId={status.account.id}
account={status.account.acct}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
{status.account.id === localAccountId && (
<HeaderDefaultActionsStatus
queryKey={queryKey}
status={status}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
{status.account.id === localAccountId && (
<HeaderDefaultActionsStatus
queryKey={queryKey}
status={status}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
{domain !== localDomain && (
<HeaderDefaultActionsDomain
queryKey={queryKey}
domain={domain}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
</BottomSheet>
{domain !== localDomain && (
<HeaderDefaultActionsDomain
queryKey={queryKey}
domain={domain}
setBottomSheetVisible={setBottomSheetVisible}
/>
)}
</BottomSheet>
)}
</View>
)
}

View File

@ -37,13 +37,12 @@ export type ComposeState = {
formatted: ReactNode
selection: { start: number; end: number }
}
tag:
| {
type: 'url' | 'accounts' | 'hashtags'
text: string
offset: number
}
| undefined
tag?: {
type: 'url' | 'accounts' | 'hashtags'
text: string
offset: number
length: number
}
emoji: {
active: boolean
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
@ -69,7 +68,7 @@ export type ComposeState = {
| string
}
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] }
attachmentUploadProgress: { progress: number; aspect?: number } | undefined
attachmentUploadProgress?: { progress: number; aspect?: number }
visibility: 'public' | 'unlisted' | 'private' | 'direct'
replyToStatus?: Mastodon.Status
}
@ -93,7 +92,7 @@ export type PostAction =
}
| {
type: 'poll'
payload: ComposeState['poll']
payload: Partial<ComposeState['poll']>
}
| {
type: 'attachments'
@ -209,7 +208,8 @@ const composeExistingState = ({
raw: replyPlaceholder,
formatted: undefined,
selection: { start: 0, end: 0 }
}
},
replyToStatus: incomingStatus.reblog || incomingStatus
}
}
}
@ -224,7 +224,7 @@ const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
case 'emoji':
return { ...state, emoji: action.payload }
case 'poll':
return { ...state, poll: action.payload }
return { ...state, poll: { ...state.poll, ...action.payload } }
case 'attachments':
return {
...state,

View File

@ -1,13 +1,6 @@
import { Feather } from '@expo/vector-icons'
import React, { Dispatch, useCallback, useMemo } from 'react'
import {
ActionSheetIOS,
Keyboard,
Pressable,
StyleSheet,
Text,
TextInput
} from 'react-native'
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, ComposeState } from '../Compose'
@ -26,19 +19,6 @@ const ComposeActions: React.FC<Props> = ({
}) => {
const { theme } = useTheme()
const getVisibilityIcon = () => {
switch (composeState.visibility) {
case 'public':
return 'globe'
case 'unlisted':
return 'unlock'
case 'private':
return 'lock'
case 'direct':
return 'mail'
}
}
const attachmentColor = useMemo(() => {
if (composeState.poll.active) return theme.disabled
if (composeState.attachmentUploadProgress) return theme.primary
@ -99,6 +79,54 @@ const ComposeActions: React.FC<Props> = ({
composeState.attachmentUploadProgress
])
const visibilityIcon = useMemo(() => {
switch (composeState.visibility) {
case 'public':
return 'globe'
case 'unlisted':
return 'unlock'
case 'private':
return 'lock'
case 'direct':
return 'mail'
}
}, [composeState.visibility])
const visibilityOnPress = useCallback(
() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4
},
buttonIndex => {
switch (buttonIndex) {
case 0:
composeDispatch({ type: 'visibility', payload: 'public' })
break
case 1:
composeDispatch({ type: 'visibility', payload: 'unlisted' })
break
case 2:
composeDispatch({ type: 'visibility', payload: 'private' })
break
case 3:
composeDispatch({ type: 'visibility', payload: 'direct' })
break
}
}
),
[]
)
const spoilerOnPress = useCallback(
() =>
composeDispatch({
type: 'spoiler',
payload: { active: !composeState.spoiler.active }
}),
[composeState.spoiler.active]
)
const emojiColor = useMemo(() => {
if (!composeState.emoji.emojis) return theme.disabled
if (composeState.emoji.active) {
@ -124,12 +152,11 @@ const ComposeActions: React.FC<Props> = ({
}, [composeState.emoji.active, composeState.emoji.emojis])
return (
<Pressable
<View
style={[
styles.additions,
{ backgroundColor: theme.background, borderTopColor: theme.border }
]}
onPress={() => Keyboard.dismiss()}
>
<Feather
name='aperture'
@ -144,44 +171,16 @@ const ComposeActions: React.FC<Props> = ({
onPress={pollOnPress}
/>
<Feather
name={getVisibilityIcon()}
name={visibilityIcon}
size={24}
color={theme.secondary}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4
},
buttonIndex => {
switch (buttonIndex) {
case 0:
composeDispatch({ type: 'visibility', payload: 'public' })
break
case 1:
composeDispatch({ type: 'visibility', payload: 'unlisted' })
break
case 2:
composeDispatch({ type: 'visibility', payload: 'private' })
break
case 3:
composeDispatch({ type: 'visibility', payload: 'direct' })
break
}
}
)
}
onPress={visibilityOnPress}
/>
<Feather
name='alert-triangle'
size={24}
color={composeState.spoiler.active ? theme.primary : theme.secondary}
onPress={() =>
composeDispatch({
type: 'spoiler',
payload: { active: !composeState.spoiler.active }
})
}
onPress={spoilerOnPress}
/>
<Feather
name='smile'
@ -189,7 +188,7 @@ const ComposeActions: React.FC<Props> = ({
color={emojiColor}
onPress={emojiOnPress}
/>
</Pressable>
</View>
)
}

View File

@ -1,4 +1,4 @@
import React, { Dispatch } from 'react'
import React, { Dispatch, useCallback, useMemo } from 'react'
import {
Image,
Pressable,
@ -20,57 +20,70 @@ export interface Props {
composeDispatch: Dispatch<PostAction>
}
const ComposeEmojis: React.FC<Props> = ({
const SingleEmoji = ({
emoji,
textInputRef,
composeState,
composeDispatch
}) => {
}: { emoji: Mastodon.Emoji } & Props) => {
const onPress = useCallback(() => {
updateText({
origin: textInputRef.current?.isFocused() ? 'text' : 'spoiler',
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
composeDispatch({
type: 'emoji',
payload: { ...composeState.emoji, active: false }
})
}, [])
const children = useMemo(
() => <Image source={{ uri: emoji.url }} style={styles.emoji} />,
[]
)
return (
<Pressable key={emoji.shortcode} onPress={onPress} children={children} />
)
}
const ComposeEmojis: React.FC<Props> = ({ ...props }) => {
const { theme } = useTheme()
const listHeader = useCallback(
({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}>{title}</Text>
),
[]
)
const emojiList = useCallback(
section =>
section.data.map((emoji: Mastodon.Emoji) => (
<SingleEmoji key={emoji.shortcode} emoji={emoji} {...props} />
)),
[]
)
const listItem = useCallback(
({ section, index }) =>
index === 0 ? (
<View key={section.title} style={styles.emojis}>
{emojiList(section)}
</View>
) : null,
[]
)
return (
<View style={styles.base}>
<SectionList
horizontal
keyboardShouldPersistTaps='handled'
sections={composeState.emoji.emojis!}
sections={props.composeState.emoji.emojis!}
keyExtractor={item => item.shortcode}
renderSectionHeader={({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}>
{title}
</Text>
)}
renderItem={({ section, index }) => {
if (index === 0) {
return (
<View key={section.title} style={styles.emojis}>
{section.data.map(emoji => (
<Pressable
key={emoji.shortcode}
onPress={() => {
updateText({
origin: textInputRef.current?.isFocused()
? 'text'
: 'spoiler',
composeState,
composeDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
composeDispatch({
type: 'emoji',
payload: { ...composeState.emoji, active: false }
})
}}
>
<Image source={{ uri: emoji.url }} style={styles.emoji} />
</Pressable>
))}
</View>
)
} else {
return null
}
}}
renderSectionHeader={listHeader}
renderItem={listItem}
/>
</View>
)

View File

@ -1,4 +1,4 @@
import React, { Dispatch, useEffect, useState } from 'react'
import React, { Dispatch, useCallback, useEffect, useState } from 'react'
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
@ -9,11 +9,14 @@ import { ButtonRow } from 'src/components/Button'
import { MenuContainer, MenuRow } from 'src/components/Menu'
export interface Props {
composeState: ComposeState
poll: ComposeState['poll']
composeDispatch: Dispatch<PostAction>
}
const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
const ComposePoll: React.FC<Props> = ({
poll: { total, options, multiple, expire },
composeDispatch
}) => {
const { theme } = useTheme()
const expireMapping: { [key: string]: string } = {
@ -31,24 +34,42 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
setFirstRender(false)
}, [])
const minusOnPress = useCallback(
() =>
total > 2 &&
composeDispatch({
type: 'poll',
payload: { total: total - 1 }
}),
[total]
)
console.log('total: ', total)
const plusOnPress = useCallback(() => {
total < 4 &&
composeDispatch({
type: 'poll',
payload: { total: total + 1 }
})
}, [total])
return (
<View style={[styles.base, { borderColor: theme.border }]}>
<View style={styles.options}>
{[...Array(composeState.poll.total)].map((e, i) => {
const restOptions = Object.keys(composeState.poll.options).filter(
o => parseInt(o) !== i && parseInt(o) < composeState.poll.total
{[...Array(total)].map((e, i) => {
const restOptions = Object.keys(options).filter(
o => parseInt(o) !== i && parseInt(o) < total
)
let hasConflict = false
restOptions.forEach(o => {
// @ts-ignore
if (composeState.poll.options[o] === composeState.poll.options[i]) {
if (options[o] === options[i]) {
hasConflict = true
}
})
return (
<View key={i} style={styles.option}>
<Feather
name={composeState.poll.multiple ? 'square' : 'circle'}
name={multiple ? 'square' : 'circle'}
size={StyleConstants.Font.Size.L}
color={theme.secondary}
/>
@ -65,14 +86,11 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
placeholderTextColor={theme.secondary}
maxLength={50}
// @ts-ignore
value={composeState.poll.options[i]}
value={options[i]}
onChangeText={e =>
composeDispatch({
type: 'poll',
payload: {
...composeState.poll,
options: { ...composeState.poll.options, [i]: e }
}
payload: { options: { ...options, [i]: e } }
})
}
/>
@ -83,35 +101,23 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
<View style={styles.controlAmount}>
<View style={styles.firstButton}>
<ButtonRow
onPress={() =>
composeState.poll.total > 2 &&
composeDispatch({
type: 'poll',
payload: { ...composeState.poll, total: composeState.poll.total - 1 }
})
}
onPress={minusOnPress}
icon='minus'
disabled={!(composeState.poll.total > 2)}
disabled={!(total > 2)}
buttonSize='S'
/>
</View>
<ButtonRow
onPress={() =>
composeState.poll.total < 4 &&
composeDispatch({
type: 'poll',
payload: { ...composeState.poll, total: composeState.poll.total + 1 }
})
}
onPress={plusOnPress}
icon='plus'
disabled={!(composeState.poll.total < 4)}
disabled={!(total < 4)}
buttonSize='S'
/>
</View>
<MenuContainer>
<MenuRow
title='可选项'
content={composeState.poll.multiple ? '多选' : '单选'}
content={multiple ? '多选' : '单选'}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
@ -122,7 +128,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
index < 2 &&
composeDispatch({
type: 'poll',
payload: { ...composeState.poll, multiple: index === 1 }
payload: { multiple: index === 1 }
})
)
}
@ -130,7 +136,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
/>
<MenuRow
title='有效期'
content={expireMapping[composeState.poll.expire]}
content={expireMapping[expire]}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
@ -141,10 +147,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
index < 7 &&
composeDispatch({
type: 'poll',
payload: {
...composeState.poll,
expire: Object.keys(expireMapping)[index]
}
payload: { expire: Object.keys(expireMapping)[index] }
})
)
}

View File

@ -0,0 +1,46 @@
import React, { Dispatch, RefObject } from 'react'
import { StyleSheet, Text, TextInput, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment'
import TimelineAvatar from 'src/components/Timelines/Timeline/Shared/Avatar'
import TimelineCard from 'src/components/Timelines/Timeline/Shared/Card'
import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content'
import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault'
export interface Props {
replyToStatus: Mastodon.Status
}
const ComposeReply: React.FC<Props> = ({ replyToStatus }) => {
const { theme } = useTheme()
return (
<View style={styles.status}>
<TimelineAvatar account={replyToStatus.account} />
<View style={styles.details}>
<TimelineHeaderDefault status={replyToStatus} />
{replyToStatus.content.length > 0 && (
<TimelineContent status={replyToStatus} />
)}
{replyToStatus.media_attachments.length > 0 && (
<TimelineAttachment status={replyToStatus} width={200} />
)}
{replyToStatus.card && <TimelineCard card={replyToStatus.card} />}
</View>
</View>
)
}
const styles = StyleSheet.create({
status: {
flex: 1,
flexDirection: 'row'
},
details: {
flex: 1
}
})
export default React.memo(ComposeReply, () => true)

View File

@ -1,5 +1,12 @@
import { forEach, groupBy, sortBy } from 'lodash'
import React, { Dispatch, useEffect, useMemo, useRef } from 'react'
import React, {
Dispatch,
RefObject,
useCallback,
useEffect,
useMemo,
useRef
} from 'react'
import {
View,
ActivityIndicator,
@ -22,6 +29,7 @@ import ComposeActions from './Actions'
import ComposeAttachments from './Attachments'
import ComposeEmojis from './Emojis'
import ComposePoll from './Poll'
import ComposeReply from './Reply'
import ComposeSpoilerInput from './SpoilerInput'
import ComposeTextInput from './TextInput'
import updateText from './updateText'
@ -32,13 +40,91 @@ export interface Props {
composeDispatch: Dispatch<PostAction>
}
const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme()
const ListItem = React.memo(
({
item,
composeState,
composeDispatch,
textInputRef
}: {
item: Mastodon.Account & Mastodon.Tag
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
textInputRef: RefObject<TextInput>
}) => {
const { theme } = useTheme()
const onPress = useCallback(() => {
const focusedInput = textInputRef.current?.isFocused()
? 'text'
: 'spoiler'
updateText({
origin: focusedInput,
composeState: {
...composeState,
[focusedInput]: {
...composeState[focusedInput],
selection: {
start: composeState.tag!.offset,
end: composeState.tag!.offset + composeState.tag!.text.length + 1
}
}
},
composeDispatch,
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
type: 'suggestion'
})
}, [])
const children = useMemo(
() =>
item.acct ? (
<View style={[styles.account, { borderBottomColor: theme.border }]}>
<Image source={{ uri: item.avatar }} style={styles.accountAvatar} />
<View>
<Text style={[styles.accountName, { color: theme.primary }]}>
{item.emojis?.length ? (
<Emojis
content={item.display_name || item.username}
emojis={item.emojis}
size={StyleConstants.Font.Size.S}
/>
) : (
item.display_name || item.username
)}
</Text>
<Text style={[styles.accountAccount, { color: theme.primary }]}>
@{item.acct}
</Text>
</View>
</View>
) : (
<View style={[styles.hashtag, { borderBottomColor: theme.border }]}>
<Text style={[styles.hashtagText, { color: theme.primary }]}>
#{item.name}
</Text>
</View>
),
[]
)
return (
<Pressable
key={item.url}
onPress={onPress}
style={styles.suggestion}
children={children}
/>
)
},
() => true
)
const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { isFetching, isSuccess, data, refetch } = useQuery(
[
'Search',
{ type: composeState.tag?.type, term: composeState.tag?.text.substring(1) }
{
type: composeState.tag?.type,
term: composeState.tag?.text.substring(1)
}
],
searchFetch,
{ enabled: false }
@ -86,6 +172,128 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
}
}, [isFetching])
const listHeader = useMemo(() => {
return (
<>
{composeState.spoiler.active ? (
<ComposeSpoilerInput
composeState={composeState}
composeDispatch={composeDispatch}
/>
) : null}
<ComposeTextInput
composeState={composeState}
composeDispatch={composeDispatch}
textInputRef={textInputRef}
/>
</>
)
}, [composeState.spoiler.active, composeState.text.formatted])
const listFooterEmojis = useMemo(
() =>
composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis
textInputRef={textInputRef}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
),
[composeState.emoji.active]
)
const listFooterAttachments = useMemo(
() =>
(composeState.attachments.uploads.length > 0 ||
composeState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
),
[composeState.attachments.uploads, composeState.attachmentUploadProgress]
)
// const listFooterPoll = useMemo(
// () =>
// composeState.poll.active && (
// <View style={styles.poll}>
// <ComposePoll
// poll={composeState.poll}
// composeDispatch={composeDispatch}
// />
// </View>
// ),
// [
// composeState.poll.active,
// composeState.poll.total,
// composeState.poll.options['0'],
// composeState.poll.options['1'],
// composeState.poll.options['2'],
// composeState.poll.options['3'],
// composeState.poll.multiple,
// composeState.poll.expire
// ]
// )
const listFooterPoll = () =>
composeState.poll.active && (
<View style={styles.poll}>
<ComposePoll
poll={composeState.poll}
composeDispatch={composeDispatch}
/>
</View>
)
const listFooterReply = useMemo(
() =>
composeState.replyToStatus && (
<View style={styles.replyTo}>
<ComposeReply replyToStatus={composeState.replyToStatus} />
</View>
),
[]
)
const listFooter = useMemo(() => {
return (
<>
{listFooterEmojis}
{listFooterAttachments}
{listFooterPoll()}
{listFooterReply}
</>
)
}, [
composeState.emoji.active,
composeState.attachments.uploads,
composeState.attachmentUploadProgress,
composeState.poll.active,
composeState.poll.total,
composeState.poll.options['0'],
composeState.poll.options['1'],
composeState.poll.options['2'],
composeState.poll.options['3'],
composeState.poll.multiple,
composeState.poll.expire
])
const listKey = useCallback(
(item: Mastodon.Account | Mastodon.Tag) => item.url,
[isSuccess]
)
const listItem = useCallback(
({ item }) =>
isSuccess ? (
<ListItem
item={item}
composeState={composeState}
composeDispatch={composeDispatch}
textInputRef={textInputRef}
/>
) : null,
[isSuccess]
)
return (
<View style={styles.base}>
<ProgressViewIOS
@ -94,134 +302,12 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
/>
<FlatList
keyboardShouldPersistTaps='handled'
ListHeaderComponent={
<>
{composeState.spoiler.active ? (
<ComposeSpoilerInput
composeState={composeState}
composeDispatch={composeDispatch}
/>
) : null}
<ComposeTextInput
composeState={composeState}
composeDispatch={composeDispatch}
textInputRef={textInputRef}
/>
</>
}
ListFooterComponent={
<>
{composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis
textInputRef={textInputRef}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
{(composeState.attachments.uploads.length > 0 ||
composeState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
{composeState.poll.active && (
<View style={styles.poll}>
<ComposePoll
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
</>
}
ListHeaderComponent={listHeader}
ListFooterComponent={listFooter}
ListEmptyComponent={listEmpty}
data={composeState.tag && isSuccess ? data[composeState.tag.type] : []}
renderItem={({ item, index }) => (
<Pressable
key={index}
onPress={() => {
const focusedInput = textInputRef.current?.isFocused()
? 'text'
: 'spoiler'
updateText({
origin: focusedInput,
composeState: {
...composeState,
[focusedInput]: {
...composeState[focusedInput],
selection: {
start: composeState.tag!.offset,
end:
composeState.tag!.offset + composeState.tag!.text.length + 1
}
}
},
composeDispatch,
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
type: 'suggestion'
})
}}
style={styles.suggestion}
>
{item.acct ? (
<View
style={[
styles.account,
{ borderBottomColor: theme.border },
index === 0 && {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: theme.border
}
]}
>
<Image
source={{ uri: item.avatar }}
style={styles.accountAvatar}
/>
<View>
<Text style={[styles.accountName, { color: theme.primary }]}>
{item.emojis.length ? (
<Emojis
content={item.display_name || item.username}
emojis={item.emojis}
size={StyleConstants.Font.Size.S}
/>
) : (
item.display_name || item.username
)}
</Text>
<Text
style={[styles.accountAccount, { color: theme.primary }]}
>
@{item.acct}
</Text>
</View>
</View>
) : (
<View
style={[
styles.hashtag,
{ borderBottomColor: theme.border },
index === 0 && {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: theme.border
}
]}
>
<Text style={[styles.hashtagText, { color: theme.primary }]}>
#{item.name}
</Text>
</View>
)}
</Pressable>
)}
data={data}
keyExtractor={listKey}
renderItem={listItem}
/>
<ComposeActions
textInputRef={textInputRef}
@ -245,6 +331,10 @@ const styles = StyleSheet.create({
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
},
replyTo: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
},
suggestion: {
flex: 1
},

View File

@ -1,4 +1,4 @@
import React, { Dispatch, RefObject } from 'react'
import React, { Dispatch } from 'react'
import { StyleSheet, Text, TextInput } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
@ -8,13 +8,11 @@ import formatText from './formatText'
export interface Props {
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
// textInputRef: RefObject<TextInput>
}
const ComposeSpoilerInput: React.FC<Props> = ({
composeState,
composeDispatch,
// textInputRef
}) => {
const { theme } = useTheme()

View File

@ -46,7 +46,10 @@ const ComposeTextInput: React.FC<Props> = ({
selection: { start, end }
}
}) => {
composeDispatch({ type: 'text', payload: { selection: { start, end } } })
composeDispatch({
type: 'text',
payload: { selection: { start, end } }
})
}}
ref={textInputRef}
scrollEnabled
@ -70,5 +73,6 @@ const styles = StyleSheet.create({
export default React.memo(
ComposeTextInput,
(prev, next) =>
prev.composeState.text.raw === next.composeState.text.raw &&
prev.composeState.text.formatted === next.composeState.text.formatted
)

View File

@ -19,5 +19,5 @@ export const searchFetch = async (
url: 'search',
params: { type, q: term, limit }
})
return Promise.resolve(res.body)
return Promise.resolve(res.body[type])
}