1
0
mirror of https://github.com/tooot-app/app synced 2025-05-22 06:24:19 +02:00

Reply working

This commit is contained in:
Zhiyuan Zheng 2020-12-13 01:24:25 +01:00
parent f0daae30cd
commit cfd2d40d02
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
13 changed files with 346 additions and 180 deletions

View File

@ -50,6 +50,7 @@ const Timeline: React.FC<Props> = ({
} }
] ]
const { const {
status,
isSuccess, isSuccess,
isLoading, isLoading,
isError, isError,
@ -109,14 +110,8 @@ const Timeline: React.FC<Props> = ({
[] []
) )
const flItemEmptyComponent = useMemo( const flItemEmptyComponent = useMemo(
() => ( () => <TimelineEmpty status={status} refetch={refetch} />,
<TimelineEmpty [isLoading, isError, isSuccess]
isLoading={isLoading}
isError={isError}
refetch={refetch}
/>
),
[isLoading, isError]
) )
const flOnRefresh = useCallback( const flOnRefresh = useCallback(
() => () =>
@ -124,7 +119,7 @@ const Timeline: React.FC<Props> = ({
fetchMore( fetchMore(
{ {
direction: 'prev', direction: 'prev',
id: flattenData[0].id id: flattenData.length ? flattenData[0].id : null
}, },
{ previous: true } { previous: true }
), ),
@ -142,11 +137,6 @@ const Timeline: React.FC<Props> = ({
) )
const flFooter = useCallback(() => { const flFooter = useCallback(() => {
return <TimelineEnd isFetchingMore={isFetchingMore} /> return <TimelineEnd isFetchingMore={isFetchingMore} />
// if (isFetchingMore) {
// return <ActivityIndicator />
// } else {
// return <TimelineEnd />
// }
}, [isFetchingMore]) }, [isFetchingMore])
const onScrollToIndexFailed = useCallback(error => { const onScrollToIndexFailed = useCallback(error => {
const offset = error.averageItemLength * error.index const offset = error.averageItemLength * error.index

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { useCallback, useMemo } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -6,40 +6,61 @@ import TimelineAvatar from './Shared/Avatar'
import TimelineHeaderConversation from './Shared/HeaderConversation' import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import TimelineActions from './Shared/Actions'
export interface Props { export interface Props {
item: Mastodon.Conversation item: Mastodon.Conversation
queryKey: App.QueryKey
highlighted?: boolean
} }
// Unread and mark as unread // Unread and mark as unread
const TimelineConversation: React.FC<Props> = ({ item }) => { const TimelineConversation: React.FC<Props> = ({
item,
queryKey,
highlighted = false
}) => {
const navigation = useNavigation() const navigation = useNavigation()
const conversationOnPress = useCallback(
() =>
item.last_status &&
navigation.navigate('Screen-Shared-Toot', {
toot: item.last_status
}),
[]
)
const conversationChildren = useMemo(() => {
return item.last_status && <TimelineContent status={item.last_status} />
}, [])
return ( return (
<View style={styles.statusView}> <View style={styles.statusView}>
<View style={styles.status}> <View style={styles.status}>
<TimelineAvatar account={item.accounts[0]} /> <TimelineAvatar account={item.accounts[0]} />
<View style={styles.details}> <View style={styles.details}>
<TimelineHeaderConversation <TimelineHeaderConversation
queryKey={queryKey}
id={item.id}
account={item.accounts[0]} account={item.accounts[0]}
created_at={item.last_status?.created_at} created_at={item.last_status?.created_at}
/> />
{/* Can pass toot info to next page to speed up performance */}
<Pressable <Pressable
onPress={() => onPress={conversationOnPress}
item.last_status && children={conversationChildren}
navigation.navigate('Screen-Shared-Toot', { />
toot: item.last_status.id
})
}
>
{item.last_status ? (
<TimelineContent status={item.last_status} />
) : (
<></>
)}
</Pressable>
</View> </View>
</View> </View>
<View
style={{
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
}}
>
<TimelineActions queryKey={queryKey} status={item.last_status!} />
</View>
</View> </View>
) )
} }

View File

@ -46,11 +46,14 @@ const TimelineDefault: React.FC<Props> = ({
const tootChildren = useMemo( const tootChildren = useMemo(
() => ( () => (
<View <View
style={{ style={[
paddingLeft: highlighted styles.content,
? 0 {
: StyleConstants.Avatar.S + StyleConstants.Spacing.S paddingLeft: highlighted
}} ? 0
: StyleConstants.Avatar.S + StyleConstants.Spacing.S
}
]}
> >
{actualStatus.content.length > 0 && ( {actualStatus.content.length > 0 && (
<TimelineContent status={actualStatus} highlighted={highlighted} /> <TimelineContent status={actualStatus} highlighted={highlighted} />
@ -98,11 +101,10 @@ const styles = StyleSheet.create({
header: { header: {
flex: 1, flex: 1,
width: '100%', width: '100%',
flexDirection: 'row', flexDirection: 'row'
marginBottom: StyleConstants.Spacing.S
}, },
content: { content: {
paddingLeft: StyleConstants.Avatar.S + StyleConstants.Spacing.S paddingTop: StyleConstants.Spacing.S
} }
}) })

View File

@ -1,35 +1,54 @@
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import React from 'react' import React, { useMemo } from 'react'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import { QueryStatus } from 'react-query'
import { ButtonRow } from 'src/components/Button' import { ButtonRow } from 'src/components/Button'
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'
export interface Props { export interface Props {
isLoading: boolean status: QueryStatus
isError: boolean
refetch: () => void refetch: () => void
} }
const TimelineEmpty: React.FC<Props> = ({ isLoading, isError, refetch }) => { const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( const children = useMemo(() => {
<View style={styles.base}> switch (status) {
{isLoading && <ActivityIndicator />} case 'loading':
{isError && ( return <ActivityIndicator />
<> case 'error':
<Feather return (
name='frown' <>
size={StyleConstants.Font.Size.L} <Feather
color={theme.primary} name='frown'
/> size={StyleConstants.Font.Size.L}
<Text style={[styles.error, { color: theme.primary }]}></Text> color={theme.primary}
<ButtonRow text='重试' onPress={() => refetch()} /> />
</> <Text style={[styles.error, { color: theme.primary }]}>
)}
</View> </Text>
) <ButtonRow text='重试' onPress={() => refetch()} />
</>
)
case 'success':
return (
<>
<Feather
name='smartphone'
size={StyleConstants.Font.Size.L}
color={theme.primary}
/>
<Text style={[styles.error, { color: theme.primary }]}>
</Text>
<ButtonRow text='刷新试试' onPress={() => refetch()} />
</>
)
}
}, [status])
return <View style={styles.base} children={children} />
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -92,7 +92,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
navigation.navigate(getCurrentTab(navigation), { navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose', screen: 'Screen-Shared-Compose',
params: { params: {
type: 'reply', type: status.visibility === 'direct' ? 'conversation' : 'reply',
incomingStatus: status incomingStatus: status
} }
}) })

View File

@ -1,65 +1,164 @@
import React from 'react' import { Feather } from '@expo/vector-icons'
import { StyleSheet, Text, View } from 'react-native' import React, { useCallback, useMemo } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryCache } from 'react-query'
import client from 'src/api/client'
import { toast } from 'src/components/toast'
import relativeTime from 'src/utils/relativeTime' import relativeTime from 'src/utils/relativeTime'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import Emojis from './Emojis' import Emojis from './Emojis'
export interface Props { export interface Props {
queryKey: App.QueryKey
id: string
account: Mastodon.Account account: Mastodon.Account
created_at?: Mastodon.Status['created_at'] created_at?: Mastodon.Status['created_at']
} }
const HeaderConversation: React.FC<Props> = ({ account, created_at }) => { const fireMutation = async ({ id }: { id: string }) => {
const res = await client({
method: 'delete',
instance: 'local',
url: `conversations/${id}`
})
console.log(res)
if (!res.body.error) {
toast({ type: 'success', content: '删除私信成功' })
return Promise.resolve()
} else {
toast({
type: 'error',
content: '删除私信失败,请重试',
autoHide: false
})
return Promise.reject()
}
}
const HeaderConversation: React.FC<Props> = ({
queryKey,
id,
account,
created_at
}) => {
const queryCache = useQueryCache()
const [mutateAction] = useMutation(fireMutation, {
onMutate: () => {
queryCache.cancelQueries(queryKey)
const oldData = queryCache.getQueryData(queryKey)
queryCache.setQueryData(queryKey, old =>
(old as {}[]).map((paging: any) => ({
toots: paging.toots.filter((toot: any) => toot.id !== id),
pointer: paging.pointer
}))
)
return oldData
},
onError: (err, _, oldData) => {
toast({ type: 'error', content: '请重试', autoHide: false })
queryCache.setQueryData(queryKey, oldData)
},
onSettled: () => {
queryCache.invalidateQueries(queryKey)
}
})
const { theme } = useTheme()
const actionOnPress = useCallback(() => mutateAction({ id }), [])
const actionChildren = useMemo(
() => (
<Feather
name='trash'
color={theme.secondary}
size={StyleConstants.Font.Size.M + 2}
/>
),
[]
)
return ( return (
<View> <View style={styles.base}>
<View style={styles.nameAndDate}> <View style={styles.nameAndDate}>
<View style={styles.name}> <View style={styles.name}>
{account.emojis ? ( {account.emojis ? (
<Emojis <Emojis
content={account.display_name || account.username} content={account.display_name || account.username}
emojis={account.emojis} emojis={account.emojis}
size={14} size={StyleConstants.Font.Size.M}
fontBold={true}
/> />
) : ( ) : (
<Text numberOfLines={1}> <Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{account.display_name || account.username} {account.display_name || account.username}
</Text> </Text>
)} )}
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
>
@{account.acct}
</Text>
</View> </View>
{created_at && ( {created_at && (
<View> <View style={styles.meta}>
<Text style={styles.created_at}>{relativeTime(created_at)}</Text> <Text style={[styles.created_at, { color: theme.secondary }]}>
{relativeTime(created_at)}
</Text>
</View> </View>
)} )}
</View> </View>
<Text style={styles.account} numberOfLines={1}>
@{account.acct} <Pressable
</Text> style={styles.action}
onPress={actionOnPress}
children={actionChildren}
/>
</View> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row'
},
nameAndDate: { nameAndDate: {
width: '100%', width: '80%'
flexDirection: 'row',
justifyContent: 'space-between'
}, },
name: { name: {
flexDirection: 'row', flexDirection: 'row'
marginRight: 8,
fontWeight: '900'
},
created_at: {
fontSize: 12,
lineHeight: 12,
marginTop: 8,
marginBottom: 8,
marginRight: 8
}, },
account: { account: {
lineHeight: 14, flexShrink: 1,
flexShrink: 1 marginLeft: StyleConstants.Spacing.XS
},
nameWithoutEmoji: {
fontSize: StyleConstants.Font.Size.M,
fontWeight: StyleConstants.Font.Weight.Bold
},
meta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
},
created_at: {
fontSize: StyleConstants.Font.Size.S
},
action: {
flexBasis: '20%',
flexDirection: 'row',
justifyContent: 'center'
} }
}) })

View File

@ -61,7 +61,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<View style={styles.nameAndAction}> <View style={styles.nameAndMeta}>
<View style={styles.name}> <View style={styles.name}>
{emojis?.length ? ( {emojis?.length ? (
<Emojis <Emojis
@ -85,41 +85,41 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
@{account} @{account}
</Text> </Text>
</View> </View>
{queryKey && ( <View style={styles.meta}>
<Pressable
style={styles.action}
onPress={onPressAction}
children={pressableAction}
/>
)}
</View>
<View style={styles.meta}>
<View>
<Text style={[styles.created_at, { color: theme.secondary }]}>
{since}
</Text>
</View>
{status.visibility === 'private' && (
<Feather
name='lock'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
style={styles.visibility}
/>
)}
{status.application && status.application.name !== 'Web' && (
<View> <View>
<Text <Text style={[styles.created_at, { color: theme.secondary }]}>
onPress={onPressApplication} {since}
style={[styles.application, { color: theme.secondary }]}
>
- {status.application.name}
</Text> </Text>
</View> </View>
)} {status.visibility === 'private' && (
<Feather
name='lock'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
style={styles.visibility}
/>
)}
{status.application && status.application.name !== 'Web' && (
<View>
<Text
onPress={onPressApplication}
style={[styles.application, { color: theme.secondary }]}
>
- {status.application.name}
</Text>
</View>
)}
</View>
</View> </View>
{queryKey && (
<Pressable
style={styles.action}
onPress={onPressAction}
children={pressableAction}
/>
)}
{queryKey && ( {queryKey && (
<BottomSheet <BottomSheet
visible={modalVisible} visible={modalVisible}
@ -157,29 +157,21 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
flex: 1
},
nameAndAction: {
flex: 1, flex: 1,
flexBasis: '100%', flexDirection: 'row'
flexDirection: 'row', },
alignItems: 'flex-start' nameAndMeta: {
flexBasis: '80%'
}, },
name: { name: {
flexBasis: '85%',
flexDirection: 'row' flexDirection: 'row'
}, },
nameWithoutEmoji: { nameWithoutEmoji: {
fontSize: StyleConstants.Font.Size.M, fontSize: StyleConstants.Font.Size.M,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}, },
action: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
account: { account: {
flexShrink: 1, flex: 1,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS
}, },
meta: { meta: {
@ -197,6 +189,11 @@ const styles = StyleSheet.create({
application: { application: {
fontSize: StyleConstants.Font.Size.S, fontSize: StyleConstants.Font.Size.S,
marginLeft: StyleConstants.Spacing.S marginLeft: StyleConstants.Spacing.S
},
action: {
flexBasis: '20%',
flexDirection: 'row',
justifyContent: 'center'
} }
}) })

View File

@ -126,31 +126,29 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<View style={{ flexBasis: '80%' }}> <View style={styles.nameAndMeta}>
<View style={styles.nameAndAction}> <View style={styles.name}>
<View style={styles.name}> {emojis?.length ? (
{emojis?.length ? ( <Emojis
<Emojis content={name}
content={name} emojis={emojis}
emojis={emojis} size={StyleConstants.Font.Size.M}
size={StyleConstants.Font.Size.M} fontBold={true}
fontBold={true} />
/> ) : (
) : (
<Text
numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
>
{name}
</Text>
)}
<Text <Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1} numberOfLines={1}
style={[styles.nameWithoutEmoji, { color: theme.primary }]}
> >
@{account} {name}
</Text> </Text>
</View> )}
<Text
style={[styles.account, { color: theme.secondary }]}
numberOfLines={1}
>
@{account}
</Text>
</View> </View>
<View style={styles.meta}> <View style={styles.meta}>
@ -180,6 +178,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
)} )}
</View> </View>
</View> </View>
{notification.type === 'follow' && ( {notification.type === 'follow' && (
<View style={styles.relationship}>{relationshipIcon}</View> <View style={styles.relationship}>{relationshipIcon}</View>
)} )}
@ -192,22 +191,16 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row' flexDirection: 'row'
}, },
nameAndAction: { nameAndMeta: {
width: '100%', width: '80%'
flexDirection: 'row',
justifyContent: 'space-between'
}, },
name: { name: {
flexBasis: '90%',
flexDirection: 'row' flexDirection: 'row'
}, },
nameWithoutEmoji: { nameWithoutEmoji: {
fontSize: StyleConstants.Font.Size.M, fontSize: StyleConstants.Font.Size.M,
fontWeight: StyleConstants.Font.Weight.Bold fontWeight: StyleConstants.Font.Weight.Bold
}, },
action: {
alignItems: 'flex-end'
},
account: { account: {
flexShrink: 1, flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS marginLeft: StyleConstants.Spacing.XS

View File

@ -96,7 +96,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
> >
<ParseContent <ParseContent
content={field.name} content={field.name}
size={StyleConstants.Font.Size.M} size={'M'}
emojis={account.emojis} emojis={account.emojis}
showFullLink showFullLink
/> />
@ -119,7 +119,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
> >
<ParseContent <ParseContent
content={field.value} content={field.value}
size={StyleConstants.Font.Size.M} size={'M'}
emojis={account.emojis} emojis={account.emojis}
showFullLink showFullLink
/> />
@ -133,7 +133,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
<View style={styles.note}> <View style={styles.note}>
<ParseContent <ParseContent
content={account.note} content={account.note}
size={StyleConstants.Font.Size.M} size={'M'}
emojis={account.emojis} emojis={account.emojis}
/> />
</View> </View>

View File

@ -6,7 +6,6 @@ import React, {
RefObject, RefObject,
useEffect, useEffect,
useReducer, useReducer,
useRef,
useState useState
} from 'react' } from 'react'
import { import {
@ -84,6 +83,7 @@ export type ComposeState = {
} }
attachmentUploadProgress?: { progress: number; aspect?: number } attachmentUploadProgress?: { progress: number; aspect?: number }
visibility: 'public' | 'unlisted' | 'private' | 'direct' visibility: 'public' | 'unlisted' | 'private' | 'direct'
visibilityLock: boolean
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
textInputFocus: { textInputFocus: {
current: 'text' | 'spoiler' current: 'text' | 'spoiler'
@ -167,6 +167,7 @@ const composeInitialState: ComposeState = {
getLocalAccountPreferences(store.getState())[ getLocalAccountPreferences(store.getState())[
'posting:default:visibility' 'posting:default:visibility'
] || 'public', ] || 'public',
visibilityLock: false,
replyToStatus: undefined, replyToStatus: undefined,
textInputFocus: { textInputFocus: {
current: 'text', current: 'text',
@ -177,7 +178,7 @@ const composeExistingState = ({
type, type,
incomingStatus incomingStatus
}: { }: {
type: 'reply' | 'edit' type: 'reply' | 'conversation' | 'edit'
incomingStatus: Mastodon.Status incomingStatus: Mastodon.Status
}): ComposeState => { }): ComposeState => {
switch (type) { switch (type) {
@ -222,11 +223,17 @@ const composeExistingState = ({
visibility: incomingStatus.visibility visibility: incomingStatus.visibility
} }
case 'reply': case 'reply':
const replyPlaceholder = `@${ case 'conversation':
incomingStatus.reblog const actualStatus = incomingStatus.reblog || incomingStatus
? incomingStatus.reblog.account.acct const allMentions = actualStatus.mentions.map(
: incomingStatus.account.acct mention => `@${mention.acct}`
} ` )
let replyPlaceholder = allMentions.join(' ')
if (replyPlaceholder.length === 0) {
replyPlaceholder = `@${actualStatus.account.acct} `
} else {
replyPlaceholder = replyPlaceholder + ' '
}
return { return {
...composeInitialState, ...composeInitialState,
text: { text: {
@ -235,6 +242,10 @@ const composeExistingState = ({
formatted: undefined, formatted: undefined,
selection: { start: 0, end: 0 } selection: { start: 0, end: 0 }
}, },
...(type === 'conversation' && {
visibility: 'direct',
visibilityLock: true
}),
replyToStatus: incomingStatus.reblog || incomingStatus replyToStatus: incomingStatus.reblog || incomingStatus
} }
} }
@ -290,7 +301,7 @@ export interface Props {
route: { route: {
params: params:
| { | {
type?: 'reply' | 'edit' type?: 'reply' | 'conversation' | 'edit'
incomingStatus: Mastodon.Status incomingStatus: Mastodon.Status
} }
| undefined | undefined
@ -349,14 +360,22 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
}) })
break break
case 'reply': case 'reply':
case 'conversation':
const actualStatus =
params.incomingStatus.reblog || params.incomingStatus
const allMentions = actualStatus.mentions.map(
mention => `@${mention.acct}`
)
let replyPlaceholder = allMentions.join(' ')
if (replyPlaceholder.length === 0) {
replyPlaceholder = `@${actualStatus.account.acct} `
} else {
replyPlaceholder = replyPlaceholder + ' '
}
formatText({ formatText({
textInput: 'text', textInput: 'text',
composeDispatch, composeDispatch,
content: `@${ content: replyPlaceholder,
params.incomingStatus.reblog
? params.incomingStatus.reblog.account.acct
: params.incomingStatus.account.acct
} `,
disableDebounce: true disableDebounce: true
}) })
break break
@ -374,6 +393,10 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
} else { } else {
const formData = new FormData() const formData = new FormData()
if (params?.type === 'conversation' || params?.type === 'reply') {
formData.append('in_reply_to_id', composeState.replyToStatus!.id)
}
if (composeState.spoiler.active) { if (composeState.spoiler.active) {
formData.append('spoiler_text', composeState.spoiler.raw) formData.append('spoiler_text', composeState.spoiler.raw)
} }
@ -469,6 +492,12 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
// doesn't work // doesn't work
const rawCount = composeState.text.raw.length const rawCount = composeState.text.raw.length
const postButtonText = {
conversation: '回复私信',
reply: '发布回复',
edit: '发嘟嘟'
}
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView <SafeAreaView
@ -513,7 +542,7 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
) : ( ) : (
<HeaderRight <HeaderRight
onPress={async () => tootPost()} onPress={async () => tootPost()}
text='发嘟嘟' text={params?.type ? postButtonText[params.type] : '发嘟嘟'}
disabled={rawCount < 1 || totalTextCount > 500} disabled={rawCount < 1 || totalTextCount > 500}
/> />
) )

View File

@ -84,6 +84,7 @@ const ComposeActions: React.FC = () => {
}, [composeState.visibility]) }, [composeState.visibility])
const visibilityOnPress = useCallback( const visibilityOnPress = useCallback(
() => () =>
!composeState.visibilityLock &&
ActionSheetIOS.showActionSheetWithOptions( ActionSheetIOS.showActionSheetWithOptions(
{ {
options: ['公开', '不公开', '仅关注着', '私信', '取消'], options: ['公开', '不公开', '仅关注着', '私信', '取消'],
@ -164,7 +165,7 @@ const ComposeActions: React.FC = () => {
<Feather <Feather
name={visibilityIcon} name={visibilityIcon}
size={24} size={24}
color={theme.secondary} color={composeState.visibilityLock ? theme.disabled : theme.secondary}
onPress={visibilityOnPress} onPress={visibilityOnPress}
/> />
<Feather <Feather

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native' import { Dimensions, StyleSheet, View } from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment' import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment'
@ -8,6 +8,7 @@ import TimelineCard from 'src/components/Timelines/Timeline/Shared/Card'
import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content' import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content'
import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault' import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault'
import { ComposeContext } from '../Compose' import { ComposeContext } from '../Compose'
import { StyleConstants } from 'src/utils/styles/constants'
const ComposeReply: React.FC = () => { const ComposeReply: React.FC = () => {
const { const {
@ -15,8 +16,14 @@ const ComposeReply: React.FC = () => {
} = useContext(ComposeContext) } = useContext(ComposeContext)
const { theme } = useTheme() const { theme } = useTheme()
const contentWidth =
Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 - // Global page padding on both sides
StyleConstants.Avatar.S - // Avatar width
StyleConstants.Spacing.S // Avatar margin to the right
return ( return (
<View style={styles.status}> <View style={[styles.status, { borderTopColor: theme.border }]}>
<TimelineAvatar account={replyToStatus!.account} /> <TimelineAvatar account={replyToStatus!.account} />
<View style={styles.details}> <View style={styles.details}>
<TimelineHeaderDefault status={replyToStatus!} /> <TimelineHeaderDefault status={replyToStatus!} />
@ -24,7 +31,7 @@ const ComposeReply: React.FC = () => {
<TimelineContent status={replyToStatus!} /> <TimelineContent status={replyToStatus!} />
)} )}
{replyToStatus!.media_attachments.length > 0 && ( {replyToStatus!.media_attachments.length > 0 && (
<TimelineAttachment status={replyToStatus!} width={200} /> <TimelineAttachment status={replyToStatus!} width={contentWidth} />
)} )}
{replyToStatus!.card && <TimelineCard card={replyToStatus!.card} />} {replyToStatus!.card && <TimelineCard card={replyToStatus!.card} />}
</View> </View>
@ -35,7 +42,9 @@ const ComposeReply: React.FC = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
status: { status: {
flex: 1, flex: 1,
flexDirection: 'row' flexDirection: 'row',
borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: StyleConstants.Spacing.Global.PagePadding
}, },
details: { details: {
flex: 1 flex: 1

View File

@ -143,6 +143,12 @@ export const timelineFetch = async (
url: `conversations`, url: `conversations`,
params params
}) })
if (pagination) {
// Bug in pull to refresh in conversations
res.body = res.body.filter(
(b: Mastodon.Conversation) => b.id !== pagination.id
)
}
return Promise.resolve({ toots: res.body, pointer: null }) return Promise.resolve({ toots: res.body, pointer: null })
case 'Bookmarks': case 'Bookmarks':