1
0
mirror of https://github.com/tooot-app/app synced 2025-04-16 19:27:29 +02:00
This commit is contained in:
xmflsct 2022-12-03 20:47:11 +01:00
parent 6a9f951dba
commit 20a55efb9c
22 changed files with 990 additions and 1175 deletions

View File

@ -168,6 +168,7 @@ export interface Props {
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean disableDetails?: boolean
selectable?: boolean selectable?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
} }
const ParseHTML = React.memo( const ParseHTML = React.memo(
@ -183,7 +184,8 @@ const ParseHTML = React.memo(
expandHint, expandHint,
highlighted = false, highlighted = false,
disableDetails = false, disableDetails = false,
selectable = false selectable = false,
setSpoilerExpanded
}: Props) => { }: Props) => {
const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale( const adaptedFontsize = adaptiveScale(
@ -253,6 +255,9 @@ const ParseHTML = React.memo(
onPress={() => { onPress={() => {
layoutAnimation() layoutAnimation()
setExpanded(!expanded) setExpanded(!expanded)
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
}} }}
style={{ style={{
flexDirection: 'row', flexDirection: 'row',

View File

@ -4,62 +4,24 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual } from 'lodash'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useMutation, useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineActions from './Shared/Actions' import TimelineActions from './Shared/Actions'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import StatusContext from './Shared/Context'
import TimelineHeaderConversation from './Shared/HeaderConversation' import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelinePoll from './Shared/Poll' import TimelinePoll from './Shared/Poll'
const Avatars: React.FC<{ accounts: Mastodon.Account[] }> = ({ accounts }) => {
return (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M,
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{accounts.slice(0, 4).map(account => (
<GracefullyImage
key={account.id}
uri={{ original: account.avatar, static: account.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height:
accounts.length > 2
? StyleConstants.Avatar.M / 2
: StyleConstants.Avatar.M
}}
style={{ flex: 1, flexBasis: '50%' }}
/>
))}
</View>
)
}
export interface Props { export interface Props {
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
highlighted?: boolean highlighted?: boolean
} }
const TimelineConversation = React.memo( const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlighted = false }) => {
({ conversation, queryKey, highlighted = false }: Props) => {
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const { colors } = useTheme() const { colors } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@ -75,8 +37,7 @@ const TimelineConversation = React.memo(
} }
}) })
const navigation = const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => { const onPress = useCallback(() => {
if (conversation.last_status) { if (conversation.last_status) {
conversation.unread && mutate() conversation.unread && mutate()
@ -88,6 +49,7 @@ const TimelineConversation = React.memo(
}, []) }, [])
return ( return (
<StatusContext.Provider value={{ queryKey, status: conversation.last_status }}>
<Pressable <Pressable
style={[ style={[
{ {
@ -100,19 +62,39 @@ const TimelineConversation = React.memo(
conversation.unread && { conversation.unread && {
borderLeftWidth: StyleConstants.Spacing.XS, borderLeftWidth: StyleConstants.Spacing.XS,
borderLeftColor: colors.blue, borderLeftColor: colors.blue,
paddingLeft: paddingLeft: StyleConstants.Spacing.Global.PagePadding - StyleConstants.Spacing.XS
StyleConstants.Spacing.Global.PagePadding -
StyleConstants.Spacing.XS
} }
]} ]}
onPress={onPress} onPress={onPress}
> >
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}> <View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<Avatars accounts={conversation.accounts} /> <View
<TimelineHeaderConversation style={{
queryKey={queryKey} borderRadius: 4,
conversation={conversation} overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M,
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{conversation.accounts.slice(0, 4).map(account => (
<GracefullyImage
key={account.id}
uri={{ original: account.avatar, static: account.avatar_static }}
dimension={{
width: StyleConstants.Avatar.M,
height:
conversation.accounts.length > 2
? StyleConstants.Avatar.M / 2
: StyleConstants.Avatar.M
}}
style={{ flex: 1, flexBasis: '50%' }}
/> />
))}
</View>
<TimelineHeaderConversation conversation={conversation} />
</View> </View>
{conversation.last_status ? ( {conversation.last_status ? (
@ -120,40 +102,19 @@ const TimelineConversation = React.memo(
<View <View
style={{ style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0, paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
? 0
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}} }}
> >
<TimelineContent <TimelineContent />
status={conversation.last_status} <TimelinePoll />
highlighted={highlighted}
/>
{conversation.last_status.poll ? (
<TimelinePoll
queryKey={queryKey}
statusId={conversation.last_status.id}
poll={conversation.last_status.poll}
reblog={false}
sameAccount={
conversation.last_status.id === instanceAccount?.id
}
/>
) : null}
</View> </View>
<TimelineActions
queryKey={queryKey} <TimelineActions />
status={conversation.last_status}
highlighted={highlighted}
accts={conversation.accounts.map(account => account.acct)}
reblog={false}
/>
</> </>
) : null} ) : null}
</Pressable> </Pressable>
</StatusContext.Provider>
) )
}, }
(prev, next) => isEqual(prev.conversation, next.conversation)
)
export default TimelineConversation export default TimelineConversation

View File

@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash' import React, { useRef, useState } from 'react'
import React, { useRef } from 'react'
import { Pressable, StyleProp, View, ViewStyle } from 'react-native' import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu' import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFeedback from './Shared/Feedback' import TimelineFeedback from './Shared/Feedback'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation' import TimelineFullConversation from './Shared/FullConversation'
@ -46,31 +46,28 @@ const TimelineDefault: React.FC<Props> = ({
disableOnPress = false disableOnPress = false
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualStatus = item.reblog ? item.reblog : item const instanceAccount = useSelector(getInstanceAccount, () => true)
const ownAccount = actualStatus.account?.id === instanceAccount?.id
const status = item.reblog ? item.reblog : item
const ownAccount = status.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false
)
const spoilerHidden = status.spoiler_text?.length
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({ const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '', content: '',
complete: false complete: false
}) })
const filtered = queryKey && shouldFilter({ copiableContent, status: actualStatus, queryKey }) const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
if (queryKey && filtered && !highlighted) { if (queryKey && filtered && !highlighted) {
return <TimelineFiltered phrase={filtered} /> return <TimelineFiltered phrase={filtered} />
} }
const onPress = () => {
if (highlighted) return
navigation.push('Tab-Shared-Toot', {
toot: actualStatus,
rootQueryKey: queryKey
})
}
const mainStyle: StyleProp<ViewStyle> = { const mainStyle: StyleProp<ViewStyle> = {
padding: StyleConstants.Spacing.Global.PagePadding, padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault, backgroundColor: colors.backgroundDefault,
@ -79,24 +76,14 @@ const TimelineDefault: React.FC<Props> = ({
const main = () => ( const main = () => (
<> <>
{item.reblog ? ( {item.reblog ? (
<TimelineActioned action='reblog' account={item.account} /> <TimelineActioned action='reblog' />
) : item._pinned ? ( ) : item._pinned ? (
<TimelineActioned action='pinned' account={item.account} /> <TimelineActioned action='pinned' />
) : null} ) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}> <View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar <TimelineAvatar />
queryKey={disableOnPress ? undefined : queryKey} <TimelineHeaderDefault />
account={actualStatus.account}
highlighted={highlighted}
/>
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={rootQueryKey}
status={actualStatus}
highlighted={highlighted}
copiableContent={copiableContent}
/>
</View> </View>
<View <View
@ -105,65 +92,44 @@ const TimelineDefault: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}} }}
> >
{typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? ( <TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelineContent <TimelinePoll />
status={actualStatus} <TimelineAttachment />
highlighted={highlighted} <TimelineCard />
disableDetails={disableDetails} <TimelineFullConversation />
/> <TimelineTranslate />
) : null} <TimelineFeedback />
{queryKey && actualStatus.poll ? (
<TimelinePoll
queryKey={queryKey}
rootQueryKey={rootQueryKey}
statusId={actualStatus.id}
poll={actualStatus.poll}
reblog={item.reblog ? true : false}
sameAccount={ownAccount}
/>
) : null}
{!disableDetails &&
Array.isArray(actualStatus.media_attachments) &&
actualStatus.media_attachments.length ? (
<TimelineAttachment status={actualStatus} />
) : null}
{!disableDetails && actualStatus.card ? <TimelineCard card={actualStatus.card} /> : null}
{!disableDetails ? (
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
) : null}
<TimelineTranslate status={actualStatus} highlighted={highlighted} />
<TimelineFeedback status={actualStatus} highlighted={highlighted} />
</View> </View>
{queryKey && !disableDetails ? ( <TimelineActions />
<TimelineActions
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={highlighted}
status={actualStatus}
ownAccount={ownAccount}
accts={uniqBy(
([actualStatus.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(actualStatus.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={item.reblog ? true : false}
/>
) : null}
</> </>
) )
const mShare = menuShare({ const mShare = menuShare({
visibility: actualStatus.visibility, visibility: status.visibility,
type: 'status', type: 'status',
url: actualStatus.url || actualStatus.uri, url: status.url || status.uri,
copiableContent copiableContent
}) })
const mStatus = menuStatus({ status: actualStatus, queryKey, rootQueryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status: actualStatus, queryKey, rootQueryKey }) const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return disableOnPress ? ( return (
<StatusContext.Provider
value={{
queryKey,
rootQueryKey,
status,
isReblog: !!item.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted,
disableDetails,
disableOnPress
}}
>
{disableOnPress ? (
<View style={mainStyle}>{main()}</View> <View style={mainStyle}>{main()}</View>
) : ( ) : (
<> <>
@ -172,7 +138,13 @@ const TimelineDefault: React.FC<Props> = ({
<Pressable <Pressable
accessible={highlighted ? false : true} accessible={highlighted ? false : true}
style={mainStyle} style={mainStyle}
onPress={onPress} disabled={highlighted}
onPress={() =>
navigation.push('Tab-Shared-Toot', {
toot: status,
rootQueryKey: queryKey
})
}
onLongPress={() => {}} onLongPress={() => {}}
children={main()} children={main()}
/> />
@ -213,12 +185,10 @@ const TimelineDefault: React.FC<Props> = ({
))} ))}
</ContextMenu.Content> </ContextMenu.Content>
</ContextMenu.Root> </ContextMenu.Root>
<TimelineHeaderAndroid <TimelineHeaderAndroid />
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={rootQueryKey}
status={actualStatus}
/>
</> </>
)}
</StatusContext.Provider>
) )
} }

View File

@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash' import React, { useCallback, useRef, useState } from 'react'
import React, { useCallback, useRef } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu' import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation' import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid' import TimelineHeaderAndroid from './Shared/HeaderAndroid'
@ -36,6 +36,17 @@ const TimelineNotifications: React.FC<Props> = ({
queryKey, queryKey,
highlighted = false highlighted = false
}) => { }) => {
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = notification.status
const account = notification.status ? notification.status.account : notification.account
const ownAccount = notification.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false
)
const spoilerHidden = notification.status?.spoiler_text?.length
? !instanceAccount.preferences['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({ const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '', content: '',
complete: false complete: false
@ -53,11 +64,8 @@ const TimelineNotifications: React.FC<Props> = ({
} }
const { colors } = useTheme() const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualAccount = notification.status ? notification.status.account : notification.account
const onPress = useCallback(() => { const onPress = useCallback(() => {
notification.status && notification.status &&
navigation.push('Tab-Shared-Toot', { navigation.push('Tab-Shared-Toot', {
@ -70,11 +78,7 @@ const TimelineNotifications: React.FC<Props> = ({
return ( return (
<> <>
{notification.type !== 'mention' ? ( {notification.type !== 'mention' ? (
<TimelineActioned <TimelineActioned action={notification.type} isNotification account={account} />
action={notification.type}
account={notification.account}
notification
/>
) : null} ) : null}
<View <View
@ -89,8 +93,8 @@ const TimelineNotifications: React.FC<Props> = ({
}} }}
> >
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}> <View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} /> <TimelineAvatar account={account} />
<TimelineHeaderNotification queryKey={queryKey} notification={notification} /> <TimelineHeaderNotification notification={notification} />
</View> </View>
{notification.status ? ( {notification.status ? (
@ -100,41 +104,16 @@ const TimelineNotifications: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}} }}
> >
{notification.status.content.length > 0 ? ( <TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelineContent status={notification.status} highlighted={highlighted} /> <TimelinePoll />
) : null} <TimelineAttachment />
{notification.status.poll ? ( <TimelineCard />
<TimelinePoll <TimelineFullConversation />
queryKey={queryKey}
statusId={notification.status.id}
poll={notification.status.poll}
reblog={false}
sameAccount={notification.account.id === instanceAccount?.id}
/>
) : null}
{notification.status.media_attachments.length > 0 ? (
<TimelineAttachment status={notification.status} />
) : null}
{notification.status.card ? <TimelineCard card={notification.status.card} /> : null}
<TimelineFullConversation queryKey={queryKey} status={notification.status} />
</View> </View>
) : null} ) : null}
</View> </View>
{notification.status ? ( <TimelineActions />
<TimelineActions
queryKey={queryKey}
status={notification.status}
highlighted={highlighted}
accts={uniqBy(
([notification.status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(notification.status.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)}
reblog={false}
/>
) : null}
</> </>
) )
} }
@ -149,7 +128,17 @@ const TimelineNotifications: React.FC<Props> = ({
const mInstance = menuInstance({ status: notification.status, queryKey }) const mInstance = menuInstance({ status: notification.status, queryKey })
return ( return (
<> <StatusContext.Provider
value={{
queryKey,
status,
isReblog: !!status?.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted
}}
>
<ContextMenu.Root> <ContextMenu.Root>
<ContextMenu.Trigger> <ContextMenu.Trigger>
<Pressable <Pressable
@ -199,8 +188,8 @@ const TimelineNotifications: React.FC<Props> = ({
))} ))}
</ContextMenu.Content> </ContextMenu.Content>
</ContextMenu.Root> </ContextMenu.Root>
<TimelineHeaderAndroid queryKey={queryKey} status={notification.status} /> <TimelineHeaderAndroid />
</> </StatusContext.Provider>
) )
} }

View File

@ -5,18 +5,22 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props { export interface Props {
account: Mastodon.Account action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
action: Mastodon.Notification['type'] | ('reblog' | 'pinned') isNotification?: boolean
notification?: boolean account?: Mastodon.Account
} }
const TimelineActioned = React.memo( const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
({ account, action, notification = false }: Props) => { const { status } = useContext(StatusContext)
const account = isNotification ? rest.account : status?.account
if (!status || !account) return null
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -108,7 +112,7 @@ const TimelineActioned = React.memo(
/> />
<Pressable onPress={onPress}> <Pressable onPress={onPress}>
{content( {content(
notification isNotification
? t('shared.actioned.reblog.notification', { name }) ? t('shared.actioned.reblog.notification', { name })
: t('shared.actioned.reblog.default', { name }) : t('shared.actioned.reblog.default', { name })
)} )}
@ -155,13 +159,10 @@ const TimelineActioned = React.memo(
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S, paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding paddingRight: StyleConstants.Spacing.Global.PagePadding
}} }}
> children={children()}
{children()} />
</View>
)
},
() => true
) )
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {

View File

@ -10,32 +10,22 @@ import {
QueryKeyTimeline, QueryKeyTimeline,
useTimelineMutation useTimelineMutation
} from '@utils/queryHooks/timeline' } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react' import { uniqBy } from 'lodash'
import React, { useCallback, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props { const TimelineActions: React.FC = () => {
queryKey: QueryKeyTimeline const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } =
rootQueryKey?: QueryKeyTimeline useContext(StatusContext)
highlighted: boolean if (!queryKey || !status || disableDetails) return null
status: Mastodon.Status
ownAccount?: boolean
accts: Mastodon.Account['acct'][] // When replying to conversations
reblog: boolean
}
const TimelineActions: React.FC<Props> = ({
queryKey,
rootQueryKey,
highlighted,
status,
ownAccount = false,
accts,
reblog
}) => {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>() const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
@ -83,16 +73,21 @@ const TimelineActions: React.FC<Props> = ({
} }
}) })
const onPressReply = useCallback( const instanceAccount = useSelector(getInstanceAccount, () => true)
() => const onPressReply = useCallback(() => {
const accts = uniqBy(
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(status.mentions)
.filter(d => d?.id !== instanceAccount?.id),
d => d?.id
).map(d => d?.acct)
navigation.navigate('Screen-Compose', { navigation.navigate('Screen-Compose', {
type: 'reply', type: 'reply',
incomingStatus: status, incomingStatus: status,
accts, accts,
queryKey queryKey
}), })
[status.replies_count] }, [status.replies_count])
)
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const onPressReblog = useCallback(() => { const onPressReblog = useCallback(() => {
if (!status.reblogged) { if (!status.reblogged) {
@ -114,7 +109,7 @@ const TimelineActions: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@ -130,7 +125,7 @@ const TimelineActions: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@ -149,7 +144,7 @@ const TimelineActions: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@ -166,7 +161,7 @@ const TimelineActions: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'favourited', property: 'favourited',
currentValue: status.favourited, currentValue: status.favourited,
@ -181,7 +176,7 @@ const TimelineActions: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'bookmarked', property: 'bookmarked',
currentValue: status.bookmarked, currentValue: status.bookmarked,

View File

@ -10,17 +10,22 @@ import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useState } from 'react' import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props { const TimelineAttachment = () => {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'> const { status, disableDetails } = useContext(StatusContext)
} if (
!status ||
disableDetails ||
!Array.isArray(status.media_attachments) ||
!status.media_attachments.length
)
return null
const TimelineAttachment = React.memo(
({ status }: Props) => {
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const account = useSelector( const account = useSelector(
@ -41,8 +46,7 @@ const TimelineAttachment = React.memo(
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
// @ts-ignore // @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
status.media_attachments
.map(attachment => { .map(attachment => {
switch (attachment.type) { switch (attachment.type) {
case 'image': case 'image':
@ -219,23 +223,6 @@ const TimelineAttachment = React.memo(
))} ))}
</View> </View>
) )
},
(prev, next) => {
let isEqual = true
if (prev.status.media_attachments.length !== next.status.media_attachments.length) {
isEqual = false
return isEqual
} }
prev.status.media_attachments.forEach((attachment, index) => {
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) {
isEqual = false
}
})
return isEqual
}
)
export default TimelineAttachment export default TimelineAttachment

View File

@ -2,37 +2,37 @@ import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import StatusContext from './Context'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline account?: Mastodon.Account
account: Mastodon.Account
highlighted: boolean
} }
const TimelineAvatar = React.memo(({ queryKey, account, highlighted }: Props) => { const TimelineAvatar: React.FC<Props> = ({ account }) => {
const { status, highlighted, disableOnPress } = useContext(StatusContext)
const actualAccount = account || status?.account
if (!actualAccount) return null
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
// Need to fix go back root
const onPress = useCallback(() => {
queryKey && navigation.push('Tab-Shared-Account', { account })
}, [])
return ( return (
<GracefullyImage <GracefullyImage
{...(highlighted && { {...(highlighted && {
accessibilityLabel: t('shared.avatar.accessibilityLabel', { accessibilityLabel: t('shared.avatar.accessibilityLabel', {
name: account.display_name name: actualAccount.display_name
}), }),
accessibilityHint: t('shared.avatar.accessibilityHint', { accessibilityHint: t('shared.avatar.accessibilityHint', {
name: account.display_name name: actualAccount.display_name
}) })
})} })}
onPress={onPress} onPress={() =>
uri={{ original: account?.avatar, static: account?.avatar_static }} !disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
}
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
dimension={{ dimension={{
width: StyleConstants.Avatar.M, width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M height: StyleConstants.Avatar.M
@ -44,6 +44,6 @@ const TimelineAvatar = React.memo(({ queryKey, account, highlighted }: Props) =>
}} }}
/> />
) )
}) }
export default TimelineAvatar export default TimelineAvatar

View File

@ -9,23 +9,23 @@ import { useSearchQuery } from '@utils/queryHooks/search'
import { useStatusQuery } from '@utils/queryHooks/status' import { useStatusQuery } from '@utils/queryHooks/status'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
import TimelineDefault from '../Default' import TimelineDefault from '../Default'
import StatusContext from './Context'
export interface Props { const TimelineCard: React.FC = () => {
card: Pick<Mastodon.Card, 'url' | 'image' | 'blurhash' | 'title' | 'description'> const { status, spoilerHidden, disableDetails } = useContext(StatusContext)
} if (!status || !status.card) return null
const TimelineCard = React.memo(({ card }: Props) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const isStatus = matchStatus(card.url) const isStatus = matchStatus(status.card.url)
const [foundStatus, setFoundStatus] = useState<Mastodon.Status>() const [foundStatus, setFoundStatus] = useState<Mastodon.Status>()
const isAccount = matchAccount(card.url) const isAccount = matchAccount(status.card.url)
const [foundAccount, setFoundAccount] = useState<Mastodon.Account>() const [foundAccount, setFoundAccount] = useState<Mastodon.Account>()
const searchQuery = useSearchQuery({ const searchQuery = useSearchQuery({
@ -38,7 +38,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
if (isStatus.sameInstance) { if (isStatus.sameInstance) {
return return
} else { } else {
return card.url return status.card.url
} }
} }
if (isAccount) { if (isAccount) {
@ -49,7 +49,7 @@ const TimelineCard = React.memo(({ card }: Props) => {
return isAccount.username return isAccount.username
} }
} else { } else {
return card.url return status.card.url
} }
} }
})(), })(),
@ -136,10 +136,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
} }
return ( return (
<> <>
{card.image ? ( {status.card?.image ? (
<GracefullyImage <GracefullyImage
uri={{ original: card.image }} uri={{ original: status.card.image }}
blurhash={card.blurhash} blurhash={status.card.blurhash}
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }} style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }} imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
/> />
@ -155,9 +155,9 @@ const TimelineCard = React.memo(({ card }: Props) => {
fontWeight='Bold' fontWeight='Bold'
testID='title' testID='title'
> >
{card.title} {status.card?.title}
</CustomText> </CustomText>
{card.description ? ( {status.card?.description ? (
<CustomText <CustomText
fontStyle='S' fontStyle='S'
numberOfLines={1} numberOfLines={1}
@ -167,17 +167,19 @@ const TimelineCard = React.memo(({ card }: Props) => {
}} }}
testID='description' testID='description'
> >
{card.description} {status.card.description}
</CustomText> </CustomText>
) : null} ) : null}
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}> <CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
{card.url} {status.card?.url}
</CustomText> </CustomText>
</View> </View>
</> </>
) )
} }
if (spoilerHidden || disableDetails) return null
return ( return (
<Pressable <Pressable
accessible accessible
@ -192,10 +194,10 @@ const TimelineCard = React.memo(({ card }: Props) => {
overflow: 'hidden', overflow: 'hidden',
borderColor: colors.border borderColor: colors.border
}} }}
onPress={async () => await openLink(card.url, navigation)} onPress={async () => status.card && (await openLink(status.card.url, navigation))}
children={cardContent} children={cardContent()}
/> />
) )
}) }
export default TimelineCard export default TimelineCard

View File

@ -1,26 +1,24 @@
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import { getInstanceAccount } from '@utils/slices/instancesSlice' import { getInstanceAccount } from '@utils/slices/instancesSlice'
import React from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props { export interface Props {
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & { setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
mentions?: Mastodon.Status['mentions']
tags?: Mastodon.Status['tags']
}
highlighted?: boolean
disableDetails?: boolean
} }
const TimelineContent = React.memo( const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
({ status, highlighted = false, disableDetails = false }: Props) => { const { status, highlighted, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true) const instanceAccount = useSelector(getInstanceAccount, () => true)
return ( return (
<> <>
{status.spoiler_text ? ( {status.spoiler_text?.length ? (
<> <>
<ParseHTML <ParseHTML
content={status.spoiler_text} content={status.spoiler_text}
@ -42,6 +40,7 @@ const TimelineContent = React.memo(
tags={status.tags} tags={status.tags}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1} numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
expandHint={t('shared.content.expandHint')} expandHint={t('shared.content.expandHint')}
setSpoilerExpanded={setSpoilerExpanded}
highlighted={highlighted} highlighted={highlighted}
disableDetails={disableDetails} disableDetails={disableDetails}
/> />
@ -60,10 +59,6 @@ const TimelineContent = React.memo(
)} )}
</> </>
) )
}, }
(prev, next) =>
prev.status.content === next.status.content &&
prev.status.spoiler_text === next.status.spoiler_text
)
export default TimelineContent export default TimelineContent

View File

@ -0,0 +1,24 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { createContext } from 'react'
type ContextType = {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
status?: Mastodon.Status
isReblog?: boolean
ownAccount?: boolean
spoilerHidden?: boolean
copiableContent?: React.MutableRefObject<{
content: string
complete: boolean
}>
highlighted?: boolean
disableDetails?: boolean
disableOnPress?: boolean
}
const StatusContext = createContext<ContextType>({} as ContextType)
export default StatusContext

View File

@ -5,20 +5,14 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useStatusHistory } from '@utils/queryHooks/statusesHistory' import { useStatusHistory } from '@utils/queryHooks/statusesHistory'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props { const TimelineFeedback = () => {
status: Pick<Mastodon.Status, 'id' | 'edited_at' | 'reblogs_count' | 'favourites_count'> const { status, highlighted } = useContext(StatusContext)
highlighted: boolean if (!status || !highlighted) return null
}
const TimelineFeedback = React.memo(
({ status, highlighted }: Props) => {
if (!highlighted) {
return null
}
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
@ -96,12 +90,7 @@ const TimelineFeedback = React.memo(
</View> </View>
</View> </View>
) )
}, }
(prev, next) =>
prev.status.edited_at === next.status.edited_at &&
prev.status.reblogs_count === next.status.reblogs_count &&
prev.status.favourites_count === next.status.favourites_count
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
text: { text: {

View File

@ -1,17 +1,14 @@
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import StatusContext from './Context'
export interface Props { const TimelineFullConversation = () => {
queryKey?: QueryKeyTimeline const { queryKey, status, disableDetails } = useContext(StatusContext)
status: Mastodon.Status if (!status || disableDetails) return null
}
const TimelineFullConversation = React.memo(
({ queryKey, status }: Props) => {
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
@ -19,9 +16,7 @@ const TimelineFullConversation = React.memo(
queryKey[1].page !== 'Toot' && queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id && status.in_reply_to_account_id &&
(status.mentions.length === 0 || (status.mentions.length === 0 ||
status.mentions.filter( status.mentions.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
mention => mention.id !== status.in_reply_to_account_id
).length) ? (
<CustomText <CustomText
fontStyle='S' fontStyle='S'
style={{ style={{
@ -32,8 +27,6 @@ const TimelineFullConversation = React.memo(
{t('shared.fullConversation')} {t('shared.fullConversation')}
</CustomText> </CustomText>
) : null ) : null
}, }
() => true
)
export default TimelineFullConversation export default TimelineFullConversation

View File

@ -3,21 +3,18 @@ import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share' import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status' import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react' import React, { useContext, useState } from 'react'
import { Platform, View } from 'react-native' import { Platform, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
export interface Props { const TimelineHeaderAndroid: React.FC = () => {
queryKey?: QueryKeyTimeline const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } =
rootQueryKey?: QueryKeyTimeline useContext(StatusContext)
status?: Mastodon.Status
}
const TimelineHeaderAndroid: React.FC<Props> = ({ queryKey, rootQueryKey, status }) => { if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
if (Platform.OS !== 'android' || !status) return null
const { colors } = useTheme() const { colors } = useTheme()

View File

@ -2,46 +2,25 @@ import Icon from '@components/Icon'
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import { ParseEmojis } from '@components/Parse' import { ParseEmojis } from '@components/Parse'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline' import { useTimelineMutation } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import StatusContext from './Context'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'
const Names = ({ accounts }: { accounts: Mastodon.Account[] }) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return (
<CustomText
numberOfLines={1}
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
>
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
{accounts.map((account, index) => (
<CustomText key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</CustomText>
))}
</CustomText>
)
}
export interface Props { export interface Props {
queryKey: QueryKeyTimeline
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
} }
const HeaderConversation = ({ queryKey, conversation }: Props) => { const HeaderConversation = ({ conversation }: Props) => {
const { queryKey } = useContext(StatusContext)
if (!queryKey) return null
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
@ -70,7 +49,22 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
return ( return (
<View style={{ flex: 1, flexDirection: 'row' }}> <View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<Names accounts={conversation.accounts} /> <CustomText
numberOfLines={1}
style={{ ...StyleConstants.FontStyle.M, color: colors.secondary }}
>
<CustomText>{t('shared.header.conversation.withAccounts')}</CustomText>
{conversation.accounts.map((account, index) => (
<CustomText key={account.id} numberOfLines={1}>
{index !== 0 ? t('common:separator') : undefined}
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</CustomText>
))}
</CustomText>
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',

View File

@ -3,37 +3,24 @@ import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share' import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status' import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react' import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Platform, Pressable, View } from 'react-native' import { Platform, Pressable, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility' import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props { const TimelineHeaderDefault: React.FC = () => {
queryKey?: QueryKeyTimeline const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
rootQueryKey?: QueryKeyTimeline useContext(StatusContext)
status: Mastodon.Status if (!status) return null
highlighted: boolean
copiableContent: React.MutableRefObject<{
content: string
complete: boolean
}>
}
const TimelineHeaderDefault: React.FC<Props> = ({
queryKey,
rootQueryKey,
status,
highlighted,
copiableContent
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('componentContextMenu') const { t } = useTranslation('componentContextMenu')
@ -76,7 +63,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
</View> </View>
</View> </View>
{Platform.OS !== 'android' && queryKey ? ( {Platform.OS !== 'android' && !disableDetails ? (
<Pressable <Pressable
accessibilityHint={t('accessibilityHint')} accessibilityHint={t('accessibilityHint')}
style={{ flex: 1, alignItems: 'center' }} style={{ flex: 1, alignItems: 'center' }}

View File

@ -4,40 +4,41 @@ import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status' import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship' import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useState } from 'react' import React, { useContext, useState } from 'react'
import { Platform, Pressable, View } from 'react-native' import { Platform, Pressable, View } from 'react-native'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted' import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility' import HeaderSharedVisibility from './HeaderShared/Visibility'
export interface Props { export type Props = {
queryKey: QueryKeyTimeline
notification: Mastodon.Notification notification: Mastodon.Notification
} }
const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const { queryKey, status } = useContext(StatusContext)
const { colors } = useTheme() const { colors } = useTheme()
const [openChange, setOpenChange] = useState(false) const [openChange, setOpenChange] = useState(false)
const mShare = menuShare({ const mShare = menuShare({
visibility: notification.status?.visibility, visibility: status?.visibility,
type: 'status', type: 'status',
url: notification.status?.url || notification.status?.uri url: status?.url || status?.uri
}) })
const mAccount = menuAccount({ const mAccount = menuAccount({
type: 'status', type: 'status',
openChange, openChange,
account: notification.status?.account, account: status?.account,
queryKey queryKey
}) })
const mStatus = menuStatus({ status: notification.status, queryKey }) const mStatus = menuStatus({ status, queryKey })
const mInstance = menuInstance({ status: notification.status, queryKey }) const mInstance = menuInstance({ status, queryKey })
const actions = () => { const actions = () => {
switch (notification.type) { switch (notification.type) {
@ -46,7 +47,7 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
case 'follow_request': case 'follow_request':
return <RelationshipIncoming id={notification.account.id} /> return <RelationshipIncoming id={notification.account.id} />
default: default:
if (notification.status) { if (status) {
return ( return (
<Pressable <Pressable
style={{ flex: 1, alignItems: 'center' }} style={{ flex: 1, alignItems: 'center' }}

View File

@ -7,39 +7,28 @@ import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { import {
MutationVarsTimelineUpdateStatusProperty, MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation useTimelineMutation
} from '@utils/queryHooks/timeline' } from '@utils/queryHooks/timeline'
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty' import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy } from 'lodash' import { maxBy } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useContext, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import StatusContext from './Context'
export interface Props { const TimelinePoll: React.FC = () => {
queryKey: QueryKeyTimeline const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } =
rootQueryKey?: QueryKeyTimeline useContext(StatusContext)
statusId: Mastodon.Status['id'] if (!queryKey || !status || !status.poll) return null
poll: NonNullable<Mastodon.Status['poll']> const poll = status.poll
reblog: boolean
sameAccount: boolean
}
const TimelinePoll: React.FC<Props> = ({
queryKey,
rootQueryKey,
statusId,
poll,
reblog,
sameAccount
}) => {
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentTimeline') const { t, i18n } = useTranslation('componentTimeline')
const [allOptions, setAllOptions] = useState(new Array(poll.options.length).fill(false)) const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useTimelineMutation({ const mutation = useTimelineMutation({
@ -79,7 +68,7 @@ const TimelinePoll: React.FC<Props> = ({
const pollButton = useMemo(() => { const pollButton = useMemo(() => {
if (!poll.expired) { if (!poll.expired) {
if (!sameAccount && !poll.voted) { if (!ownAccount && !poll.voted) {
return ( return (
<View style={{ marginRight: StyleConstants.Spacing.S }}> <View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button <Button
@ -88,8 +77,8 @@ const TimelinePoll: React.FC<Props> = ({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: statusId, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'poll', property: 'poll',
id: poll.id, id: poll.id,
@ -114,8 +103,8 @@ const TimelinePoll: React.FC<Props> = ({
type: 'updateStatusProperty', type: 'updateStatusProperty',
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: statusId, id: status.id,
reblog, isReblog,
payload: { payload: {
property: 'poll', property: 'poll',
id: poll.id, id: poll.id,
@ -258,6 +247,8 @@ const TimelinePoll: React.FC<Props> = ({
} }
} }
if (spoilerHidden || disableDetails) return null
return ( return (
<View style={{ marginTop: StyleConstants.Spacing.M }}> <View style={{ marginTop: StyleConstants.Spacing.M }}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow} {poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}

View File

@ -5,22 +5,16 @@ import { useTranslateQuery } from '@utils/queryHooks/translate'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization' import * as Localization from 'expo-localization'
import React, { useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable } from 'react-native' import { Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
import detectLanguage from 'react-native-language-detection' import detectLanguage from 'react-native-language-detection'
import StatusContext from './Context'
export interface Props { const TimelineTranslate = () => {
highlighted: boolean const { status, highlighted } = useContext(StatusContext)
status: Pick<Mastodon.Status, 'language' | 'spoiler_text' | 'content' | 'emojis'> if (!status || !highlighted) return null
}
const TimelineTranslate = React.memo(
({ highlighted, status }: Props) => {
if (!highlighted) {
return null
}
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
@ -120,16 +114,10 @@ const TimelineTranslate = React.memo(
) : null} ) : null}
</Pressable> </Pressable>
{data && data.error === undefined {data && data.error === undefined
? data.text.map((d, i) => ( ? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
<ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />
))
: null} : null}
</> </>
) )
}, }
(prev, next) =>
prev.status.content === next.status.content &&
prev.status.spoiler_text === next.status.spoiler_text
)
export default TimelineTranslate export default TimelineTranslate

View File

@ -2,10 +2,7 @@ import apiInstance, { InstanceResponse } from '@api/instance'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import { store } from '@root/store' import { store } from '@root/store'
import { import { checkInstanceFeature, getInstanceNotificationsFilter } from '@utils/slices/instancesSlice'
checkInstanceFeature,
getInstanceNotificationsFilter
} from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
import { import {
@ -30,10 +27,7 @@ export type QueryKeyTimeline = [
} }
] ]
const queryFunction = async ({ const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyTimeline>) => {
queryKey,
pageParam
}: QueryFunctionContext<QueryKeyTimeline>) => {
const { page, account, hashtag, list, toot } = queryKey[1] const { page, account, hashtag, list, toot } = queryKey[1]
let params: { [key: string]: string } = { ...pageParam } let params: { [key: string]: string } = { ...pageParam }
@ -65,9 +59,9 @@ const queryFunction = async ({
case 'Notifications': case 'Notifications':
const rootStore = store.getState() const rootStore = store.getState()
const notificationsFilter = getInstanceNotificationsFilter(rootStore) const notificationsFilter = getInstanceNotificationsFilter(rootStore)
const usePositiveFilter = checkInstanceFeature( const usePositiveFilter = checkInstanceFeature('notification_types_positive_filter')(
'notification_types_positive_filter' rootStore
)(rootStore) )
return apiInstance<Mastodon.Notification[]>({ return apiInstance<Mastodon.Notification[]>({
method: 'get', method: 'get',
url: 'notifications', url: 'notifications',
@ -99,9 +93,7 @@ const queryFunction = async ({
} }
}) })
} else { } else {
const res1 = await apiInstance< const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
(Mastodon.Status & { _pinned: boolean })[]
>({
method: 'get', method: 'get',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
@ -190,11 +182,7 @@ const queryFunction = async ({
url: `statuses/${toot}/context` url: `statuses/${toot}/context`
}) })
return { return {
body: [ body: [...res2_1.body.ancestors, res1_1.body, ...res2_1.body.descendants]
...res2_1.body.ancestors,
res1_1.body,
...res2_1.body.descendants
]
} }
default: default:
return Promise.reject() return Promise.reject()
@ -207,10 +195,7 @@ const useTimelineQuery = ({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKeyTimeline[1] & { }: QueryKeyTimeline[1] & {
options?: UseInfiniteQueryOptions< options?: UseInfiniteQueryOptions<InstanceResponse<Mastodon.Status[]>, AxiosError>
InstanceResponse<Mastodon.Status[]>,
AxiosError
>
}) => { }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }] const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunction, { return useInfiniteQuery(queryKey, queryFunction, {
@ -284,7 +269,7 @@ export type MutationVarsTimelineUpdateStatusProperty = {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline
id: Mastodon.Status['id'] | Mastodon.Poll['id'] id: Mastodon.Status['id'] | Mastodon.Poll['id']
reblog?: boolean isReblog?: boolean
payload: payload:
| { | {
property: 'bookmarked' | 'muted' | 'pinned' property: 'bookmarked' | 'muted' | 'pinned'
@ -384,9 +369,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
} }
return apiInstance<Mastodon.Status>({ return apiInstance<Mastodon.Status>({
method: 'post', method: 'post',
url: `statuses/${params.id}/${ url: `statuses/${params.id}/${params.payload.currentValue ? 'un' : ''}${
params.payload.currentValue ? 'un' : '' MapPropertyToUrl[params.payload.property]
}${MapPropertyToUrl[params.payload.property]}`, }`,
...(params.payload.property === 'reblogged' && { body }) ...(params.payload.property === 'reblogged' && { body })
}) })
} }
@ -396,9 +381,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
case 'mute': case 'mute':
return apiInstance<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
method: 'post', method: 'post',
url: `accounts/${params.id}/${ url: `accounts/${params.id}/${params.payload.currentValue ? 'un' : ''}${
params.payload.currentValue ? 'un' : '' params.payload.property
}${params.payload.property}` }`
}) })
case 'reports': case 'reports':
return apiInstance<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
@ -455,8 +440,7 @@ const useTimelineMutation = ({
...(onMutate && { ...(onMutate && {
onMutate: params => { onMutate: params => {
queryClient.cancelQueries(params.queryKey) queryClient.cancelQueries(params.queryKey)
const oldData = const oldData = params.queryKey && queryClient.getQueryData(params.queryKey)
params.queryKey && queryClient.getQueryData(params.queryKey)
haptics('Light') haptics('Light')
switch (params.type) { switch (params.type) {

View File

@ -2,32 +2,27 @@ import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/time
const updateStatus = ({ const updateStatus = ({
item, item,
reblog, isReblog,
payload payload
}: { }: {
item: Mastodon.Status item: Mastodon.Status
reblog?: boolean isReblog?: boolean
payload: MutationVarsTimelineUpdateStatusProperty['payload'] payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => { }) => {
switch (payload.property) { switch (payload.property) {
case 'poll': case 'poll':
if (reblog) { if (isReblog) {
item.reblog!.poll = payload.data item.reblog!.poll = payload.data
} else { } else {
item.poll = payload.data item.poll = payload.data
} }
break break
default: default:
if (reblog) { if (isReblog) {
item.reblog![payload.property] = item.reblog![payload.property] =
typeof payload.currentValue === 'boolean' typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
? !payload.currentValue
: true
if (payload.propertyCount) { if (payload.propertyCount) {
if ( if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
typeof payload.currentValue === 'boolean' &&
payload.currentValue
) {
item.reblog![payload.propertyCount] = payload.countValue - 1 item.reblog![payload.propertyCount] = payload.countValue - 1
} else { } else {
item.reblog![payload.propertyCount] = payload.countValue + 1 item.reblog![payload.propertyCount] = payload.countValue + 1
@ -35,14 +30,9 @@ const updateStatus = ({
} }
} else { } else {
item[payload.property] = item[payload.property] =
typeof payload.currentValue === 'boolean' typeof payload.currentValue === 'boolean' ? !payload.currentValue : true
? !payload.currentValue
: true
if (payload.propertyCount) { if (payload.propertyCount) {
if ( if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
typeof payload.currentValue === 'boolean' &&
payload.currentValue
) {
item[payload.propertyCount] = payload.countValue - 1 item[payload.propertyCount] = payload.countValue - 1
} else { } else {
item[payload.propertyCount] = payload.countValue + 1 item[payload.propertyCount] = payload.countValue + 1

View File

@ -1,9 +1,6 @@
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query' import { InfiniteData } from 'react-query'
import { import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
MutationVarsTimelineUpdateStatusProperty,
TimelineData
} from '../timeline'
import updateConversation from './update/conversation' import updateConversation from './update/conversation'
import updateNotification from './update/notification' import updateNotification from './update/notification'
import updateStatus from './update/status' import updateStatus from './update/status'
@ -12,12 +9,10 @@ const updateStatusProperty = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id, id,
reblog, isReblog,
payload payload
}: MutationVarsTimelineUpdateStatusProperty) => { }: MutationVarsTimelineUpdateStatusProperty) => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, old => {
queryKey,
old => {
if (old) { if (old) {
let foundToot = false let foundToot = false
old.pages = old.pages.map(page => { old.pages = old.pages.map(page => {
@ -25,39 +20,30 @@ const updateStatusProperty = ({
if (foundToot) { if (foundToot) {
return page return page
} else { } else {
if ( if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
typeof (page.body as Mastodon.Conversation[])[0].unread ===
'boolean'
) {
const items = page.body as Mastodon.Conversation[] const items = page.body as Mastodon.Conversation[]
const tootIndex = items.findIndex( const tootIndex = items.findIndex(({ last_status }) => last_status?.id === id)
({ last_status }) => last_status?.id === id
)
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateConversation({ item: items[tootIndex], payload }) updateConversation({ item: items[tootIndex], payload })
} }
return page return page
} else if ( } else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
typeof (page.body as Mastodon.Notification[])[0].type === 'string'
) {
const items = page.body as Mastodon.Notification[] const items = page.body as Mastodon.Notification[]
const tootIndex = items.findIndex( const tootIndex = items.findIndex(({ status }) => status?.id === id)
({ status }) => status?.id === id
)
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateNotification({ item: items[tootIndex], payload }) updateNotification({ item: items[tootIndex], payload })
} }
} else { } else {
const items = page.body as Mastodon.Status[] const items = page.body as Mastodon.Status[]
const tootIndex = reblog const tootIndex = isReblog
? items.findIndex(({ reblog }) => reblog?.id === id) ? items.findIndex(({ reblog }) => reblog?.id === id)
: items.findIndex(toot => toot.id === id) : items.findIndex(toot => toot.id === id)
// if favourites page and notifications page, remove the item instead // if favourites page and notifications page, remove the item instead
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateStatus({ item: items[tootIndex], reblog, payload }) updateStatus({ item: items[tootIndex], isReblog, payload })
} }
} }
@ -67,13 +53,10 @@ const updateStatusProperty = ({
} }
return old return old
} })
)
rootQueryKey && rootQueryKey &&
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(rootQueryKey, old => {
rootQueryKey,
old => {
if (old) { if (old) {
let foundToot = false let foundToot = false
old.pages = old.pages.map(page => { old.pages = old.pages.map(page => {
@ -81,40 +64,30 @@ const updateStatusProperty = ({
if (foundToot) { if (foundToot) {
return page return page
} else { } else {
if ( if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
typeof (page.body as Mastodon.Conversation[])[0].unread ===
'boolean'
) {
const items = page.body as Mastodon.Conversation[] const items = page.body as Mastodon.Conversation[]
const tootIndex = items.findIndex( const tootIndex = items.findIndex(({ last_status }) => last_status?.id === id)
({ last_status }) => last_status?.id === id
)
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateConversation({ item: items[tootIndex], payload }) updateConversation({ item: items[tootIndex], payload })
} }
return page return page
} else if ( } else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
typeof (page.body as Mastodon.Notification[])[0].type ===
'string'
) {
const items = page.body as Mastodon.Notification[] const items = page.body as Mastodon.Notification[]
const tootIndex = items.findIndex( const tootIndex = items.findIndex(({ status }) => status?.id === id)
({ status }) => status?.id === id
)
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateNotification({ item: items[tootIndex], payload }) updateNotification({ item: items[tootIndex], payload })
} }
} else { } else {
const items = page.body as Mastodon.Status[] const items = page.body as Mastodon.Status[]
const tootIndex = reblog const tootIndex = isReblog
? items.findIndex(({ reblog }) => reblog?.id === id) ? items.findIndex(({ reblog }) => reblog?.id === id)
: items.findIndex(toot => toot.id === id) : items.findIndex(toot => toot.id === id)
// if favourites page and notifications page, remove the item instead // if favourites page and notifications page, remove the item instead
if (tootIndex >= 0) { if (tootIndex >= 0) {
foundToot = true foundToot = true
updateStatus({ item: items[tootIndex], reblog, payload }) updateStatus({ item: items[tootIndex], isReblog, payload })
} }
} }
@ -124,8 +97,7 @@ const updateStatusProperty = ({
} }
return old return old
} })
)
} }
export default updateStatusProperty export default updateStatusProperty