mirror of
https://github.com/tooot-app/app
synced 2025-03-13 18:10:12 +01:00
Preparing for upgrading expo SDK
This commit is contained in:
parent
2272ea3841
commit
fb123b6a26
16
App.tsx
16
App.tsx
@ -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 (
|
||||
|
8
src/@types/mastodon.d.ts
vendored
8
src/@types/mastodon.d.ts
vendored
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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] }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
46
src/screens/Shared/Compose/Reply.tsx
Normal file
46
src/screens/Shared/Compose/Reply.tsx
Normal 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)
|
@ -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
|
||||
},
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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])
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user