[APP-718] Improvements and fixes to language handling (#931)
* Add locale helpers for narrowing languages * Add a translate link to posts in a different language * Update language filtering to use narrowing when multiple declared * Fix a few more RTL layout cases * Fix typeszio/stable
parent
48844aa4c7
commit
ed5a88d9d8
|
@ -4,10 +4,7 @@ import {
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import * as bcp47Match from 'bcp-47-match'
|
import {isPostInLanguage} from '../../locale/helpers'
|
||||||
import lande from 'lande'
|
|
||||||
import {hasProp} from 'lib/type-guards'
|
|
||||||
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
|
|
||||||
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
||||||
|
|
||||||
export type FeedTunerFn = (
|
export type FeedTunerFn = (
|
||||||
|
@ -245,76 +242,29 @@ export class FeedTuner {
|
||||||
* returns an array of `FeedViewPostsSlice` objects.
|
* returns an array of `FeedViewPostsSlice` objects.
|
||||||
*/
|
*/
|
||||||
static preferredLangOnly(preferredLangsCode2: string[]) {
|
static preferredLangOnly(preferredLangsCode2: string[]) {
|
||||||
const langsCode3 = preferredLangsCode2.map(
|
|
||||||
l => LANGUAGES_MAP_CODE2[l]?.code3 || l,
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
slices: FeedViewPostsSlice[],
|
slices: FeedViewPostsSlice[],
|
||||||
): FeedViewPostsSlice[] => {
|
): FeedViewPostsSlice[] => {
|
||||||
// 1. Early return if no languages have been specified
|
// early return if no languages have been specified
|
||||||
if (!preferredLangsCode2.length || preferredLangsCode2.length === 0) {
|
if (!preferredLangsCode2.length || preferredLangsCode2.length === 0) {
|
||||||
return slices
|
return slices
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = slices.length - 1; i >= 0; i--) {
|
for (let i = slices.length - 1; i >= 0; i--) {
|
||||||
// 2. Set a flag to indicate whether the item has text in a preferred language
|
|
||||||
let hasPreferredLang = false
|
let hasPreferredLang = false
|
||||||
for (const item of slices[i].items) {
|
for (const item of slices[i].items) {
|
||||||
// 3. check if the post has a `langs` property and if it is in the list of preferred languages
|
if (isPostInLanguage(item.post, preferredLangsCode2)) {
|
||||||
// if it is, set the flag to true
|
|
||||||
// if language is declared, regardless of a match, break out of the loop
|
|
||||||
if (
|
|
||||||
hasProp(item.post.record, 'langs') &&
|
|
||||||
Array.isArray(item.post.record.langs)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
bcp47Match.basicFilter(
|
|
||||||
item.post.record.langs,
|
|
||||||
preferredLangsCode2,
|
|
||||||
).length > 0
|
|
||||||
) {
|
|
||||||
hasPreferredLang = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// 4. FALLBACK if no language declared :
|
|
||||||
// Get the most likely language of the text in the post from the `lande` library and
|
|
||||||
// check if it is in the list of preferred languages
|
|
||||||
// if it is, set the flag to true and break out of the loop
|
|
||||||
else if (
|
|
||||||
hasProp(item.post.record, 'text') &&
|
|
||||||
typeof item.post.record.text === 'string'
|
|
||||||
) {
|
|
||||||
// Treat empty text the same as no text
|
|
||||||
if (item.post.record.text.length === 0) {
|
|
||||||
hasPreferredLang = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
const langsProbabilityMap = lande(item.post.record.text)
|
|
||||||
const mostLikelyLang = langsProbabilityMap[0][0]
|
|
||||||
// const secondMostLikelyLang = langsProbabilityMap[1][0]
|
|
||||||
// const thirdMostLikelyLang = langsProbabilityMap[2][0]
|
|
||||||
|
|
||||||
// we check for code3 here because that is what the `lande` library returns
|
|
||||||
if (langsCode3.includes(mostLikelyLang)) {
|
|
||||||
hasPreferredLang = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 5. no text? roll with it (eg: image-only posts, reposts, etc.)
|
|
||||||
else {
|
|
||||||
hasPreferredLang = true
|
hasPreferredLang = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. if item does not fit preferred language, remove it
|
// if item does not fit preferred language, remove it
|
||||||
if (!hasPreferredLang) {
|
if (!hasPreferredLang) {
|
||||||
slices.splice(i, 1)
|
slices.splice(i, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 7. return the filtered list of items
|
|
||||||
return slices
|
return slices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||||
|
import lande from 'lande'
|
||||||
|
import {hasProp} from 'lib/type-guards'
|
||||||
|
import * as bcp47Match from 'bcp-47-match'
|
||||||
|
import {LANGUAGES_MAP_CODE2, LANGUAGES_MAP_CODE3} from './languages'
|
||||||
|
|
||||||
|
export function code2ToCode3(lang: string): string {
|
||||||
|
if (lang.length === 2) {
|
||||||
|
return LANGUAGES_MAP_CODE2[lang]?.code3 || lang
|
||||||
|
}
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
export function code3ToCode2(lang: string): string {
|
||||||
|
if (lang.length === 3) {
|
||||||
|
return LANGUAGES_MAP_CODE3[lang]?.code2 || lang
|
||||||
|
}
|
||||||
|
return lang
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPostLanguage(
|
||||||
|
post: AppBskyFeedDefs.PostView,
|
||||||
|
): string | undefined {
|
||||||
|
let candidates: string[] = []
|
||||||
|
let postText: string = ''
|
||||||
|
if (hasProp(post.record, 'text') && typeof post.record.text === 'string') {
|
||||||
|
postText = post.record.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
AppBskyFeedPost.isRecord(post.record) &&
|
||||||
|
hasProp(post.record, 'langs') &&
|
||||||
|
Array.isArray(post.record.langs)
|
||||||
|
) {
|
||||||
|
candidates = post.record.langs
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's only one declared language, use that
|
||||||
|
if (candidates?.length === 1) {
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// no text? can't determine
|
||||||
|
if (postText.trim().length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the language model
|
||||||
|
let langsProbabilityMap = lande(postText)
|
||||||
|
|
||||||
|
// filter down using declared languages
|
||||||
|
if (candidates?.length) {
|
||||||
|
langsProbabilityMap = langsProbabilityMap.filter(
|
||||||
|
([lang, _probability]: [string, number]) => {
|
||||||
|
return candidates.includes(code3ToCode2(lang))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (langsProbabilityMap[0]) {
|
||||||
|
return code3ToCode2(langsProbabilityMap[0][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPostInLanguage(
|
||||||
|
post: AppBskyFeedDefs.PostView,
|
||||||
|
targetLangs: string[],
|
||||||
|
): boolean {
|
||||||
|
const lang = getPostLanguage(post)
|
||||||
|
if (!lang) {
|
||||||
|
// the post has no text, so we just say "yes" for now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return bcp47Match.basicFilter(lang, targetLangs).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslatorLink(lang: string, text: string): string {
|
||||||
|
return encodeURI(
|
||||||
|
`https://translate.google.com/?sl=auto&tl=${lang}&text=${text}`,
|
||||||
|
)
|
||||||
|
}
|
|
@ -555,3 +555,7 @@ export const LANGUAGES_MAP_CODE2 = Object.fromEntries(
|
||||||
export const LANGUAGES_MAP_CODE3 = Object.fromEntries(
|
export const LANGUAGES_MAP_CODE3 = Object.fromEntries(
|
||||||
LANGUAGES.map(lang => [lang.code3, lang]),
|
LANGUAGES.map(lang => [lang.code3, lang]),
|
||||||
)
|
)
|
||||||
|
// some additional manual mappings (not clear if these should be in the "official" mappings)
|
||||||
|
if (LANGUAGES_MAP_CODE2.fa) {
|
||||||
|
LANGUAGES_MAP_CODE3.pes = LANGUAGES_MAP_CODE2.fa
|
||||||
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ export const ListCard = ({
|
||||||
{descriptionRichText ? (
|
{descriptionRichText ? (
|
||||||
<View style={styles.details}>
|
<View style={styles.details}>
|
||||||
<RichTextCom
|
<RichTextCom
|
||||||
style={pal.text}
|
style={[pal.text, s.flex1]}
|
||||||
numberOfLines={20}
|
numberOfLines={20}
|
||||||
richText={descriptionRichText}
|
richText={descriptionRichText}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -347,6 +347,7 @@ const styles = StyleSheet.create({
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
},
|
},
|
||||||
headerDescription: {
|
headerDescription: {
|
||||||
|
flex: 1,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
headerBtns: {
|
headerBtns: {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, {useCallback, useMemo} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {AccessibilityActionEvent, Linking, StyleSheet, View} from 'react-native'
|
import {AccessibilityActionEvent, Linking, StyleSheet, View} from 'react-native'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {AtUri} from '@atproto/api'
|
import {AtUri, AppBskyFeedDefs} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
|
@ -18,6 +18,7 @@ import {s} from 'lib/styles'
|
||||||
import {ago, niceDate} from 'lib/strings/time'
|
import {ago, niceDate} from 'lib/strings/time'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
|
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/post-embeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
|
@ -65,6 +66,13 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
}, [item.post.uri, item.post.author.handle])
|
}, [item.post.uri, item.post.author.handle])
|
||||||
const repostsTitle = 'Reposts of this post'
|
const repostsTitle = 'Reposts of this post'
|
||||||
|
|
||||||
|
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
||||||
|
const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '')
|
||||||
|
const needsTranslation = useMemo(
|
||||||
|
() => !isPostInLanguage(item.post, store.preferences.contentLanguages),
|
||||||
|
[item.post, store.preferences.contentLanguages],
|
||||||
|
)
|
||||||
|
|
||||||
const onPressReply = React.useCallback(() => {
|
const onPressReply = React.useCallback(() => {
|
||||||
store.shell.openComposer({
|
store.shell.openComposer({
|
||||||
replyTo: {
|
replyTo: {
|
||||||
|
@ -98,17 +106,9 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
Toast.show('Copied to clipboard')
|
Toast.show('Copied to clipboard')
|
||||||
}, [record])
|
}, [record])
|
||||||
|
|
||||||
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
|
||||||
|
|
||||||
const onOpenTranslate = React.useCallback(() => {
|
const onOpenTranslate = React.useCallback(() => {
|
||||||
Linking.openURL(
|
Linking.openURL(translatorUrl)
|
||||||
encodeURI(
|
}, [translatorUrl])
|
||||||
`https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
|
|
||||||
record?.text || ''
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, [record, primaryLanguage])
|
|
||||||
|
|
||||||
const onToggleThreadMute = React.useCallback(async () => {
|
const onToggleThreadMute = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -276,6 +276,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
type="post-text-lg"
|
type="post-text-lg"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
|
style={s.flex1}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -283,9 +284,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||||
</ImageHider>
|
</ImageHider>
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
<View style={[s.mt2, s.mb10]}>
|
<ExpandedPostDetails
|
||||||
<Text style={pal.textLight}>{niceDate(item.post.indexedAt)}</Text>
|
post={item.post}
|
||||||
</View>
|
translatorUrl={translatorUrl}
|
||||||
|
needsTranslation={needsTranslation}
|
||||||
|
/>
|
||||||
{hasEngagement ? (
|
{hasEngagement ? (
|
||||||
<View style={[styles.expandedInfo, pal.border]}>
|
<View style={[styles.expandedInfo, pal.border]}>
|
||||||
{item.post.repostCount ? (
|
{item.post.repostCount ? (
|
||||||
|
@ -411,7 +414,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<RichText
|
<RichText
|
||||||
type="post-text"
|
type="post-text"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
style={pal.text}
|
style={[pal.text, s.flex1]}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -419,6 +422,15 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<ImageHider style={s.mb10} moderation={item.moderation.thread}>
|
<ImageHider style={s.mb10} moderation={item.moderation.thread}>
|
||||||
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
<PostEmbeds embed={item.post.embed} style={s.mb10} />
|
||||||
</ImageHider>
|
</ImageHider>
|
||||||
|
{needsTranslation && (
|
||||||
|
<View style={[pal.borderDark, styles.translateLink]}>
|
||||||
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
<Text type="sm" style={pal.link}>
|
||||||
|
Translate this post
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
|
@ -473,6 +485,31 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function ExpandedPostDetails({
|
||||||
|
post,
|
||||||
|
needsTranslation,
|
||||||
|
translatorUrl,
|
||||||
|
}: {
|
||||||
|
post: AppBskyFeedDefs.PostView
|
||||||
|
needsTranslation: boolean
|
||||||
|
translatorUrl: string
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View style={[s.flexRow, s.mt2, s.mb10]}>
|
||||||
|
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
||||||
|
{needsTranslation && (
|
||||||
|
<>
|
||||||
|
<Text style={pal.textLight}> • </Text>
|
||||||
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
<Text style={pal.link}>Translate</Text>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
|
@ -540,6 +577,9 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 0,
|
paddingHorizontal: 0,
|
||||||
paddingBottom: 10,
|
paddingBottom: 10,
|
||||||
},
|
},
|
||||||
|
translateLink: {
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
contentHider: {
|
contentHider: {
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {getTranslatorLink} from '../../../locale/helpers'
|
||||||
|
|
||||||
export const Post = observer(function Post({
|
export const Post = observer(function Post({
|
||||||
uri,
|
uri,
|
||||||
|
@ -167,16 +168,11 @@ const PostLoaded = observer(
|
||||||
}, [record])
|
}, [record])
|
||||||
|
|
||||||
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
||||||
|
const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '')
|
||||||
|
|
||||||
const onOpenTranslate = React.useCallback(() => {
|
const onOpenTranslate = React.useCallback(() => {
|
||||||
Linking.openURL(
|
Linking.openURL(translatorUrl)
|
||||||
encodeURI(
|
}, [translatorUrl])
|
||||||
`https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
|
|
||||||
record?.text || ''
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, [record, primaryLanguage])
|
|
||||||
|
|
||||||
const onToggleThreadMute = React.useCallback(async () => {
|
const onToggleThreadMute = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -299,6 +295,7 @@ const PostLoaded = observer(
|
||||||
type="post-text"
|
type="post-text"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
|
style={s.flex1}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
|
import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
|
||||||
|
|
||||||
export const FeedItem = observer(function ({
|
export const FeedItem = observer(function ({
|
||||||
item,
|
item,
|
||||||
|
@ -62,6 +63,12 @@ export const FeedItem = observer(function ({
|
||||||
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
|
||||||
return urip.hostname
|
return urip.hostname
|
||||||
}, [record?.reply])
|
}, [record?.reply])
|
||||||
|
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
||||||
|
const translatorUrl = getTranslatorLink(primaryLanguage, record?.text || '')
|
||||||
|
const needsTranslation = useMemo(
|
||||||
|
() => !isPostInLanguage(item.post, store.preferences.contentLanguages),
|
||||||
|
[item.post, store.preferences.contentLanguages],
|
||||||
|
)
|
||||||
|
|
||||||
const onPressReply = React.useCallback(() => {
|
const onPressReply = React.useCallback(() => {
|
||||||
track('FeedItem:PostReply')
|
track('FeedItem:PostReply')
|
||||||
|
@ -98,17 +105,9 @@ export const FeedItem = observer(function ({
|
||||||
Toast.show('Copied to clipboard')
|
Toast.show('Copied to clipboard')
|
||||||
}, [record])
|
}, [record])
|
||||||
|
|
||||||
const primaryLanguage = store.preferences.contentLanguages[0] || 'en'
|
|
||||||
|
|
||||||
const onOpenTranslate = React.useCallback(() => {
|
const onOpenTranslate = React.useCallback(() => {
|
||||||
Linking.openURL(
|
Linking.openURL(translatorUrl)
|
||||||
encodeURI(
|
}, [translatorUrl])
|
||||||
`https://translate.google.com/?sl=auto&tl=${primaryLanguage}&text=${
|
|
||||||
record?.text || ''
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, [record, primaryLanguage])
|
|
||||||
|
|
||||||
const onToggleThreadMute = React.useCallback(async () => {
|
const onToggleThreadMute = React.useCallback(async () => {
|
||||||
track('FeedItem:ThreadMute')
|
track('FeedItem:ThreadMute')
|
||||||
|
@ -301,12 +300,22 @@ export const FeedItem = observer(function ({
|
||||||
type="post-text"
|
type="post-text"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
|
style={s.flex1}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<ImageHider moderation={item.moderation.list} style={styles.embed}>
|
<ImageHider moderation={item.moderation.list} style={styles.embed}>
|
||||||
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
<PostEmbeds embed={item.post.embed} style={styles.embed} />
|
||||||
</ImageHider>
|
</ImageHider>
|
||||||
|
{needsTranslation && (
|
||||||
|
<View style={[pal.borderDark, styles.translateLink]}>
|
||||||
|
<Link href={translatorUrl} title="Translate">
|
||||||
|
<Text type="sm" style={pal.link}>
|
||||||
|
Translate this post
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
style={styles.ctrls}
|
style={styles.ctrls}
|
||||||
|
@ -402,6 +411,9 @@ const styles = StyleSheet.create({
|
||||||
embed: {
|
embed: {
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
|
translateLink: {
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
ctrls: {
|
ctrls: {
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
|
|
@ -609,6 +609,7 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
description: {
|
description: {
|
||||||
|
flex: 1,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue