Language selection and suggestion UX improvements (#9067)
* feat: don't retain accepted language suggestion after finishing or exiting post (#8886) * feat: don't retain accepted language suggestion after finishing or exiting post * fix: rebase fixes * fix: rebase fixes * chore: lint * Rename onChange for clarity * Improve logic in composer * Handle user override more explicitly * Drill in onSelectLanguage callback into dialog too * Fix typo Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Make text crystal clear * Handle multiple languages --------- Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com> Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -71,7 +71,7 @@ const schema = z.object({
|
|||||||
contentLanguages: z.array(z.string()),
|
contentLanguages: z.array(z.string()),
|
||||||
/**
|
/**
|
||||||
* The language(s) the user is currently posting in, configured within the
|
* The language(s) the user is currently posting in, configured within the
|
||||||
* composer. Multiple languages are psearate by commas.
|
* composer. Multiple languages are separated by commas.
|
||||||
*
|
*
|
||||||
* BCP-47 2-letter language code without region.
|
* BCP-47 2-letter language code without region.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ export function toPostLanguages(postLanguage: string): string[] {
|
|||||||
return postLanguage.split(',').filter(Boolean)
|
return postLanguage.split(',').filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fromPostLanguages(languages: string[]): string {
|
||||||
|
return languages.filter(Boolean).join(',')
|
||||||
|
}
|
||||||
|
|
||||||
export function hasPostLanguage(postLanguage: string, code2: string): boolean {
|
export function hasPostLanguage(postLanguage: string, code2: string): boolean {
|
||||||
return toPostLanguages(postLanguage).includes(code2)
|
return toPostLanguages(postLanguage).includes(code2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import {
|
|||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||||
import {
|
import {
|
||||||
|
fromPostLanguages,
|
||||||
toPostLanguages,
|
toPostLanguages,
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
useLanguagePrefsApi,
|
useLanguagePrefsApi,
|
||||||
@@ -197,6 +198,44 @@ export const ComposePost = ({
|
|||||||
const [publishingStage, setPublishingStage] = useState('')
|
const [publishingStage, setPublishingStage] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temporary local reference to a language suggestion that the user has
|
||||||
|
* accepted. This overrides the global post language preference, but is not
|
||||||
|
* stored permanently.
|
||||||
|
*/
|
||||||
|
const [acceptedLanguageSuggestion, setAcceptedLanguageSuggestion] = useState<
|
||||||
|
string | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The language(s) of the post being replied to.
|
||||||
|
*/
|
||||||
|
const [replyToLanguages, setReplyToLanguages] = useState<string[]>(
|
||||||
|
replyTo?.langs || [],
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected languages of the post. Prefer local temporary
|
||||||
|
* language suggestion over global lang prefs, if available.
|
||||||
|
*/
|
||||||
|
const currentLanguages = useMemo(
|
||||||
|
() =>
|
||||||
|
acceptedLanguageSuggestion
|
||||||
|
? [acceptedLanguageSuggestion]
|
||||||
|
: toPostLanguages(langPrefs.postLanguage),
|
||||||
|
[acceptedLanguageSuggestion, langPrefs.postLanguage],
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user selects a language from the composer language selector,
|
||||||
|
* clear any temporary language suggestions they may have selected
|
||||||
|
* previously, and any we might try to suggest to them.
|
||||||
|
*/
|
||||||
|
const onSelectLanguage = () => {
|
||||||
|
setAcceptedLanguageSuggestion(null)
|
||||||
|
setReplyToLanguages([])
|
||||||
|
}
|
||||||
|
|
||||||
const [composerState, composerDispatch] = useReducer(
|
const [composerState, composerDispatch] = useReducer(
|
||||||
composerReducer,
|
composerReducer,
|
||||||
{
|
{
|
||||||
@@ -414,7 +453,7 @@ export const ComposePost = ({
|
|||||||
thread,
|
thread,
|
||||||
replyTo: replyTo?.uri,
|
replyTo: replyTo?.uri,
|
||||||
onStateChange: setPublishingStage,
|
onStateChange: setPublishingStage,
|
||||||
langs: toPostLanguages(langPrefs.postLanguage),
|
langs: currentLanguages,
|
||||||
})
|
})
|
||||||
).uris[0]
|
).uris[0]
|
||||||
|
|
||||||
@@ -490,7 +529,7 @@ export const ComposePost = ({
|
|||||||
isPartOfThread: thread.posts.length > 1,
|
isPartOfThread: thread.posts.length > 1,
|
||||||
hasLink: !!post.embed.link,
|
hasLink: !!post.embed.link,
|
||||||
hasQuote: !!post.embed.quote,
|
hasQuote: !!post.embed.quote,
|
||||||
langs: langPrefs.postLanguage,
|
langs: fromPostLanguages(currentLanguages),
|
||||||
logContext: 'Composer',
|
logContext: 'Composer',
|
||||||
})
|
})
|
||||||
index++
|
index++
|
||||||
@@ -557,7 +596,7 @@ export const ComposePost = ({
|
|||||||
thread,
|
thread,
|
||||||
canPost,
|
canPost,
|
||||||
isPublishing,
|
isPublishing,
|
||||||
langPrefs.postLanguage,
|
currentLanguages,
|
||||||
onClose,
|
onClose,
|
||||||
onPost,
|
onPost,
|
||||||
onPostSuccess,
|
onPostSuccess,
|
||||||
@@ -654,8 +693,9 @@ export const ComposePost = ({
|
|||||||
<>
|
<>
|
||||||
<SuggestedLanguage
|
<SuggestedLanguage
|
||||||
text={activePost.richtext.text}
|
text={activePost.richtext.text}
|
||||||
// NOTE(@elijaharita): currently just choosing the first language if any exists
|
replyToLanguages={replyToLanguages}
|
||||||
replyToLanguage={replyTo?.langs?.[0]}
|
currentLanguages={currentLanguages}
|
||||||
|
onAcceptSuggestedLanguage={setAcceptedLanguageSuggestion}
|
||||||
/>
|
/>
|
||||||
<ComposerPills
|
<ComposerPills
|
||||||
isReply={!!replyTo}
|
isReply={!!replyTo}
|
||||||
@@ -678,6 +718,8 @@ export const ComposePost = ({
|
|||||||
type: 'add_post',
|
type: 'add_post',
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
currentLanguages={currentLanguages}
|
||||||
|
onSelectLanguage={onSelectLanguage}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -1289,6 +1331,8 @@ function ComposerFooter({
|
|||||||
onEmojiButtonPress,
|
onEmojiButtonPress,
|
||||||
onSelectVideo,
|
onSelectVideo,
|
||||||
onAddPost,
|
onAddPost,
|
||||||
|
currentLanguages,
|
||||||
|
onSelectLanguage,
|
||||||
}: {
|
}: {
|
||||||
post: PostDraft
|
post: PostDraft
|
||||||
dispatch: (action: PostAction) => void
|
dispatch: (action: PostAction) => void
|
||||||
@@ -1297,6 +1341,8 @@ function ComposerFooter({
|
|||||||
onError: (error: string) => void
|
onError: (error: string) => void
|
||||||
onSelectVideo: (postId: string, asset: ImagePickerAsset) => void
|
onSelectVideo: (postId: string, asset: ImagePickerAsset) => void
|
||||||
onAddPost: () => void
|
onAddPost: () => void
|
||||||
|
currentLanguages: string[]
|
||||||
|
onSelectLanguage?: (language: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -1450,7 +1496,10 @@ function ComposerFooter({
|
|||||||
<PlusIcon size="lg" />
|
<PlusIcon size="lg" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<PostLanguageSelect />
|
<PostLanguageSelect
|
||||||
|
currentLanguages={currentLanguages}
|
||||||
|
onSelectLanguage={onSelectLanguage}
|
||||||
|
/>
|
||||||
<CharProgress
|
<CharProgress
|
||||||
count={post.shortenedGraphemeLength}
|
count={post.shortenedGraphemeLength}
|
||||||
style={{width: 65}}
|
style={{width: 65}}
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ import * as Menu from '#/components/Menu'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {PostLanguageSelectDialog} from './PostLanguageSelectDialog'
|
import {PostLanguageSelectDialog} from './PostLanguageSelectDialog'
|
||||||
|
|
||||||
export function PostLanguageSelect() {
|
export function PostLanguageSelect({
|
||||||
|
currentLanguages: currentLanguagesProp,
|
||||||
|
onSelectLanguage,
|
||||||
|
}: {
|
||||||
|
currentLanguages?: string[]
|
||||||
|
onSelectLanguage?: (language: string) => void
|
||||||
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
@@ -27,6 +33,9 @@ export function PostLanguageSelect() {
|
|||||||
new Set([...langPrefs.postLanguageHistory, langPrefs.postLanguage]),
|
new Set([...langPrefs.postLanguageHistory, langPrefs.postLanguage]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const currentLanguages =
|
||||||
|
currentLanguagesProp ?? toPostLanguages(langPrefs.postLanguage)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dedupedHistory.length === 1 &&
|
dedupedHistory.length === 1 &&
|
||||||
dedupedHistory[0] === langPrefs.postLanguage
|
dedupedHistory[0] === langPrefs.postLanguage
|
||||||
@@ -34,7 +43,10 @@ export function PostLanguageSelect() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageBtn onPress={languageDialogControl.open} />
|
<LanguageBtn onPress={languageDialogControl.open} />
|
||||||
<PostLanguageSelectDialog control={languageDialogControl} />
|
<PostLanguageSelectDialog
|
||||||
|
control={languageDialogControl}
|
||||||
|
currentLanguages={currentLanguages}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -43,7 +55,9 @@ export function PostLanguageSelect() {
|
|||||||
<>
|
<>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Select post language`)}>
|
<Menu.Trigger label={_(msg`Select post language`)}>
|
||||||
{({props}) => <LanguageBtn {...props} />}
|
{({props}) => (
|
||||||
|
<LanguageBtn currentLanguages={currentLanguages} {...props} />
|
||||||
|
)}
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
<Menu.Outer>
|
<Menu.Outer>
|
||||||
<Menu.Group>
|
<Menu.Group>
|
||||||
@@ -56,10 +70,13 @@ export function PostLanguageSelect() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
key={historyItem}
|
key={historyItem}
|
||||||
label={_(msg`Select ${langName}`)}
|
label={_(msg`Select ${langName}`)}
|
||||||
onPress={() => setLangPrefs.setPostLanguage(historyItem)}>
|
onPress={() => {
|
||||||
|
setLangPrefs.setPostLanguage(historyItem)
|
||||||
|
onSelectLanguage?.(historyItem)
|
||||||
|
}}>
|
||||||
<Menu.ItemText>{langName}</Menu.ItemText>
|
<Menu.ItemText>{langName}</Menu.ItemText>
|
||||||
<Menu.ItemRadio
|
<Menu.ItemRadio
|
||||||
selected={historyItem === langPrefs.postLanguage}
|
selected={currentLanguages.includes(historyItem)}
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)
|
)
|
||||||
@@ -77,17 +94,26 @@ export function PostLanguageSelect() {
|
|||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
<PostLanguageSelectDialog control={languageDialogControl} />
|
<PostLanguageSelectDialog
|
||||||
|
control={languageDialogControl}
|
||||||
|
currentLanguages={currentLanguages}
|
||||||
|
onSelectLanguage={onSelectLanguage}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LanguageBtn(props: Omit<ButtonProps, 'label' | 'children'>) {
|
function LanguageBtn(
|
||||||
|
props: Omit<ButtonProps, 'label' | 'children'> & {
|
||||||
|
currentLanguages?: string[]
|
||||||
|
},
|
||||||
|
) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
|
const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
|
||||||
|
const currentLanguages = props.currentLanguages ?? postLanguagesPref
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -106,7 +132,7 @@ function LanguageBtn(props: Omit<ButtonProps, 'label' | 'children'>) {
|
|||||||
{({pressed, hovered}) => {
|
{({pressed, hovered}) => {
|
||||||
const color =
|
const color =
|
||||||
pressed || hovered ? t.palette.primary_300 : t.palette.primary_500
|
pressed || hovered ? t.palette.primary_300 : t.palette.primary_500
|
||||||
if (postLanguagesPref.length > 0) {
|
if (currentLanguages.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@@ -117,7 +143,7 @@ function LanguageBtn(props: Omit<ButtonProps, 'label' | 'children'>) {
|
|||||||
{maxWidth: 100},
|
{maxWidth: 100},
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{postLanguagesPref
|
{currentLanguages
|
||||||
.map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
|
.map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
|
||||||
.join(', ')}
|
.join(', ')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {languageName} from '#/locale/helpers'
|
|||||||
import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
|
import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
|
toPostLanguages,
|
||||||
useLanguagePrefs,
|
useLanguagePrefs,
|
||||||
useLanguagePrefsApi,
|
useLanguagePrefsApi,
|
||||||
} from '#/state/preferences/languages'
|
} from '#/state/preferences/languages'
|
||||||
@@ -23,8 +24,16 @@ import {Text} from '#/components/Typography'
|
|||||||
|
|
||||||
export function PostLanguageSelectDialog({
|
export function PostLanguageSelectDialog({
|
||||||
control,
|
control,
|
||||||
|
/**
|
||||||
|
* Optionally can be passed to show different values than what is saved in
|
||||||
|
* langPrefs.
|
||||||
|
*/
|
||||||
|
currentLanguages,
|
||||||
|
onSelectLanguage,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
|
currentLanguages?: string[]
|
||||||
|
onSelectLanguage?: (language: string) => void
|
||||||
}) {
|
}) {
|
||||||
const {height} = useWindowDimensions()
|
const {height} = useWindowDimensions()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
@@ -40,13 +49,22 @@ export function PostLanguageSelectDialog({
|
|||||||
nativeOptions={{minHeight: height - insets.top}}>
|
nativeOptions={{minHeight: height - insets.top}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||||
<DialogInner />
|
<DialogInner
|
||||||
|
currentLanguages={currentLanguages}
|
||||||
|
onSelectLanguage={onSelectLanguage}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogInner() {
|
export function DialogInner({
|
||||||
|
currentLanguages,
|
||||||
|
onSelectLanguage,
|
||||||
|
}: {
|
||||||
|
currentLanguages?: string[]
|
||||||
|
onSelectLanguage?: (language: string) => void
|
||||||
|
}) {
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const [headerHeight, setHeaderHeight] = useState(0)
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
|
|
||||||
@@ -63,8 +81,11 @@ export function DialogInner() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
|
const postLanguagesPref =
|
||||||
|
currentLanguages ?? toPostLanguages(langPrefs.postLanguage)
|
||||||
|
|
||||||
const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>(
|
const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>(
|
||||||
langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage],
|
postLanguagesPref || [langPrefs.primaryLanguage],
|
||||||
)
|
)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
@@ -79,6 +100,7 @@ export function DialogInner() {
|
|||||||
langsString = langPrefs.primaryLanguage
|
langsString = langPrefs.primaryLanguage
|
||||||
}
|
}
|
||||||
setLangPrefs.setPostLanguage(langsString)
|
setLangPrefs.setPostLanguage(langsString)
|
||||||
|
onSelectLanguage?.(langsString)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {Text as RNText, View} from 'react-native'
|
||||||
import {parseLanguage} from '@atproto/api'
|
import {parseLanguage} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import lande from 'lande'
|
import lande from 'lande'
|
||||||
|
|
||||||
import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers'
|
import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers'
|
||||||
import {
|
import {useLanguagePrefs} from '#/state/preferences/languages'
|
||||||
toPostLanguages,
|
|
||||||
useLanguagePrefs,
|
|
||||||
useLanguagePrefsApi,
|
|
||||||
} from '#/state/preferences/languages'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
|
import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
|
||||||
@@ -22,28 +18,42 @@ const cancelIdle = globalThis.cancelIdleCallback || clearTimeout
|
|||||||
|
|
||||||
export function SuggestedLanguage({
|
export function SuggestedLanguage({
|
||||||
text,
|
text,
|
||||||
replyToLanguage: replyToLanguageProp,
|
replyToLanguages: replyToLanguagesProp,
|
||||||
|
currentLanguages,
|
||||||
|
onAcceptSuggestedLanguage,
|
||||||
}: {
|
}: {
|
||||||
text: string
|
text: string
|
||||||
replyToLanguage?: string
|
/**
|
||||||
|
* All languages associated with the post being replied to.
|
||||||
|
*/
|
||||||
|
replyToLanguages: string[]
|
||||||
|
/**
|
||||||
|
* All languages currently selected for the post being composed.
|
||||||
|
*/
|
||||||
|
currentLanguages: string[]
|
||||||
|
/**
|
||||||
|
* Called when the user accepts a suggested language. We only pass a single
|
||||||
|
* language here. If the post being replied to has multiple languages, we
|
||||||
|
* only suggest the first one.
|
||||||
|
*/
|
||||||
|
onAcceptSuggestedLanguage: (language: string | null) => void
|
||||||
}) {
|
}) {
|
||||||
const replyToLanguage = cleanUpLanguage(replyToLanguageProp)
|
const langPrefs = useLanguagePrefs()
|
||||||
|
const replyToLanguages = replyToLanguagesProp
|
||||||
|
.map(lang => cleanUpLanguage(lang))
|
||||||
|
.filter(Boolean) as string[]
|
||||||
|
const [hasInteracted, setHasInteracted] = useState(false)
|
||||||
const [suggestedLanguage, setSuggestedLanguage] = useState<
|
const [suggestedLanguage, setSuggestedLanguage] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>(text.length === 0 ? replyToLanguage : undefined)
|
>(undefined)
|
||||||
const langPrefs = useLanguagePrefs()
|
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// For replies, suggest the language of the post being replied to if no text
|
if (text.length > 0 && !hasInteracted) {
|
||||||
// has been typed yet
|
setHasInteracted(true)
|
||||||
if (replyToLanguage && text.length === 0) {
|
|
||||||
setSuggestedLanguage(replyToLanguage)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}, [text, hasInteracted])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const textTrimmed = text.trim()
|
const textTrimmed = text.trim()
|
||||||
|
|
||||||
// Don't run the language model on small posts, the results are likely
|
// Don't run the language model on small posts, the results are likely
|
||||||
@@ -58,55 +68,122 @@ export function SuggestedLanguage({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => cancelIdle(idle)
|
return () => cancelIdle(idle)
|
||||||
}, [text, replyToLanguage])
|
}, [text])
|
||||||
|
|
||||||
if (
|
/*
|
||||||
suggestedLanguage &&
|
* We've detected a language, and the user hasn't already selected it.
|
||||||
!toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage)
|
*/
|
||||||
) {
|
const hasLanguageSuggestion =
|
||||||
|
suggestedLanguage && !currentLanguages.includes(suggestedLanguage)
|
||||||
|
/*
|
||||||
|
* We have not detected a different language, and the user is not already
|
||||||
|
* using or has not already selected one of the languages of the post they
|
||||||
|
* are replying to.
|
||||||
|
*/
|
||||||
|
const hasSuggestedReplyLanguage =
|
||||||
|
!hasInteracted &&
|
||||||
|
!suggestedLanguage &&
|
||||||
|
replyToLanguages.length &&
|
||||||
|
!replyToLanguages.some(l => currentLanguages.includes(l))
|
||||||
|
|
||||||
|
if (hasLanguageSuggestion) {
|
||||||
const suggestedLanguageName = codeToLanguageName(
|
const suggestedLanguageName = codeToLanguageName(
|
||||||
suggestedLanguage,
|
suggestedLanguage,
|
||||||
langPrefs.appLanguage,
|
langPrefs.appLanguage,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LanguageSuggestionButton
|
||||||
|
label={
|
||||||
|
<RNText>
|
||||||
|
<Trans>
|
||||||
|
Are you writing in{' '}
|
||||||
|
<Text style={[a.font_bold]}>{suggestedLanguageName}</Text>?
|
||||||
|
</Trans>
|
||||||
|
</RNText>
|
||||||
|
}
|
||||||
|
value={suggestedLanguage}
|
||||||
|
onAccept={onAcceptSuggestedLanguage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (hasSuggestedReplyLanguage) {
|
||||||
|
const suggestedLanguageName = codeToLanguageName(
|
||||||
|
replyToLanguages[0],
|
||||||
|
langPrefs.appLanguage,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LanguageSuggestionButton
|
||||||
|
label={
|
||||||
|
<RNText>
|
||||||
|
<Trans>
|
||||||
|
The post you're replying to was marked as being written in{' '}
|
||||||
|
{suggestedLanguageName} by its author. Would you like to reply in{' '}
|
||||||
|
<Text style={[a.font_bold]}>{suggestedLanguageName}</Text>?
|
||||||
|
</Trans>
|
||||||
|
</RNText>
|
||||||
|
}
|
||||||
|
value={replyToLanguages[0]}
|
||||||
|
onAccept={onAcceptSuggestedLanguage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function LanguageSuggestionButton({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onAccept,
|
||||||
|
}: {
|
||||||
|
label: React.ReactNode
|
||||||
|
value: string
|
||||||
|
onAccept: (language: string | null) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.px_lg, a.py_sm]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
t.atoms.border_contrast_low,
|
a.gap_md,
|
||||||
a.gap_sm,
|
|
||||||
a.border,
|
a.border,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
a.px_lg,
|
a.p_md,
|
||||||
a.py_md,
|
a.pl_lg,
|
||||||
a.mx_md,
|
|
||||||
a.my_sm,
|
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
<EarthIcon />
|
<EarthIcon />
|
||||||
<Text style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Trans>
|
<Text
|
||||||
Are you writing in{' '}
|
style={[
|
||||||
<Text style={[a.font_semi_bold]}>{suggestedLanguageName}</Text>?
|
a.flex_1,
|
||||||
</Trans>
|
a.leading_snug,
|
||||||
|
{
|
||||||
|
maxWidth: 400,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
color="secondary"
|
||||||
onPress={() => setLangPrefs.setPostLanguage(suggestedLanguage)}
|
onPress={() => onAccept(value)}
|
||||||
label={_(msg`Change post language to ${suggestedLanguageName}`)}>
|
label={_(msg`Accept this language suggestion`)}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Yes</Trans>
|
<Trans>Yes</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user