Update presentation based on latest design

This commit is contained in:
DS Boyce
2026-03-03 14:47:10 -08:00
parent 4d6f98ba87
commit 44a53ea66d
4 changed files with 170 additions and 39 deletions
+151 -15
View File
@@ -1,28 +1,49 @@
import {useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {Platform, View} from 'react-native' import {type GestureResponderEvent, Platform, View} from 'react-native'
import {type AppBskyFeedDefs} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants' import {HITSLOP_30} from '#/lib/constants'
import {useTranslate} from '#/lib/translation' import {useTranslate} from '#/lib/translation'
import {type TranslationFunction} from '#/lib/translation/types' import {type TranslationFunction} from '#/lib/translation/types'
import {codeToLanguageName, languageName} from '#/locale/helpers' import {
codeToLanguageName,
getTranslatorLink,
isPostInLanguage,
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, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' 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'
export function TranslatedPost({ export function TranslatedPost({
translationKey, post,
postText, postText,
}: { }: {
translationKey: string post: AppBskyFeedDefs.PostView
postText: string postText: string
}) { }) {
const {translate, translationState} = useTranslate({key: translationKey}) const langPrefs = useLanguagePrefs()
const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri,
})
const needsTranslation = useMemo(
() =>
Boolean(
langPrefs.primaryLanguage &&
!isPostInLanguage(post, [langPrefs.primaryLanguage]),
),
[post, langPrefs.primaryLanguage],
)
if (translationState.status === 'loading') { if (translationState.status === 'loading') {
return <TranslationLoading /> return <TranslationLoading />
@@ -31,6 +52,7 @@ export function TranslatedPost({
if (translationState.status === 'success') { if (translationState.status === 'success') {
return ( return (
<TranslationResult <TranslationResult
clearTranslation={clearTranslation}
translate={translate} translate={translate}
postText={postText} postText={postText}
sourceLanguage={translationState.sourceLanguage} sourceLanguage={translationState.sourceLanguage}
@@ -39,7 +61,15 @@ export function TranslatedPost({
) )
} }
return null return (
needsTranslation && (
<TranslationLink
postText={postText}
primaryLanguage={langPrefs.primaryLanguage}
translate={translate}
/>
)
)
} }
function TranslationLoading() { function TranslationLoading() {
@@ -57,34 +87,131 @@ function TranslationLoading() {
) )
} }
function TranslationLink({
postText,
primaryLanguage,
translate,
}: {
postText: string
primaryLanguage: string
translate: TranslationFunction
}) {
const t = useTheme()
const {t: l} = useLingui()
const ax = useAnalytics()
const handleTranslate = useCallback(
(e: GestureResponderEvent) => {
e.preventDefault()
void translate({
text: postText,
targetLangCode: primaryLanguage,
})
ax.metric('translate', {
sourceLanguages: [],
targetLanguage: primaryLanguage,
textLength: postText.length,
})
return false
},
[ax, postText, primaryLanguage, translate],
)
return (
<View style={[a.gap_md, a.pt_md, a.align_start]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<InlineLinkText
// Overridden to open an intent on android, but keep as anchor tag
// for accessibility
to={getTranslatorLink(postText, primaryLanguage)}
label={l`Translate`}
style={[a.text_sm]}
onPress={handleTranslate}>
<Trans>Translate</Trans>
</InlineLinkText>
</Text>
</View>
</View>
)
}
function TranslationResult({ function TranslationResult({
clearTranslation,
translate, translate,
postText, postText,
sourceLanguage, sourceLanguage,
translatedText, translatedText,
}: { }: {
clearTranslation: () => void
translate: TranslationFunction translate: TranslationFunction
postText: string postText: string
sourceLanguage: string | null sourceLanguage: string | null
translatedText: string translatedText: string
}) { }) {
const t = useTheme() const t = useTheme()
const {i18n} = useLingui() const langPrefs = useLanguagePrefs()
const {i18n, t: l} = useLingui()
const langName = sourceLanguage const langName = sourceLanguage
? codeToLanguageName(sourceLanguage, i18n.locale) ? codeToLanguageName(sourceLanguage, i18n.locale)
: undefined : undefined
return ( return (
<View style={[a.py_xs, a.gap_xs, a.mt_sm]}> <View>
<View style={[a.flex_row, a.align_center]}> <View
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}> style={[
a.px_lg,
a.pt_sm,
a.pb_md,
a.mt_sm,
a.border,
a.rounded_md,
t.atoms.border_contrast_high,
]}>
<View style={[a.flex_row, a.align_center, a.mb_xs]}>
{langName ? ( {langName ? (
<Trans>Translated from {langName}</Trans> <View style={[a.flex_row, a.align_center]}>
) : ( <Text
<Trans>Translated</Trans> style={[
a.text_xs,
a.font_medium,
t.atoms.text_contrast_medium,
]}>
{langName}{' '}
</Text>
<View>
<ArrowRight
size="sm"
fill={t.atoms.text_contrast_medium.color}
/>
</View>
<Text
style={[
a.text_xs,
a.font_medium,
t.atoms.text_contrast_medium,
]}>
{' '}
{codeToLanguageName(
langPrefs.primaryLanguage,
langPrefs.appLanguage,
)} )}
</Text> </Text>
</View>
) : (
<Text
style={[
a.text_xs,
a.font_medium,
t.atoms.text_contrast_medium,
a.mb_xs,
]}>
<Trans>Translated</Trans>
</Text>
)}
{sourceLanguage != null && ( {sourceLanguage != null && (
<> <>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
@@ -102,6 +229,15 @@ function TranslationResult({
<Text emoji selectable style={[a.text_md, a.leading_snug]}> <Text emoji selectable style={[a.text_md, a.leading_snug]}>
{translatedText} {translatedText}
</Text> </Text>
<Button
label={l`Hide translation`}
hitSlop={HITSLOP_30}
hoverStyle={native({opacity: 0.5})}
style={[a.absolute, a.z_10, {top: 12, right: 14}]}
onPress={clearTranslation}>
<Times size="sm" fill={t.atoms.text_contrast_medium.color} />
</Button>
</View>
</View> </View>
) )
} }
@@ -165,7 +301,7 @@ function TranslationLanguageSelect({
{...props} {...props}
hitSlop={HITSLOP_30} hitSlop={HITSLOP_30}
hoverStyle={native({opacity: 0.5})}> hoverStyle={native({opacity: 0.5})}>
<Text style={[a.text_xs]}> <Text style={[a.text_xs, a.font_medium]}>
<Trans>Change</Trans> <Trans>Change</Trans>
</Text> </Text>
</Button> </Button>
@@ -407,7 +407,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true} shouldProxyLinks={true}
/> />
) : undefined} ) : undefined}
<TranslatedPost translationKey={post.uri} postText={record.text} /> <TranslatedPost post={post} postText={record.text} />
{post.embed && ( {post.embed && (
<View style={[a.py_xs]}> <View style={[a.py_xs]}>
<Embed <Embed
+1 -4
View File
@@ -217,10 +217,7 @@ function PostInner({
onPress={onPressShowMore} onPress={onPressShowMore}
/> />
)} )}
<TranslatedPost <TranslatedPost post={post} postText={record.text} />
translationKey={post.uri}
postText={record.text}
/>
</View> </View>
) : undefined} ) : undefined}
{post.embed ? ( {post.embed ? (
+1 -3
View File
@@ -493,9 +493,7 @@ let PostContent = ({
/> />
)} )}
</View> </View>
{record && ( {record && <TranslatedPost post={post} postText={record.text} />}
<TranslatedPost translationKey={post.uri} postText={record.text} />
)}
</> </>
) : undefined} ) : undefined}
{postEmbed ? ( {postEmbed ? (