move translate stuff to a features directory
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
import {useCallback, useMemo} from 'react'
|
||||||
|
import {type GestureResponderEvent, View} from 'react-native'
|
||||||
|
import {AppBskyFeedPost} from '@atproto/api'
|
||||||
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
|
import {getPostLanguage, isPostInLanguage} from '#/locale/helpers'
|
||||||
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
||||||
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
|
import {Button} from '#/components/Button'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {useTranslateOnDevice} from '#/features/translation'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
|
export function InlineTranslateButton({
|
||||||
|
post,
|
||||||
|
}: {
|
||||||
|
post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const langPrefs = useLanguagePrefs()
|
||||||
|
|
||||||
|
const {translate, clearTranslation, translationState} = useTranslateOnDevice()
|
||||||
|
|
||||||
|
const needsTranslation = useMemo(
|
||||||
|
() =>
|
||||||
|
Boolean(
|
||||||
|
langPrefs.primaryLanguage &&
|
||||||
|
!isPostInLanguage(post, [langPrefs.primaryLanguage]),
|
||||||
|
),
|
||||||
|
[post, langPrefs.primaryLanguage],
|
||||||
|
)
|
||||||
|
|
||||||
|
const sourceLanguage = getPostLanguage(post)
|
||||||
|
|
||||||
|
const onTranslatePress = useCallback(
|
||||||
|
(e: GestureResponderEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
void translate(
|
||||||
|
post.record.text || '',
|
||||||
|
langPrefs.primaryLanguage,
|
||||||
|
sourceLanguage,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||||
|
post.record,
|
||||||
|
AppBskyFeedPost.isRecord,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ax.metric('translate', {
|
||||||
|
sourceLanguages: post.record.langs ?? [],
|
||||||
|
targetLanguage: langPrefs.primaryLanguage,
|
||||||
|
textLength: post.record.text.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[ax, sourceLanguage, translate, langPrefs, post],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onHideTranslation = useCallback(
|
||||||
|
(e: GestureResponderEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
clearTranslation()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[clearTranslation],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
needsTranslation && (
|
||||||
|
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
||||||
|
{translationState.status === 'loading' ? (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<Loader size="xs" />
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Translating…</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : translationState.status === 'success' ? (
|
||||||
|
<Button
|
||||||
|
label={l`Hide translation`}
|
||||||
|
onPress={onHideTranslation}
|
||||||
|
hoverStyle={native({opacity: 0.5})}
|
||||||
|
hitSlop={HITSLOP_30}>
|
||||||
|
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
|
||||||
|
<Trans>Hide translation</Trans>
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
label={l`Translate`}
|
||||||
|
onPress={onTranslatePress}
|
||||||
|
hoverStyle={native({opacity: 0.5})}
|
||||||
|
hitSlop={HITSLOP_30}>
|
||||||
|
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
|
||||||
|
<Trans>Translate</Trans>
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+5
-91
@@ -1,107 +1,21 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
import {Platform, View} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {codeToLanguageName, languageName} from '#/locale/helpers'
|
import {languageName} from '#/locale/helpers'
|
||||||
import {LANGUAGES} from '#/locale/languages'
|
import {LANGUAGES} from '#/locale/languages'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
import {atoms as a, native} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import * as Select from '#/components/Select'
|
import * as Select from '#/components/Select'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {useTranslateOnDevice} from '#/translation'
|
import {useTranslateOnDevice} from '#/features/translation'
|
||||||
|
|
||||||
export function TranslatedPost({
|
export function TranslationLanguageSelect({
|
||||||
postText,
|
|
||||||
hideLoading = false,
|
|
||||||
}: {
|
|
||||||
postText: string
|
|
||||||
hideLoading: boolean
|
|
||||||
}) {
|
|
||||||
const {translationState} = useTranslateOnDevice()
|
|
||||||
|
|
||||||
if (translationState.status === 'loading' && !hideLoading) {
|
|
||||||
return <TranslationLoading />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (translationState.status === 'success') {
|
|
||||||
return (
|
|
||||||
<TranslationResult
|
|
||||||
postText={postText}
|
|
||||||
sourceLanguage={translationState.sourceLanguage}
|
|
||||||
translatedText={translationState.translatedText}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function TranslationLoading() {
|
|
||||||
const t = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}>
|
|
||||||
<Loader size="sm" />
|
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Translating…</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TranslationResult({
|
|
||||||
postText,
|
|
||||||
sourceLanguage,
|
|
||||||
translatedText,
|
|
||||||
}: {
|
|
||||||
postText: string
|
|
||||||
sourceLanguage: string | null
|
|
||||||
translatedText: string
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const {i18n} = useLingui()
|
|
||||||
|
|
||||||
const langName = sourceLanguage
|
|
||||||
? codeToLanguageName(sourceLanguage, i18n.locale)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.py_xs, a.gap_xs, a.mt_sm]}>
|
|
||||||
<View style={[a.flex_row, a.align_center]}>
|
|
||||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
|
||||||
{langName ? (
|
|
||||||
<Trans>Translated from {langName}</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Translated</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
{sourceLanguage != null && (
|
|
||||||
<>
|
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
|
||||||
{' '}
|
|
||||||
·{' '}
|
|
||||||
</Text>
|
|
||||||
<TranslationLanguageSelect
|
|
||||||
sourceLanguage={sourceLanguage}
|
|
||||||
postText={postText}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Text emoji selectable style={[a.text_md, a.leading_snug]}>
|
|
||||||
{translatedText}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TranslationLanguageSelect({
|
|
||||||
postText,
|
postText,
|
||||||
sourceLanguage,
|
sourceLanguage,
|
||||||
}: {
|
}: {
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {codeToLanguageName} from '#/locale/helpers'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useTranslateOnDevice} from '#/features/translation'
|
||||||
|
import {TranslationLanguageSelect} from './TranslationLanguageSelect'
|
||||||
|
|
||||||
|
export function TranslationResult({
|
||||||
|
postText,
|
||||||
|
hideLoading = false,
|
||||||
|
}: {
|
||||||
|
postText: string
|
||||||
|
hideLoading: boolean
|
||||||
|
}) {
|
||||||
|
const {translationState} = useTranslateOnDevice()
|
||||||
|
|
||||||
|
if (translationState.status === 'loading' && !hideLoading) {
|
||||||
|
return <TranslationLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translationState.status === 'success') {
|
||||||
|
return (
|
||||||
|
<TranslationSuccess
|
||||||
|
postText={postText}
|
||||||
|
sourceLanguage={translationState.sourceLanguage}
|
||||||
|
translatedText={translationState.translatedText}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function TranslationLoading() {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}>
|
||||||
|
<Loader size="sm" />
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Translating…</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TranslationSuccess({
|
||||||
|
postText,
|
||||||
|
sourceLanguage,
|
||||||
|
translatedText,
|
||||||
|
}: {
|
||||||
|
postText: string
|
||||||
|
sourceLanguage: string | null
|
||||||
|
translatedText: string
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
|
||||||
|
const langName = sourceLanguage
|
||||||
|
? codeToLanguageName(sourceLanguage, i18n.locale)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.py_xs, a.gap_xs, a.mt_sm]}>
|
||||||
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
|
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||||
|
{langName ? (
|
||||||
|
<Trans>Translated from {langName}</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Translated</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
{sourceLanguage != null && (
|
||||||
|
<>
|
||||||
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
</Text>
|
||||||
|
<TranslationLanguageSelect
|
||||||
|
sourceLanguage={sourceLanguage}
|
||||||
|
postText={postText}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text emoji selectable style={[a.text_md, a.leading_snug]}>
|
||||||
|
{translatedText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import React, {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {LayoutAnimation, Platform} from 'react-native'
|
import {LayoutAnimation, Platform} from 'react-native'
|
||||||
import {getLocales} from 'expo-localization'
|
import {getLocales} from 'expo-localization'
|
||||||
import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types'
|
import {onTranslateTask} from '@bsky.app/expo-translate-text'
|
||||||
|
|
||||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||||
import {getTranslatorLink} from '#/locale/helpers'
|
import {getTranslatorLink} from '#/locale/helpers'
|
||||||
@@ -16,6 +16,8 @@ import {useLanguagePrefs} from '#/state/preferences'
|
|||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
|
|
||||||
|
type TranslationTaskResult = Awaited<ReturnType<typeof onTranslateTask>>
|
||||||
|
|
||||||
type TranslationState =
|
type TranslationState =
|
||||||
| {status: 'idle'}
|
| {status: 'idle'}
|
||||||
| {status: 'loading'}
|
| {status: 'loading'}
|
||||||
@@ -68,10 +70,6 @@ async function attemptTranslation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onTranslateTask} =
|
|
||||||
// Needed in order to type check the dynamically imported module.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
||||||
require('@bsky.app/expo-translate-text') as typeof import('@bsky.app/expo-translate-text')
|
|
||||||
const result = await onTranslateTask({
|
const result = await onTranslateTask({
|
||||||
input,
|
input,
|
||||||
targetLangCode,
|
targetLangCode,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {memo, useCallback, useMemo} from 'react'
|
import {memo, useCallback, useMemo} from 'react'
|
||||||
import {type GestureResponderEvent, Text as RNText, View} from 'react-native'
|
import {Text as RNText, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
@@ -9,13 +9,11 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {niceDate} from '#/lib/strings/time'
|
import {niceDate} from '#/lib/strings/time'
|
||||||
import {getPostLanguage, isPostInLanguage} from '#/locale/helpers'
|
|
||||||
import {
|
import {
|
||||||
POST_TOMBSTONE,
|
POST_TOMBSTONE,
|
||||||
type Shadow,
|
type Shadow,
|
||||||
@@ -23,7 +21,6 @@ import {
|
|||||||
} from '#/state/cache/post-shadow'
|
} from '#/state/cache/post-shadow'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
|
||||||
import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||||
@@ -36,19 +33,17 @@ import {
|
|||||||
OUTER_SPACE,
|
OUTER_SPACE,
|
||||||
REPLY_LINE_WIDTH,
|
REPLY_LINE_WIDTH,
|
||||||
} from '#/screens/PostThread/const'
|
} from '#/screens/PostThread/const'
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
|
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
|
||||||
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
|
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {TranslatedPost} from '#/components/Post/Translated'
|
|
||||||
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
|
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
|
||||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
@@ -60,10 +55,9 @@ import {VerificationCheckButton} from '#/components/verification/VerificationChe
|
|||||||
import {WhoCanReply} from '#/components/WhoCanReply'
|
import {WhoCanReply} from '#/components/WhoCanReply'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {useActorStatus} from '#/features/liveNow'
|
import {useActorStatus} from '#/features/liveNow'
|
||||||
import {
|
import {Provider as TranslateOnDeviceProvider} from '#/features/translation'
|
||||||
Provider as TranslateOnDeviceProvider,
|
import {InlineTranslateButton} from '#/features/translation/components/InlineTranslateButton'
|
||||||
useTranslateOnDevice,
|
import {TranslationResult} from '#/features/translation/components/TranslationResult'
|
||||||
} from '#/translation'
|
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function ThreadItemAnchor({
|
export function ThreadItemAnchor({
|
||||||
@@ -417,8 +411,8 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
shouldProxyLinks={true}
|
shouldProxyLinks={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<TranslatedPost postText={record.text} hideLoading />
|
<TranslationResult postText={record.text} hideLoading />
|
||||||
<TranslateLink post={item.value.post} />
|
<InlineTranslateButton post={item.value.post} />
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.py_xs]}>
|
<View style={[a.py_xs]}>
|
||||||
<Embed
|
<Embed
|
||||||
@@ -554,101 +548,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function TranslateLink({
|
|
||||||
post,
|
|
||||||
}: {
|
|
||||||
post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post']
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const ax = useAnalytics()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const langPrefs = useLanguagePrefs()
|
|
||||||
|
|
||||||
const {translate, clearTranslation, translationState} = useTranslateOnDevice()
|
|
||||||
|
|
||||||
const needsTranslation = useMemo(
|
|
||||||
() =>
|
|
||||||
Boolean(
|
|
||||||
langPrefs.primaryLanguage &&
|
|
||||||
!isPostInLanguage(post, [langPrefs.primaryLanguage]),
|
|
||||||
),
|
|
||||||
[post, langPrefs.primaryLanguage],
|
|
||||||
)
|
|
||||||
|
|
||||||
const sourceLanguage = getPostLanguage(post)
|
|
||||||
|
|
||||||
const onTranslatePress = useCallback(
|
|
||||||
(e: GestureResponderEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
void translate(
|
|
||||||
post.record.text || '',
|
|
||||||
langPrefs.primaryLanguage,
|
|
||||||
sourceLanguage,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
|
||||||
post.record,
|
|
||||||
AppBskyFeedPost.isRecord,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ax.metric('translate', {
|
|
||||||
sourceLanguages: post.record.langs ?? [],
|
|
||||||
targetLanguage: langPrefs.primaryLanguage,
|
|
||||||
textLength: post.record.text.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
[ax, sourceLanguage, translate, langPrefs, post],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onHideTranslation = useCallback(
|
|
||||||
(e: GestureResponderEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
clearTranslation()
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
[clearTranslation],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
needsTranslation && (
|
|
||||||
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
|
||||||
{translationState.status === 'loading' ? (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
|
||||||
<Loader size="xs" />
|
|
||||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
|
||||||
<Trans>Translating…</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : translationState.status === 'success' ? (
|
|
||||||
<Button
|
|
||||||
label={l`Hide translation`}
|
|
||||||
onPress={onHideTranslation}
|
|
||||||
hoverStyle={native({opacity: 0.5})}
|
|
||||||
hitSlop={HITSLOP_30}>
|
|
||||||
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
|
|
||||||
<Trans>Hide translation</Trans>
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
label={l`Translate`}
|
|
||||||
onPress={onTranslatePress}
|
|
||||||
hoverStyle={native({opacity: 0.5})}
|
|
||||||
hitSlop={HITSLOP_30}>
|
|
||||||
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
|
|
||||||
<Trans>Translate</Trans>
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExpandedPostDetails({
|
function ExpandedPostDetails({
|
||||||
post,
|
post,
|
||||||
isThreadAuthor,
|
isThreadAuthor,
|
||||||
|
|||||||
Reference in New Issue
Block a user