1
0
mirror of https://github.com/tooot-app/app synced 2025-04-15 02:42:04 +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,90 +4,52 @@ 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 { colors } = useTheme()
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const { colors } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback(() => { const fireMutation = useCallback(() => {
return apiInstance<Mastodon.Conversation>({ return apiInstance<Mastodon.Conversation>({
method: 'post', method: 'post',
url: `conversations/${conversation.id}/read` url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
}) })
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
})
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() navigation.push('Tab-Shared-Toot', {
navigation.push('Tab-Shared-Toot', { toot: conversation.last_status,
toot: conversation.last_status, rootQueryKey: queryKey
rootQueryKey: queryKey })
}) }
} }, [])
}, [])
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,120 +92,103 @@ 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 (
<View style={mainStyle}>{main()}</View> <StatusContext.Provider
) : ( value={{
<> queryKey,
<ContextMenu.Root> rootQueryKey,
<ContextMenu.Trigger> status,
<Pressable isReblog: !!item.reblog,
accessible={highlighted ? false : true} ownAccount,
style={mainStyle} spoilerHidden,
onPress={onPress} copiableContent,
onLongPress={() => {}} highlighted,
children={main()} disableDetails,
/> disableOnPress
</ContextMenu.Trigger> }}
>
{disableOnPress ? (
<View style={mainStyle}>{main()}</View>
) : (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable
accessible={highlighted ? false : true}
style={mainStyle}
disabled={highlighted}
onPress={() =>
navigation.push('Tab-Shared-Toot', {
toot: status,
rootQueryKey: queryKey
})
}
onLongPress={() => {}}
children={main()}
/>
</ContextMenu.Trigger>
<ContextMenu.Content> <ContextMenu.Content>
{mShare.map((mGroup, index) => ( {mShare.map((mGroup, index) => (
<ContextMenu.Group key={index}> <ContextMenu.Group key={index}>
{mGroup.map(menu => ( {mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}> <ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} /> <ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} /> <ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item> </ContextMenu.Item>
))}
</ContextMenu.Group>
))} ))}
</ContextMenu.Group>
))}
{mStatus.map((mGroup, index) => ( {mStatus.map((mGroup, index) => (
<ContextMenu.Group key={index}> <ContextMenu.Group key={index}>
{mGroup.map(menu => ( {mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}> <ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} /> <ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} /> <ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item> </ContextMenu.Item>
))}
</ContextMenu.Group>
))} ))}
</ContextMenu.Group>
))}
{mInstance.map((mGroup, index) => ( {mInstance.map((mGroup, index) => (
<ContextMenu.Group key={index}> <ContextMenu.Group key={index}>
{mGroup.map(menu => ( {mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}> <ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} /> <ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} /> <ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item> </ContextMenu.Item>
))}
</ContextMenu.Group>
))} ))}
</ContextMenu.Group> </ContextMenu.Content>
))} </ContextMenu.Root>
</ContextMenu.Content> <TimelineHeaderAndroid />
</ContextMenu.Root> </>
<TimelineHeaderAndroid )}
queryKey={disableOnPress ? undefined : queryKey} </StatusContext.Provider>
rootQueryKey={rootQueryKey}
status={actualStatus}
/>
</>
) )
} }

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,163 +5,164 @@ 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 { t } = useTranslation('componentTimeline') const account = isNotification ? rest.account : status?.account
const { colors } = useTheme() if (!status || !account) return null
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const name = account?.display_name || account?.username
const iconColor = colors.primaryDefault
const content = (content: string) => ( const { t } = useTranslation('componentTimeline')
<ParseEmojis content={content} emojis={account.emojis} size='S' /> const { colors } = useTheme()
) const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const name = account?.display_name || account?.username
const iconColor = colors.primaryDefault
const onPress = () => navigation.push('Tab-Shared-Account', { account }) const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
)
const children = () => { const onPress = () => navigation.push('Tab-Shared-Account', { account })
switch (action) {
case 'pinned': const children = () => {
return ( switch (action) {
<> case 'pinned':
<Icon return (
name='Anchor' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='Anchor'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
{content(t('shared.actioned.pinned'))} style={styles.icon}
</> />
) {content(t('shared.actioned.pinned'))}
case 'favourite': </>
return ( )
<> case 'favourite':
<Icon return (
name='Heart' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='Heart'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
<Pressable onPress={onPress}> style={styles.icon}
{content(t('shared.actioned.favourite', { name }))} />
</Pressable> <Pressable onPress={onPress}>
</> {content(t('shared.actioned.favourite', { name }))}
) </Pressable>
case 'follow': </>
return ( )
<> case 'follow':
<Icon return (
name='UserPlus' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='UserPlus'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
<Pressable onPress={onPress}> style={styles.icon}
{content(t('shared.actioned.follow', { name }))} />
</Pressable> <Pressable onPress={onPress}>
</> {content(t('shared.actioned.follow', { name }))}
) </Pressable>
case 'follow_request': </>
return ( )
<> case 'follow_request':
<Icon return (
name='UserPlus' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='UserPlus'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
<Pressable onPress={onPress}> style={styles.icon}
{content(t('shared.actioned.follow_request', { name }))} />
</Pressable> <Pressable onPress={onPress}>
</> {content(t('shared.actioned.follow_request', { name }))}
) </Pressable>
case 'poll': </>
return ( )
<> case 'poll':
<Icon return (
name='BarChart2' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='BarChart2'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
{content(t('shared.actioned.poll'))} style={styles.icon}
</> />
) {content(t('shared.actioned.poll'))}
case 'reblog': </>
return ( )
<> case 'reblog':
<Icon return (
name='Repeat' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='Repeat'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
<Pressable onPress={onPress}> style={styles.icon}
{content( />
notification <Pressable onPress={onPress}>
? t('shared.actioned.reblog.notification', { name }) {content(
: t('shared.actioned.reblog.default', { name }) isNotification
)} ? t('shared.actioned.reblog.notification', { name })
</Pressable> : t('shared.actioned.reblog.default', { name })
</> )}
) </Pressable>
case 'status': </>
return ( )
<> case 'status':
<Icon return (
name='Activity' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='Activity'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
<Pressable onPress={onPress}> style={styles.icon}
{content(t('shared.actioned.status', { name }))} />
</Pressable> <Pressable onPress={onPress}>
</> {content(t('shared.actioned.status', { name }))}
) </Pressable>
case 'update': </>
return ( )
<> case 'update':
<Icon return (
name='BarChart2' <>
size={StyleConstants.Font.Size.S} <Icon
color={iconColor} name='BarChart2'
style={styles.icon} size={StyleConstants.Font.Size.S}
/> color={iconColor}
{content(t('shared.actioned.update'))} style={styles.icon}
</> />
) {content(t('shared.actioned.update'))}
default: </>
return <></> )
} default:
return <></>
} }
}
return ( return (
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: StyleConstants.Spacing.S, marginBottom: StyleConstants.Spacing.S,
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(() => {
navigation.navigate('Screen-Compose', { const accts = uniqBy(
type: 'reply', ([status.account] as Mastodon.Account[] & Mastodon.Mention[])
incomingStatus: status, .concat(status.mentions)
accts, .filter(d => d?.id !== instanceAccount?.id),
queryKey d => d?.id
}), ).map(d => d?.acct)
[status.replies_count] navigation.navigate('Screen-Compose', {
) type: 'reply',
incomingStatus: status,
accts,
queryKey
})
}, [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,51 +10,140 @@ 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( const { t } = useTranslation('componentTimeline')
({ status }: Props) => {
const { t } = useTranslation('componentTimeline')
const account = useSelector( const account = useSelector(
getInstanceAccount, getInstanceAccount,
(prev, next) => (prev, next) =>
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media'] prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
) )
const defaultSensitive = () => { const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) { switch (account.preferences['reading:expand:media']) {
case 'show_all': case 'show_all':
return false return false
case 'hide_all': case 'hide_all':
return true return true
default: default:
return status.sensitive return status.sensitive
}
} }
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) {
case 'image':
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
}
}
})
.filter(i => i)
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigateToImagesViewer = (id: string) => {
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
}
return (
<View>
<View
style={{
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'stretch'
}}
>
{status.media_attachments.map((attachment, index) => {
switch (attachment.type) { switch (attachment.type) {
case 'image': case 'image':
return { return (
id: attachment.id, <AttachmentImage
preview_url: attachment.preview_url, key={index}
url: attachment.url, total={status.media_attachments.length}
remote_url: attachment.remote_url, index={index}
blurhash: attachment.blurhash, sensitiveShown={sensitiveShown}
width: attachment.meta?.original?.width, image={attachment}
height: attachment.meta?.original?.height navigateToImagesViewer={navigateToImagesViewer}
} />
)
case 'video':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
/>
)
case 'gifv':
return (
<AttachmentVideo
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default: default:
if ( if (
attachment.preview_url?.endsWith('.jpg') || attachment.preview_url?.endsWith('.jpg') ||
@ -66,176 +155,74 @@ const TimelineAttachment = React.memo(
attachment.remote_url?.endsWith('.png') || attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif') attachment.remote_url?.endsWith('.gif')
) { ) {
return {
id: attachment.id,
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
}
}
})
.filter(i => i)
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>()
const navigateToImagesViewer = (id: string) => {
navigation.navigate('Screen-ImagesViewer', { imageUrls, id })
}
return (
<View>
<View
style={{
marginTop: StyleConstants.Spacing.S,
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignContent: 'stretch'
}}
>
{status.media_attachments.map((attachment, index) => {
switch (attachment.type) {
case 'image':
return ( return (
<AttachmentImage <AttachmentImage
key={index} key={index}
total={status.media_attachments.length} total={status.media_attachments.length}
index={index} index={index}
sensitiveShown={sensitiveShown} sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment} image={attachment}
navigateToImagesViewer={navigateToImagesViewer} navigateToImagesViewer={navigateToImagesViewer}
/> />
) )
case 'video': } else {
return ( return (
<AttachmentVideo <AttachmentUnsupported
key={index} key={index}
total={status.media_attachments.length} total={status.media_attachments.length}
index={index} index={index}
sensitiveShown={sensitiveShown} sensitiveShown={sensitiveShown}
video={attachment} attachment={attachment}
/> />
) )
case 'gifv': }
return ( }
<AttachmentVideo })}
key={index} </View>
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
gifv
/>
)
case 'audio':
return (
<AttachmentAudio
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
audio={attachment}
/>
)
default:
if (
attachment.preview_url?.endsWith('.jpg') ||
attachment.preview_url?.endsWith('.jpeg') ||
attachment.preview_url?.endsWith('.png') ||
attachment.preview_url?.endsWith('.gif') ||
attachment.remote_url?.endsWith('.jpg') ||
attachment.remote_url?.endsWith('.jpeg') ||
attachment.remote_url?.endsWith('.png') ||
attachment.remote_url?.endsWith('.gif')
) {
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
} else {
return (
<AttachmentUnsupported
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
attachment={attachment}
/>
)
}
}
})}
</View>
{defaultSensitive() && {defaultSensitive() &&
(sensitiveShown ? ( (sensitiveShown ? (
<Pressable <Pressable
style={{ style={{
position: 'absolute', position: 'absolute',
width: '100%', width: '100%',
height: '100%', height: '100%',
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}} }}
> >
<Button
type='text'
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
/>
</Pressable>
) : (
<Button <Button
type='icon' type='text'
content='EyeOff' content={t('shared.attachment.sensitive.button')}
round
overlay overlay
onPress={() => { onPress={() => {
setSensitiveShown(true) layoutAnimation()
setSensitiveShown(false)
haptics('Light') haptics('Light')
}} }}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/> />
))} </Pressable>
</View> ) : (
) <Button
}, type='icon'
(prev, next) => { content='EyeOff'
let isEqual = true round
overlay
if (prev.status.media_attachments.length !== next.status.media_attachments.length) { onPress={() => {
isEqual = false setSensitiveShown(true)
return isEqual haptics('Light')
} }}
style={{
prev.status.media_attachments.forEach((attachment, index) => { position: 'absolute',
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) { top: StyleConstants.Spacing.S * 2,
isEqual = false left: StyleConstants.Spacing.S
} }}
}) />
))}
return isEqual </View>
} )
) }
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,52 +1,36 @@
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)
const { t } = useTranslation('componentTimeline') if (!status || typeof status.content !== 'string' || !status.content.length) return null
const instanceAccount = useSelector(getInstanceAccount, () => true)
return ( const { t } = useTranslation('componentTimeline')
<> const instanceAccount = useSelector(getInstanceAccount, () => true)
{status.spoiler_text ? (
<> return (
<ParseHTML <>
content={status.spoiler_text} {status.spoiler_text?.length ? (
size={highlighted ? 'L' : 'M'} <>
adaptiveSize <ParseHTML
emojis={status.emojis} content={status.spoiler_text}
mentions={status.mentions} size={highlighted ? 'L' : 'M'}
tags={status.tags} adaptiveSize
numberOfLines={999} emojis={status.emojis}
highlighted={highlighted} mentions={status.mentions}
disableDetails={disableDetails} tags={status.tags}
/> numberOfLines={999}
<ParseHTML highlighted={highlighted}
content={status.content} disableDetails={disableDetails}
size={highlighted ? 'L' : 'M'} />
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
expandHint={t('shared.content.expandHint')}
highlighted={highlighted}
disableDetails={disableDetails}
/>
</>
) : (
<ParseHTML <ParseHTML
content={status.content} content={status.content}
size={highlighted ? 'L' : 'M'} size={highlighted ? 'L' : 'M'}
@ -54,16 +38,27 @@ const TimelineContent = React.memo(
emojis={status.emojis} emojis={status.emojis}
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={highlighted ? 999 : undefined} numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
expandHint={t('shared.content.expandHint')}
setSpoilerExpanded={setSpoilerExpanded}
highlighted={highlighted}
disableDetails={disableDetails} disableDetails={disableDetails}
/> />
)} </>
</> ) : (
) <ParseHTML
}, content={status.content}
(prev, next) => size={highlighted ? 'L' : 'M'}
prev.status.content === next.status.content && adaptiveSize
prev.status.spoiler_text === next.status.spoiler_text emojis={status.emojis}
) mentions={status.mentions}
tags={status.tags}
numberOfLines={highlighted ? 999 : undefined}
disableDetails={disableDetails}
/>
)}
</>
)
}
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,103 +5,92 @@ 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( const { t } = useTranslation('componentTimeline')
({ status, highlighted }: Props) => { const { colors } = useTheme()
if (!highlighted) { const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
return null
}
const { t } = useTranslation('componentTimeline') const { data } = useStatusHistory({
const { colors } = useTheme() id: status.id,
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() options: { enabled: status.edited_at !== undefined }
})
const { data } = useStatusHistory({ return (
id: status.id, <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
options: { enabled: status.edited_at !== undefined } <View style={{ flexDirection: 'row' }}>
}) {status.reblogs_count > 0 ? (
<CustomText
return ( accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> count: status.reblogs_count
<View style={{ flexDirection: 'row' }}> })}
{status.reblogs_count > 0 ? ( accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
<CustomText accessibilityRole='button'
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', { style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'reblogged_by',
count: status.reblogs_count count: status.reblogs_count
})} })
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')} }
accessibilityRole='button' >
style={[styles.text, { color: colors.blue }]} {t('shared.actionsUsers.reblogged_by.text', {
onPress={() => count: status.reblogs_count
navigation.push('Tab-Shared-Users', { })}
reference: 'statuses', </CustomText>
id: status.id, ) : null}
type: 'reblogged_by', {status.favourites_count > 0 ? (
count: status.reblogs_count <CustomText
}) accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
} count: status.reblogs_count
> })}
{t('shared.actionsUsers.reblogged_by.text', { accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
count: status.reblogs_count accessibilityRole='button'
})} style={[styles.text, { color: colors.blue }]}
</CustomText> onPress={() =>
) : null} navigation.push('Tab-Shared-Users', {
{status.favourites_count > 0 ? ( reference: 'statuses',
<CustomText id: status.id,
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', { type: 'favourited_by',
count: status.reblogs_count
})}
accessibilityHint={t('shared.actionsUsers.favourited_by.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'favourited_by',
count: status.favourites_count
})
}
>
{t('shared.actionsUsers.favourited_by.text', {
count: status.favourites_count count: status.favourites_count
})} })
</CustomText> }
) : null} >
</View> {t('shared.actionsUsers.favourited_by.text', {
<View> count: status.favourites_count
{data && data.length > 1 ? ( })}
<CustomText </CustomText>
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', { ) : null}
count: data.length - 1
})}
accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { marginRight: 0, color: colors.blue }]}
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
>
{t('shared.actionsUsers.history.text', {
count: data.length - 1
})}
</CustomText>
) : null}
</View>
</View> </View>
) <View>
}, {data && data.length > 1 ? (
(prev, next) => <CustomText
prev.status.edited_at === next.status.edited_at && accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
prev.status.reblogs_count === next.status.reblogs_count && count: data.length - 1
prev.status.favourites_count === next.status.favourites_count })}
) accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { marginRight: 0, color: colors.blue }]}
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
>
{t('shared.actionsUsers.history.text', {
count: data.length - 1
})}
</CustomText>
) : null}
</View>
</View>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
text: { text: {

View File

@ -1,39 +1,32 @@
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 { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return queryKey &&
queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id &&
(status.mentions.length === 0 ||
status.mentions.filter(mention => mention.id !== status.in_reply_to_account_id).length) ? (
<CustomText
fontStyle='S'
style={{
color: colors.blue,
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}
</CustomText>
) : null
} }
const TimelineFullConversation = React.memo(
({ queryKey, status }: Props) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
return queryKey &&
queryKey[1].page !== 'Toot' &&
status.in_reply_to_account_id &&
(status.mentions.length === 0 ||
status.mentions.filter(
mention => mention.id !== status.in_reply_to_account_id
).length) ? (
<CustomText
fontStyle='S'
style={{
color: colors.blue,
marginTop: StyleConstants.Spacing.S
}}
>
{t('shared.fullConversation')}
</CustomText>
) : 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,131 +5,119 @@ 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( const { t } = useTranslation('componentTimeline')
({ highlighted, status }: Props) => { const { colors } = useTheme()
if (!highlighted) {
return null const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
} }
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
}
const { t } = useTranslation('componentTimeline') const [detectedLanguage, setDetectedLanguage] = useState<string>('')
const { colors } = useTheme() useEffect(() => {
const detect = async () => {
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content] const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
for (const i in text) { })
for (const emoji of status.emojis) { result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
}
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
} }
detect()
}, [])
const [detectedLanguage, setDetectedLanguage] = useState<string>('') const settingsLanguage = getLanguage()
useEffect(() => { const targetLanguage = settingsLanguage?.startsWith('en')
const detect = async () => { ? Localization.locale || settingsLanguage || 'en'
const result = await detectLanguage(text.join(`\n\n`)).catch(() => { : settingsLanguage || Localization.locale || 'en'
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
}
detect()
}, [])
const settingsLanguage = getLanguage() const [enabled, setEnabled] = useState(false)
const targetLanguage = settingsLanguage?.startsWith('en') const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
? Localization.locale || settingsLanguage || 'en' source: detectedLanguage,
: settingsLanguage || Localization.locale || 'en' target: targetLanguage,
text,
options: { enabled }
})
const [enabled, setEnabled] = useState(false) if (!detectedLanguage) {
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ return null
source: detectedLanguage, }
target: targetLanguage, if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
text, return null
options: { enabled } }
}) if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (!detectedLanguage) { return (
return null <>
} <Pressable
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) { style={{
return null flexDirection: 'row',
} alignItems: 'center',
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) { paddingVertical: StyleConstants.Spacing.S,
return null paddingBottom: isSuccess ? 0 : undefined
} }}
onPress={() => {
return ( if (enabled) {
<> if (!isSuccess) {
<Pressable refetch()
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingBottom: isSuccess ? 0 : undefined
}}
onPress={() => {
if (enabled) {
if (!isSuccess) {
refetch()
}
} else {
setEnabled(true)
} }
} else {
setEnabled(true)
}
}}
>
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}} }}
> >
<CustomText {isError
fontStyle='M' ? t('shared.translate.failed')
style={{ : isSuccess
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue ? typeof data?.error === 'string'
}} ? t(`shared.translate.${data.error}`)
> : t('shared.translate.succeed', {
{isError provider: data?.provider,
? t('shared.translate.failed') source: data?.sourceLanguage
: isSuccess })
? typeof data?.error === 'string' : t('shared.translate.default')}
? t(`shared.translate.${data.error}`) </CustomText>
: t('shared.translate.succeed', { <CustomText>
provider: data?.provider, {__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
source: data?.sourceLanguage </CustomText>
}) {isLoading ? (
: t('shared.translate.default')} <Circle
</CustomText> size={StyleConstants.Font.Size.M}
<CustomText> color={colors.disabled}
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined} style={{ marginLeft: StyleConstants.Spacing.S }}
</CustomText> />
{isLoading ? ( ) : null}
<Circle </Pressable>
size={StyleConstants.Font.Size.M} {data && data.error === undefined
color={colors.disabled} ? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
style={{ marginLeft: StyleConstants.Spacing.S }} : null}
/> </>
) : null} )
</Pressable> }
{data && data.error === undefined
? data.text.map((d, i) => (
<ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />
))
: 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,54 @@ 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, if (old) {
old => { let foundToot = false
old.pages = old.pages.map(page => {
// Skip rest of the pages if any toot is found
if (foundToot) {
return page
} else {
if (typeof (page.body as Mastodon.Conversation[])[0].unread === 'boolean') {
const items = page.body as Mastodon.Conversation[]
const tootIndex = items.findIndex(({ last_status }) => last_status?.id === id)
if (tootIndex >= 0) {
foundToot = true
updateConversation({ item: items[tootIndex], payload })
}
return page
} else if (typeof (page.body as Mastodon.Notification[])[0].type === 'string') {
const items = page.body as Mastodon.Notification[]
const tootIndex = items.findIndex(({ status }) => status?.id === id)
if (tootIndex >= 0) {
foundToot = true
updateNotification({ item: items[tootIndex], payload })
}
} else {
const items = page.body as Mastodon.Status[]
const tootIndex = isReblog
? items.findIndex(({ reblog }) => reblog?.id === id)
: items.findIndex(toot => toot.id === id)
// if favourites page and notifications page, remove the item instead
if (tootIndex >= 0) {
foundToot = true
updateStatus({ item: items[tootIndex], isReblog, payload })
}
}
return page
}
})
}
return old
})
rootQueryKey &&
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(rootQueryKey, 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 +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 })
} }
} }
@ -67,65 +97,7 @@ const updateStatusProperty = ({
} }
return old return old
} })
)
rootQueryKey &&
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
rootQueryKey,
old => {
if (old) {
let foundToot = false
old.pages = old.pages.map(page => {
// Skip rest of the pages if any toot is found
if (foundToot) {
return page
} else {
if (
typeof (page.body as Mastodon.Conversation[])[0].unread ===
'boolean'
) {
const items = page.body as Mastodon.Conversation[]
const tootIndex = items.findIndex(
({ last_status }) => last_status?.id === id
)
if (tootIndex >= 0) {
foundToot = true
updateConversation({ item: items[tootIndex], payload })
}
return page
} else if (
typeof (page.body as Mastodon.Notification[])[0].type ===
'string'
) {
const items = page.body as Mastodon.Notification[]
const tootIndex = items.findIndex(
({ status }) => status?.id === id
)
if (tootIndex >= 0) {
foundToot = true
updateNotification({ item: items[tootIndex], payload })
}
} else {
const items = page.body as Mastodon.Status[]
const tootIndex = reblog
? items.findIndex(({ reblog }) => reblog?.id === id)
: items.findIndex(toot => toot.id === id)
// if favourites page and notifications page, remove the item instead
if (tootIndex >= 0) {
foundToot = true
updateStatus({ item: items[tootIndex], reblog, payload })
}
}
return page
}
})
}
return old
}
)
} }
export default updateStatusProperty export default updateStatusProperty