1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +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

@ -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