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

View File

@ -4,90 +4,52 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual } from 'lodash'
import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import TimelineActions from './Shared/Actions'
import TimelineContent from './Shared/Content'
import StatusContext from './Shared/Context'
import TimelineHeaderConversation from './Shared/HeaderConversation'
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 {
conversation: Mastodon.Conversation
queryKey: QueryKeyTimeline
highlighted?: boolean
}
const TimelineConversation = React.memo(
({ conversation, queryKey, highlighted = false }: Props) => {
const instanceAccount = useSelector(
getInstanceAccount,
(prev, next) => prev?.id === next?.id
)
const { colors } = useTheme()
const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlighted = false }) => {
const { colors } = useTheme()
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return apiInstance<Mastodon.Conversation>({
method: 'post',
url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
const queryClient = useQueryClient()
const fireMutation = useCallback(() => {
return apiInstance<Mastodon.Conversation>({
method: 'post',
url: `conversations/${conversation.id}/read`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
})
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
if (conversation.last_status) {
conversation.unread && mutate()
navigation.push('Tab-Shared-Toot', {
toot: conversation.last_status,
rootQueryKey: queryKey
})
}
}, [])
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
if (conversation.last_status) {
conversation.unread && mutate()
navigation.push('Tab-Shared-Toot', {
toot: conversation.last_status,
rootQueryKey: queryKey
})
}
}, [])
return (
return (
<StatusContext.Provider value={{ queryKey, status: conversation.last_status }}>
<Pressable
style={[
{
@ -100,19 +62,39 @@ const TimelineConversation = React.memo(
conversation.unread && {
borderLeftWidth: StyleConstants.Spacing.XS,
borderLeftColor: colors.blue,
paddingLeft:
StyleConstants.Spacing.Global.PagePadding -
StyleConstants.Spacing.XS
paddingLeft: StyleConstants.Spacing.Global.PagePadding - StyleConstants.Spacing.XS
}
]}
onPress={onPress}
>
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<Avatars accounts={conversation.accounts} />
<TimelineHeaderConversation
queryKey={queryKey}
conversation={conversation}
/>
<View
style={{
borderRadius: 4,
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>
{conversation.last_status ? (
@ -120,40 +102,19 @@ const TimelineConversation = React.memo(
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: StyleConstants.Avatar.M + StyleConstants.Spacing.S
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineContent
status={conversation.last_status}
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}
<TimelineContent />
<TimelinePoll />
</View>
<TimelineActions
queryKey={queryKey}
status={conversation.last_status}
highlighted={highlighted}
accts={conversation.accounts.map(account => account.acct)}
reblog={false}
/>
<TimelineActions />
</>
) : null}
</Pressable>
)
},
(prev, next) => isEqual(prev.conversation, next.conversation)
)
</StatusContext.Provider>
)
}
export default TimelineConversation

View File

@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
import React, { useRef } from 'react'
import React, { useRef, useState } from 'react'
import { Pressable, StyleProp, View, ViewStyle } from 'react-native'
import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFeedback from './Shared/Feedback'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
@ -46,31 +46,28 @@ const TimelineDefault: React.FC<Props> = ({
disableOnPress = false
}) => {
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualStatus = item.reblog ? item.reblog : item
const ownAccount = actualStatus.account?.id === instanceAccount?.id
const instanceAccount = useSelector(getInstanceAccount, () => true)
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 }>({
content: '',
complete: false
})
const filtered = queryKey && shouldFilter({ copiableContent, status: actualStatus, queryKey })
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
if (queryKey && filtered && !highlighted) {
return <TimelineFiltered phrase={filtered} />
}
const onPress = () => {
if (highlighted) return
navigation.push('Tab-Shared-Toot', {
toot: actualStatus,
rootQueryKey: queryKey
})
}
const mainStyle: StyleProp<ViewStyle> = {
padding: StyleConstants.Spacing.Global.PagePadding,
backgroundColor: colors.backgroundDefault,
@ -79,24 +76,14 @@ const TimelineDefault: React.FC<Props> = ({
const main = () => (
<>
{item.reblog ? (
<TimelineActioned action='reblog' account={item.account} />
<TimelineActioned action='reblog' />
) : item._pinned ? (
<TimelineActioned action='pinned' account={item.account} />
<TimelineActioned action='pinned' />
) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar
queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account}
highlighted={highlighted}
/>
<TimelineHeaderDefault
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={rootQueryKey}
status={actualStatus}
highlighted={highlighted}
copiableContent={copiableContent}
/>
<TimelineAvatar />
<TimelineHeaderDefault />
</View>
<View
@ -105,120 +92,103 @@ const TimelineDefault: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{typeof actualStatus.content === 'string' && actualStatus.content.length > 0 ? (
<TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
) : null}
{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} />
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelinePoll />
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
<TimelineTranslate />
<TimelineFeedback />
</View>
{queryKey && !disableDetails ? (
<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}
<TimelineActions />
</>
)
const mShare = menuShare({
visibility: actualStatus.visibility,
visibility: status.visibility,
type: 'status',
url: actualStatus.url || actualStatus.uri,
url: status.url || status.uri,
copiableContent
})
const mStatus = menuStatus({ status: actualStatus, queryKey, rootQueryKey })
const mInstance = menuInstance({ status: actualStatus, queryKey, rootQueryKey })
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return disableOnPress ? (
<View style={mainStyle}>{main()}</View>
) : (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable
accessible={highlighted ? false : true}
style={mainStyle}
onPress={onPress}
onLongPress={() => {}}
children={main()}
/>
</ContextMenu.Trigger>
return (
<StatusContext.Provider
value={{
queryKey,
rootQueryKey,
status,
isReblog: !!item.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted,
disableDetails,
disableOnPress
}}
>
{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>
{mShare.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
<ContextMenu.Content>
{mShare.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
</ContextMenu.Group>
))}
{mStatus.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
{mStatus.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
</ContextMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
{mInstance.map((mGroup, index) => (
<ContextMenu.Group key={index}>
{mGroup.map(menu => (
<ContextMenu.Item key={menu.key} {...menu.item}>
<ContextMenu.ItemTitle children={menu.title} />
<ContextMenu.ItemIcon iosIconName={menu.icon} />
</ContextMenu.Item>
))}
</ContextMenu.Group>
))}
</ContextMenu.Group>
))}
</ContextMenu.Content>
</ContextMenu.Root>
<TimelineHeaderAndroid
queryKey={disableOnPress ? undefined : queryKey}
rootQueryKey={rootQueryKey}
status={actualStatus}
/>
</>
</ContextMenu.Content>
</ContextMenu.Root>
<TimelineHeaderAndroid />
</>
)}
</StatusContext.Provider>
)
}

View File

@ -16,11 +16,11 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
import React, { useCallback, useRef } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
@ -36,6 +36,17 @@ const TimelineNotifications: React.FC<Props> = ({
queryKey,
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 }>({
content: '',
complete: false
@ -53,11 +64,8 @@ const TimelineNotifications: React.FC<Props> = ({
}
const { colors } = useTheme()
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.id === next?.id)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const actualAccount = notification.status ? notification.status.account : notification.account
const onPress = useCallback(() => {
notification.status &&
navigation.push('Tab-Shared-Toot', {
@ -70,11 +78,7 @@ const TimelineNotifications: React.FC<Props> = ({
return (
<>
{notification.type !== 'mention' ? (
<TimelineActioned
action={notification.type}
account={notification.account}
notification
/>
<TimelineActioned action={notification.type} isNotification account={account} />
) : null}
<View
@ -89,8 +93,8 @@ const TimelineNotifications: React.FC<Props> = ({
}}
>
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<TimelineAvatar queryKey={queryKey} account={actualAccount} highlighted={highlighted} />
<TimelineHeaderNotification queryKey={queryKey} notification={notification} />
<TimelineAvatar account={account} />
<TimelineHeaderNotification notification={notification} />
</View>
{notification.status ? (
@ -100,41 +104,16 @@ const TimelineNotifications: React.FC<Props> = ({
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{notification.status.content.length > 0 ? (
<TimelineContent status={notification.status} highlighted={highlighted} />
) : null}
{notification.status.poll ? (
<TimelinePoll
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} />
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelinePoll />
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
</View>
) : null}
</View>
{notification.status ? (
<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}
<TimelineActions />
</>
)
}
@ -149,7 +128,17 @@ const TimelineNotifications: React.FC<Props> = ({
const mInstance = menuInstance({ status: notification.status, queryKey })
return (
<>
<StatusContext.Provider
value={{
queryKey,
status,
isReblog: !!status?.reblog,
ownAccount,
spoilerHidden,
copiableContent,
highlighted
}}
>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Pressable
@ -199,8 +188,8 @@ const TimelineNotifications: React.FC<Props> = ({
))}
</ContextMenu.Content>
</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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props {
account: Mastodon.Account
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
notification?: boolean
action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
isNotification?: boolean
account?: Mastodon.Account
}
const TimelineActioned = React.memo(
({ account, action, notification = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const name = account?.display_name || account?.username
const iconColor = colors.primaryDefault
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
const { status } = useContext(StatusContext)
const account = isNotification ? rest.account : status?.account
if (!status || !account) return null
const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
)
const { t } = useTranslation('componentTimeline')
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 = () => {
switch (action) {
case 'pinned':
return (
<>
<Icon
name='Anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.pinned'))}
</>
)
case 'favourite':
return (
<>
<Icon
name='Heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.favourite', { name }))}
</Pressable>
</>
)
case 'follow':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow', { name }))}
</Pressable>
</>
)
case 'follow_request':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow_request', { name }))}
</Pressable>
</>
)
case 'poll':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.poll'))}
</>
)
case 'reblog':
return (
<>
<Icon
name='Repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(
notification
? t('shared.actioned.reblog.notification', { name })
: t('shared.actioned.reblog.default', { name })
)}
</Pressable>
</>
)
case 'status':
return (
<>
<Icon
name='Activity'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.status', { name }))}
</Pressable>
</>
)
case 'update':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.update'))}
</>
)
default:
return <></>
}
const onPress = () => navigation.push('Tab-Shared-Account', { account })
const children = () => {
switch (action) {
case 'pinned':
return (
<>
<Icon
name='Anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.pinned'))}
</>
)
case 'favourite':
return (
<>
<Icon
name='Heart'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.favourite', { name }))}
</Pressable>
</>
)
case 'follow':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow', { name }))}
</Pressable>
</>
)
case 'follow_request':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow_request', { name }))}
</Pressable>
</>
)
case 'poll':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.poll'))}
</>
)
case 'reblog':
return (
<>
<Icon
name='Repeat'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(
isNotification
? t('shared.actioned.reblog.notification', { name })
: t('shared.actioned.reblog.default', { name })
)}
</Pressable>
</>
)
case 'status':
return (
<>
<Icon
name='Activity'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.status', { name }))}
</Pressable>
</>
)
case 'update':
return (
<>
<Icon
name='BarChart2'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.update'))}
</>
)
default:
return <></>
}
}
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}}
>
{children()}
</View>
)
},
() => true
)
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M - StyleConstants.Font.Size.S,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}}
children={children()}
/>
)
}
const styles = StyleSheet.create({
icon: {

View File

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

View File

@ -10,51 +10,140 @@ import { RootStackParamList } from '@utils/navigation/navigators'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useState } from 'react'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props {
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
}
const TimelineAttachment = () => {
const { status, disableDetails } = useContext(StatusContext)
if (
!status ||
disableDetails ||
!Array.isArray(status.media_attachments) ||
!status.media_attachments.length
)
return null
const TimelineAttachment = React.memo(
({ status }: Props) => {
const { t } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline')
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
const account = useSelector(
getInstanceAccount,
(prev, next) =>
prev.preferences['reading:expand:media'] === next.preferences['reading:expand:media']
)
const defaultSensitive = () => {
switch (account.preferences['reading:expand:media']) {
case 'show_all':
return false
case 'hide_all':
return true
default:
return status.sensitive
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
}
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
// @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] =
status.media_attachments
.map(attachment => {
// @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
.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) {
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
}
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
image={attachment}
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:
if (
attachment.preview_url?.endsWith('.jpg') ||
@ -66,176 +155,74 @@ const TimelineAttachment = React.memo(
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) {
case 'image':
return (
<AttachmentImage
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
// @ts-ignore
image={attachment}
navigateToImagesViewer={navigateToImagesViewer}
/>
)
case 'video':
} else {
return (
<AttachmentVideo
<AttachmentUnsupported
key={index}
total={status.media_attachments.length}
index={index}
sensitiveShown={sensitiveShown}
video={attachment}
attachment={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:
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>
}
}
})}
</View>
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button
type='text'
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
/>
</Pressable>
) : (
{defaultSensitive() &&
(sensitiveShown ? (
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button
type='icon'
content='EyeOff'
round
type='text'
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
setSensitiveShown(true)
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/>
))}
</View>
)
},
(prev, next) => {
let isEqual = true
if (prev.status.media_attachments.length !== next.status.media_attachments.length) {
isEqual = false
return isEqual
}
prev.status.media_attachments.forEach((attachment, index) => {
if (attachment.preview_url !== next.status.media_attachments[index].preview_url) {
isEqual = false
}
})
return isEqual
}
)
</Pressable>
) : (
<Button
type='icon'
content='EyeOff'
round
overlay
onPress={() => {
setSensitiveShown(true)
haptics('Light')
}}
style={{
position: 'absolute',
top: StyleConstants.Spacing.S * 2,
left: StyleConstants.Spacing.S
}}
/>
))}
</View>
)
}
export default TimelineAttachment

View File

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

View File

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

View File

@ -1,52 +1,36 @@
import { ParseHTML } from '@components/Parse'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import StatusContext from './Context'
export interface Props {
status: Pick<Mastodon.Status, 'content' | 'spoiler_text' | 'emojis'> & {
mentions?: Mastodon.Status['mentions']
tags?: Mastodon.Status['tags']
}
highlighted?: boolean
disableDetails?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
}
const TimelineContent = React.memo(
({ status, highlighted = false, disableDetails = false }: Props) => {
const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true)
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
const { status, highlighted, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
return (
<>
{status.spoiler_text ? (
<>
<ParseHTML
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
/>
<ParseHTML
content={status.content}
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}
/>
</>
) : (
const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true)
return (
<>
{status.spoiler_text?.length ? (
<>
<ParseHTML
content={status.spoiler_text}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
/>
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
@ -54,16 +38,27 @@ const TimelineContent = React.memo(
emojis={status.emojis}
mentions={status.mentions}
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}
/>
)}
</>
)
},
(prev, next) =>
prev.status.content === next.status.content &&
prev.status.spoiler_text === next.status.spoiler_text
)
</>
) : (
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}
adaptiveSize
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={highlighted ? 999 : undefined}
disableDetails={disableDetails}
/>
)}
</>
)
}
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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import StatusContext from './Context'
export interface Props {
status: Pick<Mastodon.Status, 'id' | 'edited_at' | 'reblogs_count' | 'favourites_count'>
highlighted: boolean
}
const TimelineFeedback = () => {
const { status, highlighted } = useContext(StatusContext)
if (!status || !highlighted) return null
const TimelineFeedback = React.memo(
({ status, highlighted }: Props) => {
if (!highlighted) {
return null
}
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { data } = useStatusHistory({
id: status.id,
options: { enabled: status.edited_at !== undefined }
})
const { data } = useStatusHistory({
id: status.id,
options: { enabled: status.edited_at !== undefined }
})
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row' }}>
{status.reblogs_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row' }}>
{status.reblogs_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.reblogged_by.accessibilityLabel', {
count: status.reblogs_count
})}
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'reblogged_by',
count: status.reblogs_count
})}
accessibilityHint={t('shared.actionsUsers.reblogged_by.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { color: colors.blue }]}
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
type: 'reblogged_by',
count: status.reblogs_count
})
}
>
{t('shared.actionsUsers.reblogged_by.text', {
count: status.reblogs_count
})}
</CustomText>
) : null}
{status.favourites_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
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', {
})
}
>
{t('shared.actionsUsers.reblogged_by.text', {
count: status.reblogs_count
})}
</CustomText>
) : null}
{status.favourites_count > 0 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.favourited_by.accessibilityLabel', {
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
})}
</CustomText>
) : null}
</View>
<View>
{data && data.length > 1 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
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>
})
}
>
{t('shared.actionsUsers.favourited_by.text', {
count: status.favourites_count
})}
</CustomText>
) : null}
</View>
)
},
(prev, next) =>
prev.status.edited_at === next.status.edited_at &&
prev.status.reblogs_count === next.status.reblogs_count &&
prev.status.favourites_count === next.status.favourites_count
)
<View>
{data && data.length > 1 ? (
<CustomText
accessibilityLabel={t('shared.actionsUsers.history.accessibilityLabel', {
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>
)
}
const styles = StyleSheet.create({
text: {

View File

@ -1,39 +1,32 @@
import CustomText from '@components/Text'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import StatusContext from './Context'
export interface Props {
queryKey?: QueryKeyTimeline
status: Mastodon.Status
const TimelineFullConversation = () => {
const { queryKey, status, disableDetails } = useContext(StatusContext)
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

View File

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

View File

@ -2,46 +2,25 @@ import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import { ParseEmojis } from '@components/Parse'
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 { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import StatusContext from './Context'
import HeaderSharedCreated from './HeaderShared/Created'
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 {
queryKey: QueryKeyTimeline
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 { t } = useTranslation('componentTimeline')
@ -70,7 +49,22 @@ const HeaderConversation = ({ queryKey, conversation }: Props) => {
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<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
style={{
flexDirection: 'row',

View File

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

View File

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

View File

@ -7,39 +7,28 @@ import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusProperty'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
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 { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import StatusContext from './Context'
export interface Props {
queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
statusId: Mastodon.Status['id']
poll: NonNullable<Mastodon.Status['poll']>
reblog: boolean
sameAccount: boolean
}
const TimelinePoll: React.FC = () => {
const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } =
useContext(StatusContext)
if (!queryKey || !status || !status.poll) return null
const poll = status.poll
const TimelinePoll: React.FC<Props> = ({
queryKey,
rootQueryKey,
statusId,
poll,
reblog,
sameAccount
}) => {
const { colors, theme } = useTheme()
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 mutation = useTimelineMutation({
@ -79,7 +68,7 @@ const TimelinePoll: React.FC<Props> = ({
const pollButton = useMemo(() => {
if (!poll.expired) {
if (!sameAccount && !poll.voted) {
if (!ownAccount && !poll.voted) {
return (
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button
@ -88,8 +77,8 @@ const TimelinePoll: React.FC<Props> = ({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: statusId,
reblog,
id: status.id,
isReblog,
payload: {
property: 'poll',
id: poll.id,
@ -114,8 +103,8 @@ const TimelinePoll: React.FC<Props> = ({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: statusId,
reblog,
id: status.id,
isReblog,
payload: {
property: 'poll',
id: poll.id,
@ -258,6 +247,8 @@ const TimelinePoll: React.FC<Props> = ({
}
}
if (spoilerHidden || disableDetails) return null
return (
<View style={{ marginTop: StyleConstants.Spacing.M }}>
{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 { useTheme } from '@utils/styles/ThemeManager'
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 { Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import detectLanguage from 'react-native-language-detection'
import StatusContext from './Context'
export interface Props {
highlighted: boolean
status: Pick<Mastodon.Status, 'language' | 'spoiler_text' | 'content' | 'emojis'>
}
const TimelineTranslate = () => {
const { status, highlighted } = useContext(StatusContext)
if (!status || !highlighted) return null
const TimelineTranslate = React.memo(
({ highlighted, status }: Props) => {
if (!highlighted) {
return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
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 { colors } = useTheme()
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 [detectedLanguage, setDetectedLanguage] = useState<string>('')
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
}
detect()
}, [])
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
}
detect()
}, [])
const settingsLanguage = getLanguage()
const targetLanguage = settingsLanguage?.startsWith('en')
? Localization.locale || settingsLanguage || 'en'
: settingsLanguage || Localization.locale || 'en'
const settingsLanguage = getLanguage()
const targetLanguage = settingsLanguage?.startsWith('en')
? Localization.locale || settingsLanguage || 'en'
: settingsLanguage || Localization.locale || 'en'
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage,
target: targetLanguage,
text,
options: { enabled }
})
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage,
target: targetLanguage,
text,
options: { enabled }
})
if (!detectedLanguage) {
return null
}
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (!detectedLanguage) {
return null
}
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
return null
}
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
}
return (
<>
<Pressable
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingBottom: isSuccess ? 0 : undefined
}}
onPress={() => {
if (enabled) {
if (!isSuccess) {
refetch()
}
} else {
setEnabled(true)
return (
<>
<Pressable
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingBottom: isSuccess ? 0 : undefined
}}
onPress={() => {
if (enabled) {
if (!isSuccess) {
refetch()
}
} else {
setEnabled(true)
}
}}
>
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}}
>
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}}
>
{isError
? t('shared.translate.failed')
: isSuccess
? typeof data?.error === 'string'
? t(`shared.translate.${data.error}`)
: t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
</CustomText>
<CustomText>
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
</CustomText>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : 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
)
{isError
? t('shared.translate.failed')
: isSuccess
? typeof data?.error === 'string'
? t(`shared.translate.${data.error}`)
: t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
</CustomText>
<CustomText>
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
</CustomText>
{isLoading ? (
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
style={{ marginLeft: StyleConstants.Spacing.S }}
/>
) : null}
</Pressable>
{data && data.error === undefined
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
: null}
</>
)
}
export default TimelineTranslate

View File

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

View File

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

View File

@ -1,9 +1,6 @@
import queryClient from '@helpers/queryClient'
import { InfiniteData } from 'react-query'
import {
MutationVarsTimelineUpdateStatusProperty,
TimelineData
} from '../timeline'
import { MutationVarsTimelineUpdateStatusProperty, TimelineData } from '../timeline'
import updateConversation from './update/conversation'
import updateNotification from './update/notification'
import updateStatus from './update/status'
@ -12,12 +9,54 @@ const updateStatusProperty = ({
queryKey,
rootQueryKey,
id,
reblog,
isReblog,
payload
}: MutationVarsTimelineUpdateStatusProperty) => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
old => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, 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 = 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) {
let foundToot = false
old.pages = old.pages.map(page => {
@ -25,39 +64,30 @@ const updateStatusProperty = ({
if (foundToot) {
return page
} else {
if (
typeof (page.body as Mastodon.Conversation[])[0].unread ===
'boolean'
) {
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
)
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'
) {
} 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
)
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
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], reblog, payload })
updateStatus({ item: items[tootIndex], isReblog, payload })
}
}
@ -67,65 +97,7 @@ const updateStatusProperty = ({
}
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