On-device translation
This commit is contained in:
@@ -164,6 +164,7 @@
|
|||||||
"expo-splash-screen": "~31.0.12",
|
"expo-splash-screen": "~31.0.12",
|
||||||
"expo-system-ui": "~6.0.9",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-task-manager": "~14.0.9",
|
"expo-task-manager": "~14.0.9",
|
||||||
|
"expo-translate-text": "^0.1.0",
|
||||||
"expo-updates": "~29.0.14",
|
"expo-updates": "~29.0.14",
|
||||||
"expo-video": "~3.0.15",
|
"expo-video": "~3.0.15",
|
||||||
"expo-video-thumbnails": "^10.0.8",
|
"expo-video-thumbnails": "^10.0.8",
|
||||||
|
|||||||
@@ -667,6 +667,9 @@ export type Events = {
|
|||||||
targetLanguage: string
|
targetLanguage: string
|
||||||
textLength: number
|
textLength: number
|
||||||
}
|
}
|
||||||
|
'translate:result': {
|
||||||
|
method: 'on-device' | 'google-translate' | 'fallback-alert'
|
||||||
|
}
|
||||||
|
|
||||||
'verification:create': {}
|
'verification:create': {}
|
||||||
'verification:revoke': {}
|
'verification:revoke': {}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {codeToLanguageName} from '#/locale/helpers'
|
||||||
|
import {useTranslationState} from '#/state/translation'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function TranslatedPost({
|
||||||
|
postUri,
|
||||||
|
hideLoading,
|
||||||
|
}: {
|
||||||
|
postUri: string
|
||||||
|
hideLoading?: boolean
|
||||||
|
}) {
|
||||||
|
const state = useTranslationState(postUri)
|
||||||
|
|
||||||
|
if (state.status === 'loading' && !hideLoading) {
|
||||||
|
return <TranslationLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'success') {
|
||||||
|
return (
|
||||||
|
<TranslationResult
|
||||||
|
translatedText={state.translatedText}
|
||||||
|
sourceLanguage={state.sourceLanguage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
translatedText,
|
||||||
|
sourceLanguage,
|
||||||
|
}: {
|
||||||
|
translatedText: string
|
||||||
|
sourceLanguage: string
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
|
||||||
|
const langName = sourceLanguage
|
||||||
|
? codeToLanguageName(sourceLanguage, i18n.locale)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.py_xs, a.gap_xs]}>
|
||||||
|
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||||
|
{langName ? (
|
||||||
|
<Trans>Translated from {langName}</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Translated</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text selectable style={[a.text_md]}>
|
||||||
|
{translatedText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -258,7 +258,7 @@ let PostMenuItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPressTranslate = () => {
|
const onPressTranslate = () => {
|
||||||
translate(record.text, langPrefs.primaryLanguage)
|
translate(record.text, langPrefs.primaryLanguage, {postUri: post.uri})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||||
|
|||||||
@@ -1,54 +1,109 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import * as IntentLauncher from 'expo-intent-launcher'
|
import {Alert} from 'react-native'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {getTranslatorLink} from '#/locale/helpers'
|
import {getTranslatorLink} from '#/locale/helpers'
|
||||||
import {IS_ANDROID} from '#/env'
|
import {logger} from '#/logger'
|
||||||
|
import {setTranslationState} from '#/state/translation'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {useOpenLink} from './useOpenLink'
|
import {useOpenLink} from './useOpenLink'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts on-device translation via expo-translate-text.
|
||||||
|
* Uses a lazy require to avoid crashing if the native module
|
||||||
|
* isn't linked into the current build.
|
||||||
|
*/
|
||||||
|
async function attemptTranslation(
|
||||||
|
text: string,
|
||||||
|
language: string,
|
||||||
|
postUri: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const {onTranslateTask} =
|
||||||
|
require('expo-translate-text') as typeof import('expo-translate-text')
|
||||||
|
const result = await onTranslateTask({
|
||||||
|
input: text,
|
||||||
|
targetLangCode: language,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTranslationState(postUri, {
|
||||||
|
status: 'success',
|
||||||
|
translatedText:
|
||||||
|
typeof result.translatedTexts === 'string' ? result.translatedTexts : '',
|
||||||
|
sourceLanguage: result.sourceLanguage ?? '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native translation hook. Attempts on-device translation using
|
||||||
|
* Apple Translation (iOS 18+) or Google ML Kit (Android).
|
||||||
|
* Falls back to Google Translate URL if the language pack is
|
||||||
|
* unavailable or the user declines to download it.
|
||||||
|
*
|
||||||
|
* Web uses useTranslate.web.ts which always opens Google Translate.
|
||||||
|
*/
|
||||||
export function useTranslate() {
|
export function useTranslate() {
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (text: string, language: string) => {
|
async (text: string, language: string, opts?: {postUri?: string}) => {
|
||||||
const translateUrl = getTranslatorLink(text, language)
|
// No postUri means non-post context (e.g. DMs) — open Google Translate
|
||||||
if (IS_ANDROID) {
|
if (!opts?.postUri) {
|
||||||
try {
|
const translateUrl = getTranslatorLink(text, language)
|
||||||
// use getApplicationIconAsync to determine if the translate app is installed
|
|
||||||
if (
|
|
||||||
!(await IntentLauncher.getApplicationIconAsync(
|
|
||||||
'com.google.android.apps.translate',
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
throw new Error('Translate app not installed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this should only be called one at a time, use something like
|
|
||||||
// RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn
|
|
||||||
await IntentLauncher.startActivityAsync(
|
|
||||||
'android.intent.action.PROCESS_TEXT',
|
|
||||||
{
|
|
||||||
type: 'text/plain',
|
|
||||||
extra: {
|
|
||||||
'android.intent.extra.PROCESS_TEXT': text,
|
|
||||||
'android.intent.extra.PROCESS_TEXT_READONLY': true,
|
|
||||||
},
|
|
||||||
// note: to skip the intermediate app select, we need to specify a
|
|
||||||
// `className`. however, this isn't safe to hardcode, we'd need to query the
|
|
||||||
// package manager for the correct activity. this requires native code, so
|
|
||||||
// skip for now -sfn
|
|
||||||
// packageName: 'com.google.android.apps.translate',
|
|
||||||
// className: 'com.google.android.apps.translate.TranslateActivity',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
if (__DEV__) console.error(err)
|
|
||||||
// most likely means they don't have the translate app
|
|
||||||
await openLink(translateUrl)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await openLink(translateUrl)
|
await openLink(translateUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const postUri = opts.postUri
|
||||||
|
setTranslationState(postUri, {status: 'loading'})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await attemptTranslation(text, language, postUri)
|
||||||
|
ax.metric('translate:result', {method: 'on-device'})
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to translate post', {safeMessage: err})
|
||||||
|
setTranslationState(postUri, {status: 'idle'})
|
||||||
|
|
||||||
|
// On-device translation failed (language pack missing or user
|
||||||
|
// dismissed the download prompt). Show options to retry or
|
||||||
|
// fall back to Google Translate.
|
||||||
|
ax.metric('translate:result', {method: 'fallback-alert'})
|
||||||
|
Alert.alert(
|
||||||
|
_(msg`Translation unavailable`),
|
||||||
|
_(msg`The required language pack is not installed on your device.`),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: _(msg`Download language pack`),
|
||||||
|
onPress: async () => {
|
||||||
|
setTranslationState(postUri, {status: 'loading'})
|
||||||
|
try {
|
||||||
|
await attemptTranslation(text, language, postUri)
|
||||||
|
ax.metric('translate:result', {method: 'on-device'})
|
||||||
|
} catch (retryErr) {
|
||||||
|
logger.error('Failed to translate post', {
|
||||||
|
safeMessage: retryErr,
|
||||||
|
})
|
||||||
|
setTranslationState(postUri, {status: 'idle'})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: _(msg`Use Google Translate`),
|
||||||
|
onPress: () => {
|
||||||
|
ax.metric('translate:result', {method: 'google-translate'})
|
||||||
|
openLink(getTranslatorLink(text, language))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: _(msg`Cancel`),
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[openLink],
|
[_, ax, openLink],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
|
||||||
|
import {getTranslatorLink} from '#/locale/helpers'
|
||||||
|
import {useOpenLink} from './useOpenLink'
|
||||||
|
|
||||||
|
export function useTranslate() {
|
||||||
|
const openLink = useOpenLink()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async (text: string, language: string, _opts?: {postUri?: string}) => {
|
||||||
|
const translateUrl = getTranslatorLink(text, language)
|
||||||
|
await openLink(translateUrl)
|
||||||
|
},
|
||||||
|
[openLink],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ 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'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
|
import {clearTranslation, useTranslationState} from '#/state/translation'
|
||||||
import {type PostSource} from '#/state/unstable-post-source'
|
import {type PostSource} from '#/state/unstable-post-source'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
|
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
|
||||||
@@ -45,11 +46,13 @@ 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 {InlineLinkText, Link} from '#/components/Link'
|
import {InlineLinkText, 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/TranslatedPost'
|
||||||
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'
|
||||||
@@ -412,6 +415,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
shouldProxyLinks={true}
|
shouldProxyLinks={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
<TranslatedPost postUri={post.uri} hideLoading />
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.py_xs]}>
|
<View style={[a.py_xs]}>
|
||||||
<Embed
|
<Embed
|
||||||
@@ -558,10 +562,14 @@ function ExpandedPostDetails({
|
|||||||
[post, langPrefs.primaryLanguage],
|
[post, langPrefs.primaryLanguage],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const translationState = useTranslationState(post.uri)
|
||||||
|
|
||||||
const onTranslatePress = useCallback(
|
const onTranslatePress = useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
translate(post.record.text || '', langPrefs.primaryLanguage)
|
translate(post.record.text || '', langPrefs.primaryLanguage, {
|
||||||
|
postUri: post.uri,
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||||
@@ -581,6 +589,15 @@ function ExpandedPostDetails({
|
|||||||
[ax, translate, langPrefs, post],
|
[ax, translate, langPrefs, post],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onHideTranslation = useCallback(
|
||||||
|
(e: GestureResponderEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
clearTranslation(post.uri)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[post.uri],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
||||||
<BackdatedPostIndicator post={post} />
|
<BackdatedPostIndicator post={post} />
|
||||||
@@ -597,18 +614,33 @@ function ExpandedPostDetails({
|
|||||||
·
|
·
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<InlineLinkText
|
{translationState.status === 'loading' ? (
|
||||||
// overridden to open an intent on android, but keep
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
// as anchor tag for accessibility
|
<Loader size="xs" />
|
||||||
to={getTranslatorLink(
|
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
post.record.text,
|
<Trans>Translating...</Trans>
|
||||||
langPrefs.primaryLanguage,
|
</Text>
|
||||||
)}
|
</View>
|
||||||
label={_(msg`Translate`)}
|
) : translationState.status === 'success' ? (
|
||||||
style={[a.text_sm]}
|
<InlineLinkText
|
||||||
onPress={onTranslatePress}>
|
to="#"
|
||||||
<Trans>Translate</Trans>
|
label={_(msg`Hide translation`)}
|
||||||
</InlineLinkText>
|
style={[a.text_sm]}
|
||||||
|
onPress={onHideTranslation}>
|
||||||
|
<Trans>Hide translation</Trans>
|
||||||
|
</InlineLinkText>
|
||||||
|
) : (
|
||||||
|
<InlineLinkText
|
||||||
|
to={getTranslatorLink(
|
||||||
|
post.record.text,
|
||||||
|
langPrefs.primaryLanguage,
|
||||||
|
)}
|
||||||
|
label={_(msg`Translate`)}
|
||||||
|
style={[a.text_sm]}
|
||||||
|
onPress={onTranslatePress}>
|
||||||
|
<Trans>Translate</Trans>
|
||||||
|
</InlineLinkText>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Per-post translation state store. Uses the EventEmitter + Map pattern
|
||||||
|
* (same as post-shadow.ts) so only components subscribed to a specific
|
||||||
|
* post URI re-render when its translation state changes.
|
||||||
|
*/
|
||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import EventEmitter from 'eventemitter3'
|
||||||
|
|
||||||
|
export type TranslationState =
|
||||||
|
| {status: 'idle'}
|
||||||
|
| {status: 'loading'}
|
||||||
|
| {status: 'success'; translatedText: string; sourceLanguage: string}
|
||||||
|
|
||||||
|
const IDLE: TranslationState = {status: 'idle'}
|
||||||
|
|
||||||
|
const emitter = new EventEmitter()
|
||||||
|
const translations = new Map<string, TranslationState>()
|
||||||
|
|
||||||
|
export function setTranslationState(postUri: string, state: TranslationState) {
|
||||||
|
translations.set(postUri, state)
|
||||||
|
emitter.emit(postUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearTranslation(postUri: string) {
|
||||||
|
translations.delete(postUri)
|
||||||
|
emitter.emit(postUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslationState(postUri: string): TranslationState {
|
||||||
|
const [state, setState] = useState<TranslationState>(
|
||||||
|
() => translations.get(postUri) ?? IDLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onUpdate() {
|
||||||
|
setState(translations.get(postUri) ?? IDLE)
|
||||||
|
}
|
||||||
|
emitter.addListener(postUri, onUpdate)
|
||||||
|
return () => {
|
||||||
|
emitter.removeListener(postUri, onUpdate)
|
||||||
|
}
|
||||||
|
}, [postUri])
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
|
|||||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||||
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
||||||
|
import {TranslatedPost} from '#/components/Post/TranslatedPost'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {SubtleHover} from '#/components/SubtleHover'
|
import {SubtleHover} from '#/components/SubtleHover'
|
||||||
@@ -217,6 +218,7 @@ function PostInner({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
<TranslatedPost postUri={post.uri} />
|
||||||
{post.embed ? (
|
{post.embed ? (
|
||||||
<Embed
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {Embed} from '#/components/Post/Embed'
|
|||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||||
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
||||||
|
import {TranslatedPost} from '#/components/Post/TranslatedPost'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
@@ -481,6 +482,7 @@ let PostContent = ({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
<TranslatedPost postUri={post.uri} />
|
||||||
{postEmbed ? (
|
{postEmbed ? (
|
||||||
<View style={[a.pb_xs]}>
|
<View style={[a.pb_xs]}>
|
||||||
<Embed
|
<Embed
|
||||||
|
|||||||
@@ -3403,7 +3403,7 @@
|
|||||||
"@babel/parser" "^7.27.2"
|
"@babel/parser" "^7.27.2"
|
||||||
"@babel/types" "^7.27.1"
|
"@babel/types" "^7.27.1"
|
||||||
|
|
||||||
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
|
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
||||||
version "7.25.9"
|
version "7.25.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
||||||
@@ -3464,19 +3464,6 @@
|
|||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
|
||||||
version "7.25.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
|
||||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/code-frame" "^7.25.9"
|
|
||||||
"@babel/generator" "^7.25.9"
|
|
||||||
"@babel/parser" "^7.25.9"
|
|
||||||
"@babel/template" "^7.25.9"
|
|
||||||
"@babel/types" "^7.25.9"
|
|
||||||
debug "^4.3.1"
|
|
||||||
globals "^11.1.0"
|
|
||||||
|
|
||||||
"@babel/traverse@^7.26.10":
|
"@babel/traverse@^7.26.10":
|
||||||
version "7.26.10"
|
version "7.26.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
|
||||||
@@ -11777,6 +11764,11 @@ expo-task-manager@~14.0.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
unimodules-app-loader "~6.0.8"
|
unimodules-app-loader "~6.0.8"
|
||||||
|
|
||||||
|
expo-translate-text@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-translate-text/-/expo-translate-text-0.1.0.tgz#16904f1baac126d5a452b4196495d238dcbbb6af"
|
||||||
|
integrity sha512-zrRJ0Do6Gie12yZYzgD4McQNIvb54pfPsBRegkUSuADec9DrXdxJXvw/OieZ+L4oMQu0Fx/NJDnHGQoaa0CVgA==
|
||||||
|
|
||||||
expo-updates-interface@~2.0.0:
|
expo-updates-interface@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz#7721cb64c37bcb46b23827b2717ef451a9378749"
|
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz#7721cb64c37bcb46b23827b2717ef451a9378749"
|
||||||
@@ -18691,16 +18683,7 @@ string-natural-compare@^3.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -18859,7 +18842,7 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -18873,13 +18856,6 @@ strip-ansi@^5.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^4.1.0"
|
ansi-regex "^4.1.0"
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^7.0.1:
|
strip-ansi@^7.0.1:
|
||||||
version "7.1.0"
|
version "7.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||||
@@ -20276,7 +20252,7 @@ wordwrap@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@@ -20294,15 +20270,6 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|||||||
Reference in New Issue
Block a user