From c6768452fb247f58a4ecef1ac4611dbe17ba8112 Mon Sep 17 00:00:00 2001 From: Elijah Seed-Arita Date: Tue, 23 Sep 2025 07:49:44 -0700 Subject: [PATCH 1/8] 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 --- src/state/persisted/schema.ts | 2 +- src/state/preferences/languages.tsx | 4 +++ src/view/com/composer/Composer.tsx | 33 ++++++++++++++--- .../select-language/PostLanguageSelect.tsx | 36 ++++++++++++++----- .../PostLanguageSelectDialog.tsx | 14 ++++++-- .../select-language/SuggestedLanguage.tsx | 18 ++++------ 6 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 11204f30907..1381f66b5d4 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -71,7 +71,7 @@ const schema = z.object({ contentLanguages: z.array(z.string()), /** * 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. */ diff --git a/src/state/preferences/languages.tsx b/src/state/preferences/languages.tsx index 14ba62dba98..5d4336814c3 100644 --- a/src/state/preferences/languages.tsx +++ b/src/state/preferences/languages.tsx @@ -156,6 +156,10 @@ export function toPostLanguages(postLanguage: string): string[] { return postLanguage.split(',').filter(Boolean) } +export function fromPostLanguages(languages: string[]): string { + return languages.filter(Boolean).join(',') +} + export function hasPostLanguage(postLanguage: string, code2: string): boolean { return toPostLanguages(postLanguage).includes(code2) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index a4e0bd06fbd..82b0dc6e29b 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -88,6 +88,7 @@ import { import {useModalControls} from '#/state/modals' import {useRequireAltTextEnabled} from '#/state/preferences' import { + fromPostLanguages, toPostLanguages, useLanguagePrefs, useLanguagePrefsApi, @@ -196,6 +197,25 @@ export const ComposePost = ({ const [isPublishing, setIsPublishing] = useState(false) const [publishingStage, setPublishingStage] = useState('') const [error, setError] = useState('') + const [acceptedLanguageSuggestion, setAcceptedLanguageSuggestion] = useState< + string | null + >(null) + + // NOTE(@elijaharita): if a temporary language suggestion has been accepted, + // show that as the post language instead of the one from langPrefs. + const currentLanguages = useMemo( + () => + acceptedLanguageSuggestion + ? [acceptedLanguageSuggestion] + : toPostLanguages(langPrefs.postLanguage), + [acceptedLanguageSuggestion, langPrefs.postLanguage], + ) + + // This effect clears the temporary language suggestion if the post language + // is manually changed, so the user doesn't get stuck with the suggestion. + useEffect(() => { + setAcceptedLanguageSuggestion(null) + }, [langPrefs.postLanguage]) const [composerState, composerDispatch] = useReducer( composerReducer, @@ -414,7 +434,7 @@ export const ComposePost = ({ thread, replyTo: replyTo?.uri, onStateChange: setPublishingStage, - langs: toPostLanguages(langPrefs.postLanguage), + langs: currentLanguages, }) ).uris[0] @@ -490,7 +510,7 @@ export const ComposePost = ({ isPartOfThread: thread.posts.length > 1, hasLink: !!post.embed.link, hasQuote: !!post.embed.quote, - langs: langPrefs.postLanguage, + langs: fromPostLanguages(currentLanguages), logContext: 'Composer', }) index++ @@ -557,7 +577,7 @@ export const ComposePost = ({ thread, canPost, isPublishing, - langPrefs.postLanguage, + currentLanguages, onClose, onPost, onPostSuccess, @@ -656,6 +676,8 @@ export const ComposePost = ({ text={activePost.richtext.text} // NOTE(@elijaharita): currently just choosing the first language if any exists replyToLanguage={replyTo?.langs?.[0]} + currentLanguages={currentLanguages} + onChange={setAcceptedLanguageSuggestion} /> ) @@ -1289,6 +1312,7 @@ function ComposerFooter({ onEmojiButtonPress, onSelectVideo, onAddPost, + currentLanguages, }: { post: PostDraft dispatch: (action: PostAction) => void @@ -1297,6 +1321,7 @@ function ComposerFooter({ onError: (error: string) => void onSelectVideo: (postId: string, asset: ImagePickerAsset) => void onAddPost: () => void + currentLanguages: string[] }) { const t = useTheme() const {_} = useLingui() @@ -1450,7 +1475,7 @@ function ComposerFooter({ )} - + - + ) } @@ -43,7 +53,9 @@ export function PostLanguageSelect() { <> - {({props}) => } + {({props}) => ( + + )} @@ -59,7 +71,7 @@ export function PostLanguageSelect() { onPress={() => setLangPrefs.setPostLanguage(historyItem)}> {langName} ) @@ -77,17 +89,25 @@ export function PostLanguageSelect() { - + ) } -function LanguageBtn(props: Omit) { +function LanguageBtn( + props: Omit & { + currentLanguages?: string[] + }, +) { const {_} = useLingui() const langPrefs = useLanguagePrefs() const t = useTheme() const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) + const currentLanguages = props.currentLanguages ?? postLanguagesPref return ( - ) - } else { - return null - } + + ) } /** From ec7682f3f0d63d40e5a21ac8dbfc755196f06b56 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 23 Sep 2025 10:39:19 -0500 Subject: [PATCH 4/8] Handle user override more explicitly --- src/view/com/composer/Composer.tsx | 43 +++++++++++++++---- .../select-language/PostLanguageSelect.tsx | 7 ++- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e3bda277ae7..b5951408787 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -197,12 +197,28 @@ export const ComposePost = ({ const [isPublishing, setIsPublishing] = useState(false) const [publishingStage, setPublishingStage] = useState('') const [error, setError] = useState('') + + /** + * A temporarly 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) - // NOTE(@elijaharita): if a temporary language suggestion has been accepted, - // show that as the post language instead of the one from langPrefs. + /** + * The language of the post being replied to, if any. We just use the first + * language available, for now. + */ + const [replyToLanguage, setReplyToLanguage] = useState( + replyTo?.langs?.[0], + ) + + /** + * The currently selected languages of the post. Prefer local temporary + * language suggestion over global lang prefs, if available. + */ const currentLanguages = useMemo( () => acceptedLanguageSuggestion @@ -211,11 +227,15 @@ export const ComposePost = ({ [acceptedLanguageSuggestion, langPrefs.postLanguage], ) - // This effect clears the temporary language suggestion if the post language - // is manually changed, so the user doesn't get stuck with the suggestion. - useEffect(() => { + /** + * 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) - }, [langPrefs.postLanguage]) + setReplyToLanguage(undefined) + } const [composerState, composerDispatch] = useReducer( composerReducer, @@ -674,8 +694,7 @@ export const ComposePost = ({ <> @@ -701,6 +720,7 @@ export const ComposePost = ({ }) }} currentLanguages={currentLanguages} + onSelectLanguage={onSelectLanguage} /> ) @@ -1313,6 +1333,7 @@ function ComposerFooter({ onSelectVideo, onAddPost, currentLanguages, + onSelectLanguage, }: { post: PostDraft dispatch: (action: PostAction) => void @@ -1322,6 +1343,7 @@ function ComposerFooter({ onSelectVideo: (postId: string, asset: ImagePickerAsset) => void onAddPost: () => void currentLanguages: string[] + onSelectLanguage?: (language: string) => void }) { const t = useTheme() const {_} = useLingui() @@ -1475,7 +1497,10 @@ function ComposerFooter({ )} - + void }) { const {_} = useLingui() const langPrefs = useLanguagePrefs() @@ -68,7 +70,10 @@ export function PostLanguageSelect({ setLangPrefs.setPostLanguage(historyItem)}> + onPress={() => { + setLangPrefs.setPostLanguage(historyItem) + onSelectLanguage?.(historyItem) + }}> {langName} Date: Tue, 23 Sep 2025 10:46:49 -0500 Subject: [PATCH 5/8] Drill in onSelectLanguage callback into dialog too --- .../select-language/PostLanguageSelect.tsx | 1 + .../PostLanguageSelectDialog.tsx | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/view/com/composer/select-language/PostLanguageSelect.tsx b/src/view/com/composer/select-language/PostLanguageSelect.tsx index 0b194221d66..2bc425e8405 100644 --- a/src/view/com/composer/select-language/PostLanguageSelect.tsx +++ b/src/view/com/composer/select-language/PostLanguageSelect.tsx @@ -97,6 +97,7 @@ export function PostLanguageSelect({ ) diff --git a/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx b/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx index dff719f3875..4995d4d353e 100644 --- a/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx +++ b/src/view/com/composer/select-language/PostLanguageSelectDialog.tsx @@ -23,13 +23,17 @@ import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Ti import {Text} from '#/components/Typography' export function PostLanguageSelectDialog({ - /** Optionally can be passed to show different values than what is saved in - * langPrefs. */ - currentLanguages, control, + /** + * Optionally can be passed to show different values than what is saved in + * langPrefs. + */ + currentLanguages, + onSelectLanguage, }: { - currentLanguages?: string[] control: Dialog.DialogControlProps + currentLanguages?: string[] + onSelectLanguage?: (language: string) => void }) { const {height} = useWindowDimensions() const insets = useSafeAreaInsets() @@ -45,13 +49,22 @@ export function PostLanguageSelectDialog({ nativeOptions={{minHeight: height - insets.top}}> - + ) } -export function DialogInner({currentLanguages}: {currentLanguages?: string[]}) { +export function DialogInner({ + currentLanguages, + onSelectLanguage, +}: { + currentLanguages?: string[] + onSelectLanguage?: (language: string) => void +}) { const control = Dialog.useDialogContext() const [headerHeight, setHeaderHeight] = useState(0) @@ -87,6 +100,7 @@ export function DialogInner({currentLanguages}: {currentLanguages?: string[]}) { langsString = langPrefs.primaryLanguage } setLangPrefs.setPostLanguage(langsString) + onSelectLanguage?.(langsString) }) } From ebf47154a2837736a0b07e8362815c0b86ae5e43 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 24 Sep 2025 11:51:22 -0500 Subject: [PATCH 6/8] Fix typo Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- src/view/com/composer/Composer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index b5951408787..c484dd1188a 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -199,7 +199,7 @@ export const ComposePost = ({ const [error, setError] = useState('') /** - * A temporarly local reference to a language suggestion that the user has + * 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. */ From e8ccd57cd66801836413c8aa283d54e9a44b7f92 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 1 Oct 2025 17:50:36 -0500 Subject: [PATCH 7/8] Make text crystal clear --- src/view/com/composer/select-language/SuggestedLanguage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx index eb4afbc8e80..f5619cee994 100644 --- a/src/view/com/composer/select-language/SuggestedLanguage.tsx +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -103,8 +103,8 @@ export function SuggestedLanguage({ label={ - The post you're replying to is written in {suggestedLanguageName}. - Would you like to reply in{' '} + The post you're replying to was marked as being written in{' '} + {suggestedLanguageName} by its author. Would you like to reply in{' '} {suggestedLanguageName}? From af177b0dad0a738baf88aa3eec90e709ea0aa0e6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 1 Oct 2025 18:02:39 -0500 Subject: [PATCH 8/8] Handle multiple languages --- src/view/com/composer/Composer.tsx | 11 +++---- .../select-language/SuggestedLanguage.tsx | 32 +++++++++++++------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 3139eb98f82..8cbb2d37baf 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -208,11 +208,10 @@ export const ComposePost = ({ >(null) /** - * The language of the post being replied to, if any. We just use the first - * language available, for now. + * The language(s) of the post being replied to. */ - const [replyToLanguage, setReplyToLanguage] = useState( - replyTo?.langs?.[0], + const [replyToLanguages, setReplyToLanguages] = useState( + replyTo?.langs || [], ) /** @@ -234,7 +233,7 @@ export const ComposePost = ({ */ const onSelectLanguage = () => { setAcceptedLanguageSuggestion(null) - setReplyToLanguage(undefined) + setReplyToLanguages([]) } const [composerState, composerDispatch] = useReducer( @@ -694,7 +693,7 @@ export const ComposePost = ({ <> diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx index f5619cee994..82e8ebd30bf 100644 --- a/src/view/com/composer/select-language/SuggestedLanguage.tsx +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -18,17 +18,30 @@ const cancelIdle = globalThis.cancelIdleCallback || clearTimeout export function SuggestedLanguage({ text, - replyToLanguage: replyToLanguageProp, + replyToLanguages: replyToLanguagesProp, currentLanguages, onAcceptSuggestedLanguage, }: { 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 langPrefs = useLanguagePrefs() - const replyToLanguage = cleanUpLanguage(replyToLanguageProp) + const replyToLanguages = replyToLanguagesProp + .map(lang => cleanUpLanguage(lang)) + .filter(Boolean) as string[] const [hasInteracted, setHasInteracted] = useState(false) const [suggestedLanguage, setSuggestedLanguage] = useState< string | undefined @@ -63,14 +76,15 @@ export function SuggestedLanguage({ const hasLanguageSuggestion = suggestedLanguage && !currentLanguages.includes(suggestedLanguage) /* - * We have not detected a different language, and the user has not already - * selected the language of the post they are replying to. + * 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 && - replyToLanguage && - !currentLanguages.includes(replyToLanguage) + replyToLanguages.length && + !replyToLanguages.some(l => currentLanguages.includes(l)) if (hasLanguageSuggestion) { const suggestedLanguageName = codeToLanguageName( @@ -94,7 +108,7 @@ export function SuggestedLanguage({ ) } else if (hasSuggestedReplyLanguage) { const suggestedLanguageName = codeToLanguageName( - replyToLanguage, + replyToLanguages[0], langPrefs.appLanguage, ) @@ -109,7 +123,7 @@ export function SuggestedLanguage({ } - value={replyToLanguage} + value={replyToLanguages[0]} onAccept={onAcceptSuggestedLanguage} /> )