mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Preparing for upgrading expo SDK
This commit is contained in:
16
App.tsx
16
App.tsx
@ -16,14 +16,14 @@ setConsole({
|
|||||||
error: console.warn
|
error: console.warn
|
||||||
})
|
})
|
||||||
|
|
||||||
// if (__DEV__) {
|
if (__DEV__) {
|
||||||
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
// whyDidYouRender(React, {
|
whyDidYouRender(React, {
|
||||||
// trackAllPureComponents: true,
|
trackAllPureComponents: true,
|
||||||
// trackHooks: true,
|
trackHooks: true,
|
||||||
// hotReloadBufferMs: 1000
|
hotReloadBufferMs: 1000
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
8
src/@types/mastodon.d.ts
vendored
8
src/@types/mastodon.d.ts
vendored
@ -43,7 +43,7 @@ declare namespace Mastodon {
|
|||||||
| AttachmentVideo
|
| AttachmentVideo
|
||||||
| AttachmentGifv
|
| AttachmentGifv
|
||||||
| AttachmentAudio
|
| AttachmentAudio
|
||||||
// | AttachmentUnknown
|
// | AttachmentUnknown
|
||||||
|
|
||||||
type AttachmentImage = {
|
type AttachmentImage = {
|
||||||
// Base
|
// Base
|
||||||
@ -267,6 +267,12 @@ declare namespace Mastodon {
|
|||||||
'reading:expand:spoilers'?: boolean
|
'reading:expand:spoilers'?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Results = {
|
||||||
|
accounts?: Account[]
|
||||||
|
statuses?: Status[]
|
||||||
|
hashtags?: Tag[]
|
||||||
|
}
|
||||||
|
|
||||||
type Status = {
|
type Status = {
|
||||||
// Base
|
// Base
|
||||||
id: string
|
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'
|
import HeaderDefaultActionsDomain from './HeaderDefault/ActionsDomain'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
queryKey: App.QueryKey
|
queryKey?: App.QueryKey
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,11 +83,13 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
@{account}
|
@{account}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Pressable
|
{queryKey && (
|
||||||
style={styles.action}
|
<Pressable
|
||||||
onPress={onPressAction}
|
style={styles.action}
|
||||||
children={pressableAction}
|
onPress={onPressAction}
|
||||||
/>
|
children={pressableAction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
@ -116,35 +118,37 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<BottomSheet
|
{queryKey && (
|
||||||
visible={modalVisible}
|
<BottomSheet
|
||||||
handleDismiss={() => setBottomSheetVisible(false)}
|
visible={modalVisible}
|
||||||
>
|
handleDismiss={() => setBottomSheetVisible(false)}
|
||||||
{status.account.id !== localAccountId && (
|
>
|
||||||
<HeaderDefaultActionsAccount
|
{status.account.id !== localAccountId && (
|
||||||
queryKey={queryKey}
|
<HeaderDefaultActionsAccount
|
||||||
accountId={status.account.id}
|
queryKey={queryKey}
|
||||||
account={status.account.acct}
|
accountId={status.account.id}
|
||||||
setBottomSheetVisible={setBottomSheetVisible}
|
account={status.account.acct}
|
||||||
/>
|
setBottomSheetVisible={setBottomSheetVisible}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{status.account.id === localAccountId && (
|
{status.account.id === localAccountId && (
|
||||||
<HeaderDefaultActionsStatus
|
<HeaderDefaultActionsStatus
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
status={status}
|
status={status}
|
||||||
setBottomSheetVisible={setBottomSheetVisible}
|
setBottomSheetVisible={setBottomSheetVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{domain !== localDomain && (
|
{domain !== localDomain && (
|
||||||
<HeaderDefaultActionsDomain
|
<HeaderDefaultActionsDomain
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
setBottomSheetVisible={setBottomSheetVisible}
|
setBottomSheetVisible={setBottomSheetVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,12 @@ export type ComposeState = {
|
|||||||
formatted: ReactNode
|
formatted: ReactNode
|
||||||
selection: { start: number; end: number }
|
selection: { start: number; end: number }
|
||||||
}
|
}
|
||||||
tag:
|
tag?: {
|
||||||
| {
|
type: 'url' | 'accounts' | 'hashtags'
|
||||||
type: 'url' | 'accounts' | 'hashtags'
|
text: string
|
||||||
text: string
|
offset: number
|
||||||
offset: number
|
length: number
|
||||||
}
|
}
|
||||||
| undefined
|
|
||||||
emoji: {
|
emoji: {
|
||||||
active: boolean
|
active: boolean
|
||||||
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
|
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
|
||||||
@ -69,7 +68,7 @@ export type ComposeState = {
|
|||||||
| string
|
| string
|
||||||
}
|
}
|
||||||
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] }
|
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] }
|
||||||
attachmentUploadProgress: { progress: number; aspect?: number } | undefined
|
attachmentUploadProgress?: { progress: number; aspect?: number }
|
||||||
visibility: 'public' | 'unlisted' | 'private' | 'direct'
|
visibility: 'public' | 'unlisted' | 'private' | 'direct'
|
||||||
replyToStatus?: Mastodon.Status
|
replyToStatus?: Mastodon.Status
|
||||||
}
|
}
|
||||||
@ -93,7 +92,7 @@ export type PostAction =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'poll'
|
type: 'poll'
|
||||||
payload: ComposeState['poll']
|
payload: Partial<ComposeState['poll']>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'attachments'
|
type: 'attachments'
|
||||||
@ -209,7 +208,8 @@ const composeExistingState = ({
|
|||||||
raw: replyPlaceholder,
|
raw: replyPlaceholder,
|
||||||
formatted: undefined,
|
formatted: undefined,
|
||||||
selection: { start: 0, end: 0 }
|
selection: { start: 0, end: 0 }
|
||||||
}
|
},
|
||||||
|
replyToStatus: incomingStatus.reblog || incomingStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
|
|||||||
case 'emoji':
|
case 'emoji':
|
||||||
return { ...state, emoji: action.payload }
|
return { ...state, emoji: action.payload }
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return { ...state, poll: action.payload }
|
return { ...state, poll: { ...state.poll, ...action.payload } }
|
||||||
case 'attachments':
|
case 'attachments':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
import { Feather } from '@expo/vector-icons'
|
import { Feather } from '@expo/vector-icons'
|
||||||
import React, { Dispatch, useCallback, useMemo } from 'react'
|
import React, { Dispatch, useCallback, useMemo } from 'react'
|
||||||
import {
|
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
|
||||||
ActionSheetIOS,
|
|
||||||
Keyboard,
|
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TextInput
|
|
||||||
} from 'react-native'
|
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import { PostAction, ComposeState } from '../Compose'
|
import { PostAction, ComposeState } from '../Compose'
|
||||||
@ -26,19 +19,6 @@ const ComposeActions: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
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(() => {
|
const attachmentColor = useMemo(() => {
|
||||||
if (composeState.poll.active) return theme.disabled
|
if (composeState.poll.active) return theme.disabled
|
||||||
if (composeState.attachmentUploadProgress) return theme.primary
|
if (composeState.attachmentUploadProgress) return theme.primary
|
||||||
@ -99,6 +79,54 @@ const ComposeActions: React.FC<Props> = ({
|
|||||||
composeState.attachmentUploadProgress
|
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(() => {
|
const emojiColor = useMemo(() => {
|
||||||
if (!composeState.emoji.emojis) return theme.disabled
|
if (!composeState.emoji.emojis) return theme.disabled
|
||||||
if (composeState.emoji.active) {
|
if (composeState.emoji.active) {
|
||||||
@ -124,12 +152,11 @@ const ComposeActions: React.FC<Props> = ({
|
|||||||
}, [composeState.emoji.active, composeState.emoji.emojis])
|
}, [composeState.emoji.active, composeState.emoji.emojis])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.additions,
|
styles.additions,
|
||||||
{ backgroundColor: theme.background, borderTopColor: theme.border }
|
{ backgroundColor: theme.background, borderTopColor: theme.border }
|
||||||
]}
|
]}
|
||||||
onPress={() => Keyboard.dismiss()}
|
|
||||||
>
|
>
|
||||||
<Feather
|
<Feather
|
||||||
name='aperture'
|
name='aperture'
|
||||||
@ -144,44 +171,16 @@ const ComposeActions: React.FC<Props> = ({
|
|||||||
onPress={pollOnPress}
|
onPress={pollOnPress}
|
||||||
/>
|
/>
|
||||||
<Feather
|
<Feather
|
||||||
name={getVisibilityIcon()}
|
name={visibilityIcon}
|
||||||
size={24}
|
size={24}
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
onPress={() =>
|
onPress={visibilityOnPress}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Feather
|
<Feather
|
||||||
name='alert-triangle'
|
name='alert-triangle'
|
||||||
size={24}
|
size={24}
|
||||||
color={composeState.spoiler.active ? theme.primary : theme.secondary}
|
color={composeState.spoiler.active ? theme.primary : theme.secondary}
|
||||||
onPress={() =>
|
onPress={spoilerOnPress}
|
||||||
composeDispatch({
|
|
||||||
type: 'spoiler',
|
|
||||||
payload: { active: !composeState.spoiler.active }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Feather
|
<Feather
|
||||||
name='smile'
|
name='smile'
|
||||||
@ -189,7 +188,7 @@ const ComposeActions: React.FC<Props> = ({
|
|||||||
color={emojiColor}
|
color={emojiColor}
|
||||||
onPress={emojiOnPress}
|
onPress={emojiOnPress}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Dispatch } from 'react'
|
import React, { Dispatch, useCallback, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
Pressable,
|
Pressable,
|
||||||
@ -20,57 +20,70 @@ export interface Props {
|
|||||||
composeDispatch: Dispatch<PostAction>
|
composeDispatch: Dispatch<PostAction>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposeEmojis: React.FC<Props> = ({
|
const SingleEmoji = ({
|
||||||
|
emoji,
|
||||||
textInputRef,
|
textInputRef,
|
||||||
composeState,
|
composeState,
|
||||||
composeDispatch
|
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 { 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 (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<SectionList
|
<SectionList
|
||||||
horizontal
|
horizontal
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
sections={composeState.emoji.emojis!}
|
sections={props.composeState.emoji.emojis!}
|
||||||
keyExtractor={item => item.shortcode}
|
keyExtractor={item => item.shortcode}
|
||||||
renderSectionHeader={({ section: { title } }) => (
|
renderSectionHeader={listHeader}
|
||||||
<Text style={[styles.group, { color: theme.secondary }]}>
|
renderItem={listItem}
|
||||||
{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
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</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 { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
|
||||||
import { Feather } from '@expo/vector-icons'
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
|
||||||
@ -9,11 +9,14 @@ import { ButtonRow } from 'src/components/Button'
|
|||||||
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
import { MenuContainer, MenuRow } from 'src/components/Menu'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
composeState: ComposeState
|
poll: ComposeState['poll']
|
||||||
composeDispatch: Dispatch<PostAction>
|
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 { theme } = useTheme()
|
||||||
|
|
||||||
const expireMapping: { [key: string]: string } = {
|
const expireMapping: { [key: string]: string } = {
|
||||||
@ -31,24 +34,42 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
setFirstRender(false)
|
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 (
|
return (
|
||||||
<View style={[styles.base, { borderColor: theme.border }]}>
|
<View style={[styles.base, { borderColor: theme.border }]}>
|
||||||
<View style={styles.options}>
|
<View style={styles.options}>
|
||||||
{[...Array(composeState.poll.total)].map((e, i) => {
|
{[...Array(total)].map((e, i) => {
|
||||||
const restOptions = Object.keys(composeState.poll.options).filter(
|
const restOptions = Object.keys(options).filter(
|
||||||
o => parseInt(o) !== i && parseInt(o) < composeState.poll.total
|
o => parseInt(o) !== i && parseInt(o) < total
|
||||||
)
|
)
|
||||||
let hasConflict = false
|
let hasConflict = false
|
||||||
restOptions.forEach(o => {
|
restOptions.forEach(o => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (composeState.poll.options[o] === composeState.poll.options[i]) {
|
if (options[o] === options[i]) {
|
||||||
hasConflict = true
|
hasConflict = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<View key={i} style={styles.option}>
|
<View key={i} style={styles.option}>
|
||||||
<Feather
|
<Feather
|
||||||
name={composeState.poll.multiple ? 'square' : 'circle'}
|
name={multiple ? 'square' : 'circle'}
|
||||||
size={StyleConstants.Font.Size.L}
|
size={StyleConstants.Font.Size.L}
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
/>
|
/>
|
||||||
@ -65,14 +86,11 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
placeholderTextColor={theme.secondary}
|
placeholderTextColor={theme.secondary}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
value={composeState.poll.options[i]}
|
value={options[i]}
|
||||||
onChangeText={e =>
|
onChangeText={e =>
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'poll',
|
type: 'poll',
|
||||||
payload: {
|
payload: { options: { ...options, [i]: e } }
|
||||||
...composeState.poll,
|
|
||||||
options: { ...composeState.poll.options, [i]: e }
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -83,35 +101,23 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
<View style={styles.controlAmount}>
|
<View style={styles.controlAmount}>
|
||||||
<View style={styles.firstButton}>
|
<View style={styles.firstButton}>
|
||||||
<ButtonRow
|
<ButtonRow
|
||||||
onPress={() =>
|
onPress={minusOnPress}
|
||||||
composeState.poll.total > 2 &&
|
|
||||||
composeDispatch({
|
|
||||||
type: 'poll',
|
|
||||||
payload: { ...composeState.poll, total: composeState.poll.total - 1 }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
icon='minus'
|
icon='minus'
|
||||||
disabled={!(composeState.poll.total > 2)}
|
disabled={!(total > 2)}
|
||||||
buttonSize='S'
|
buttonSize='S'
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ButtonRow
|
<ButtonRow
|
||||||
onPress={() =>
|
onPress={plusOnPress}
|
||||||
composeState.poll.total < 4 &&
|
|
||||||
composeDispatch({
|
|
||||||
type: 'poll',
|
|
||||||
payload: { ...composeState.poll, total: composeState.poll.total + 1 }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
icon='plus'
|
icon='plus'
|
||||||
disabled={!(composeState.poll.total < 4)}
|
disabled={!(total < 4)}
|
||||||
buttonSize='S'
|
buttonSize='S'
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title='可选项'
|
title='可选项'
|
||||||
content={composeState.poll.multiple ? '多选' : '单选'}
|
content={multiple ? '多选' : '单选'}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
ActionSheetIOS.showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@ -122,7 +128,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
index < 2 &&
|
index < 2 &&
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'poll',
|
type: 'poll',
|
||||||
payload: { ...composeState.poll, multiple: index === 1 }
|
payload: { multiple: index === 1 }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -130,7 +136,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
/>
|
/>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title='有效期'
|
title='有效期'
|
||||||
content={expireMapping[composeState.poll.expire]}
|
content={expireMapping[expire]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
ActionSheetIOS.showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@ -141,10 +147,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
index < 7 &&
|
index < 7 &&
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'poll',
|
type: 'poll',
|
||||||
payload: {
|
payload: { expire: Object.keys(expireMapping)[index] }
|
||||||
...composeState.poll,
|
|
||||||
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 { forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { Dispatch, useEffect, useMemo, useRef } from 'react'
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef
|
||||||
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@ -22,6 +29,7 @@ import ComposeActions from './Actions'
|
|||||||
import ComposeAttachments from './Attachments'
|
import ComposeAttachments from './Attachments'
|
||||||
import ComposeEmojis from './Emojis'
|
import ComposeEmojis from './Emojis'
|
||||||
import ComposePoll from './Poll'
|
import ComposePoll from './Poll'
|
||||||
|
import ComposeReply from './Reply'
|
||||||
import ComposeSpoilerInput from './SpoilerInput'
|
import ComposeSpoilerInput from './SpoilerInput'
|
||||||
import ComposeTextInput from './TextInput'
|
import ComposeTextInput from './TextInput'
|
||||||
import updateText from './updateText'
|
import updateText from './updateText'
|
||||||
@ -32,13 +40,91 @@ export interface Props {
|
|||||||
composeDispatch: Dispatch<PostAction>
|
composeDispatch: Dispatch<PostAction>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
const ListItem = React.memo(
|
||||||
const { theme } = useTheme()
|
({
|
||||||
|
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(
|
const { isFetching, isSuccess, data, refetch } = useQuery(
|
||||||
[
|
[
|
||||||
'Search',
|
'Search',
|
||||||
{ type: composeState.tag?.type, term: composeState.tag?.text.substring(1) }
|
{
|
||||||
|
type: composeState.tag?.type,
|
||||||
|
term: composeState.tag?.text.substring(1)
|
||||||
|
}
|
||||||
],
|
],
|
||||||
searchFetch,
|
searchFetch,
|
||||||
{ enabled: false }
|
{ enabled: false }
|
||||||
@ -86,6 +172,128 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
}
|
}
|
||||||
}, [isFetching])
|
}, [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 (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<ProgressViewIOS
|
<ProgressViewIOS
|
||||||
@ -94,134 +302,12 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
|
|||||||
/>
|
/>
|
||||||
<FlatList
|
<FlatList
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={listHeader}
|
||||||
<>
|
ListFooterComponent={listFooter}
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty}
|
||||||
data={composeState.tag && isSuccess ? data[composeState.tag.type] : []}
|
data={data}
|
||||||
renderItem={({ item, index }) => (
|
keyExtractor={listKey}
|
||||||
<Pressable
|
renderItem={listItem}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<ComposeActions
|
<ComposeActions
|
||||||
textInputRef={textInputRef}
|
textInputRef={textInputRef}
|
||||||
@ -245,6 +331,10 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding
|
padding: StyleConstants.Spacing.Global.PagePadding
|
||||||
},
|
},
|
||||||
|
replyTo: {
|
||||||
|
flex: 1,
|
||||||
|
padding: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
},
|
||||||
suggestion: {
|
suggestion: {
|
||||||
flex: 1
|
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 { StyleSheet, Text, TextInput } from 'react-native'
|
||||||
import { StyleConstants } from 'src/utils/styles/constants'
|
import { StyleConstants } from 'src/utils/styles/constants'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
@ -8,13 +8,11 @@ import formatText from './formatText'
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
composeState: ComposeState
|
composeState: ComposeState
|
||||||
composeDispatch: Dispatch<PostAction>
|
composeDispatch: Dispatch<PostAction>
|
||||||
// textInputRef: RefObject<TextInput>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposeSpoilerInput: React.FC<Props> = ({
|
const ComposeSpoilerInput: React.FC<Props> = ({
|
||||||
composeState,
|
composeState,
|
||||||
composeDispatch,
|
composeDispatch,
|
||||||
// textInputRef
|
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
@ -46,7 +46,10 @@ const ComposeTextInput: React.FC<Props> = ({
|
|||||||
selection: { start, end }
|
selection: { start, end }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
composeDispatch({ type: 'text', payload: { selection: { start, end } } })
|
composeDispatch({
|
||||||
|
type: 'text',
|
||||||
|
payload: { selection: { start, end } }
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
ref={textInputRef}
|
ref={textInputRef}
|
||||||
scrollEnabled
|
scrollEnabled
|
||||||
@ -70,5 +73,6 @@ const styles = StyleSheet.create({
|
|||||||
export default React.memo(
|
export default React.memo(
|
||||||
ComposeTextInput,
|
ComposeTextInput,
|
||||||
(prev, next) =>
|
(prev, next) =>
|
||||||
|
prev.composeState.text.raw === next.composeState.text.raw &&
|
||||||
prev.composeState.text.formatted === next.composeState.text.formatted
|
prev.composeState.text.formatted === next.composeState.text.formatted
|
||||||
)
|
)
|
||||||
|
@ -19,5 +19,5 @@ export const searchFetch = async (
|
|||||||
url: 'search',
|
url: 'search',
|
||||||
params: { type, q: term, limit }
|
params: { type, q: term, limit }
|
||||||
})
|
})
|
||||||
return Promise.resolve(res.body)
|
return Promise.resolve(res.body[type])
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user