From 51a243eeea8e2746f2820baa8923ad202aa48ab4 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:02:57 +0800 Subject: [PATCH] fix: toggle translation settings, close #4187 --- .../entry-column/Items/picture-masonry.tsx | 7 +++++- .../modules/entry-column/Items/video-item.tsx | 10 +++++--- .../src/modules/entry-column/item.tsx | 10 +++++--- .../entry-content/components/EntryTitle.tsx | 9 ++++++-- .../src/modules/entry-content/hooks.tsx | 13 +++++++---- .../native/webview/EntryContentWebView.tsx | 2 +- .../modules/entry-content/EntryGridFooter.tsx | 9 ++++++-- .../src/modules/entry-content/EntryTitle.tsx | 15 ++++++++---- .../entry-list/EntryListContentArticle.tsx | 2 +- .../entry-list/EntryListContentPicture.tsx | 2 +- .../entry-list/EntryListContentSocial.tsx | 2 +- .../entry-list/EntryListContentVideo.tsx | 2 +- .../entry-list/templates/EntryNormalItem.tsx | 9 ++++++-- .../entry-list/templates/EntrySocialItem.tsx | 7 +++++- .../entry-list/templates/EntryTranslation.tsx | 9 ++------ .../entries/[entryId]/EntryDetailScreen.tsx | 16 ++++++------- .../internal/store/src/translation/hooks.ts | 23 ++++++++++++++----- 17 files changed, 99 insertions(+), 48 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx index edf4677d4..213b3224a 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx @@ -283,8 +283,13 @@ const MasonryRender: React.ComponentType< }> > = ({ data, index }) => { const firstScreenReady = use(FirstScreenReadyContext) + const enableTranslation = useGeneralSettingKey("translation") const actionLanguage = useActionLanguage() - const translation = useEntryTranslation(data.entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId: data.entryId, + language: actionLanguage, + setting: enableTranslation, + }) if (data.entryId.startsWith("placeholder")) { return diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/Items/video-item.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/Items/video-item.tsx index afe45c5ed..c520ad416 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/Items/video-item.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/Items/video-item.tsx @@ -173,13 +173,17 @@ const PreviewVideoModalContent: ModalContentComponent<{ }> = ({ dismiss, src, entryId }) => { const entry = useEntry(entryId, (state) => ({ content: state.content })) - const actionLanguage = useActionLanguage() const enableTranslation = useGeneralSettingKey("translation") - const translation = useEntryTranslation(entryId, actionLanguage) + const actionLanguage = useActionLanguage() + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) usePrefetchEntryTranslation({ entryIds: [entryId], checkLanguage, - translation: enableTranslation, + setting: enableTranslation, language: actionLanguage, withContent: true, }) diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx index d813c8e42..8c45f2359 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx @@ -22,13 +22,17 @@ const EntryItemImpl = memo(function EntryItemImpl({ entryId: string view: FeedViewType }) { - const actionLanguage = useActionLanguage() const enableTranslation = useGeneralSettingKey("translation") - const translation = useEntryTranslation(entryId, actionLanguage) + const actionLanguage = useActionLanguage() + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) usePrefetchEntryTranslation({ entryIds: [entryId], checkLanguage, - translation: enableTranslation, + setting: enableTranslation, language: actionLanguage, withContent: view === FeedViewType.SocialMedia, }) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx index e5ddcac10..26200ce19 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx @@ -6,6 +6,7 @@ import { useWhoami } from "@follow/store/user/hooks" import { formatEstimatedMins, formatTimeToSeconds } from "@follow/utils" import { titleCase } from "title-case" +import { useShowAITranslation } from "~/atoms/ai-translation" import { useActionLanguage } from "~/atoms/settings/general" import { useUISettingKey } from "~/atoms/settings/ui" import { RelativeTime } from "~/components/ui/datetime" @@ -57,9 +58,13 @@ export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => { const data = useEntryReadHistory(entryId) const entryHistory = data?.entryReadHistories const populatedFullHref = useFeedSafeUrl(entryId) + const enableTranslation = useShowAITranslation() const actionLanguage = useActionLanguage() - - const translation = useEntryTranslation(entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) const dateFormat = useUISettingKey("dateFormat") diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx index 5b898ceac..d5b5946d0 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/hooks.tsx @@ -5,8 +5,9 @@ import { createElement, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" +import { useShowAITranslation } from "~/atoms/ai-translation" import { useEntryIsInReadability, useEntryIsInReadabilitySuccess } from "~/atoms/readability" -import { useActionLanguage, useGeneralSettingKey } from "~/atoms/settings/general" +import { useActionLanguage } from "~/atoms/settings/general" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { checkLanguage } from "~/lib/translate" @@ -46,13 +47,17 @@ export const useEntryContent = (entryId: string) => { const isInReadabilityMode = useEntryIsInReadability(entryId) const isReadabilitySuccess = useEntryIsInReadabilitySuccess(entryId) + const enableTranslation = useShowAITranslation() const actionLanguage = useActionLanguage() - const enableTranslation = useGeneralSettingKey("translation") - const contentTranslated = useEntryTranslation(entryId, actionLanguage) + const contentTranslated = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) usePrefetchEntryTranslation({ entryIds: [entryId], checkLanguage, - translation: enableTranslation, + setting: enableTranslation, language: actionLanguage, withContent: true, target: isReadabilitySuccess ? "readabilityContent" : "content", diff --git a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx index 519a18fb5..aa9525c2e 100644 --- a/apps/mobile/src/components/native/webview/EntryContentWebView.tsx +++ b/apps/mobile/src/components/native/webview/EntryContentWebView.tsx @@ -60,7 +60,7 @@ export function EntryContentWebView(props: EntryContentWebViewProps) { const { entryId, noMedia, showReadability, showTranslation } = props const entry = useEntry(entryId, (state) => state) const language = useActionLanguage() - const translation = useEntryTranslation(entryId, language) + const translation = useEntryTranslation({ entryId, language, setting: showTranslation ?? false }) const [mode, setMode] = React.useState<"normal" | "debug">("normal") diff --git a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx index 4e8c57950..ebda3b740 100644 --- a/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx +++ b/apps/mobile/src/modules/entry-content/EntryGridFooter.tsx @@ -5,7 +5,7 @@ import { useEntryTranslation } from "@follow/store/translation/hooks" import { cn } from "@follow/utils" import { Text, View } from "react-native" -import { useActionLanguage } from "@/src/atoms/settings/general" +import { useActionLanguage, useGeneralSettingKey } from "@/src/atoms/settings/general" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" @@ -27,8 +27,13 @@ export const EntryGridFooter = ({ read: state.read, translation: state.settings?.translation, })) + const enableTranslation = useGeneralSettingKey("translation") const actionLanguage = useActionLanguage() - const translation = useEntryTranslation(entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) const feed = useFeedById(entry?.feedId || "") if (!entry) return null diff --git a/apps/mobile/src/modules/entry-content/EntryTitle.tsx b/apps/mobile/src/modules/entry-content/EntryTitle.tsx index 319bb7dc8..33e68f67a 100644 --- a/apps/mobile/src/modules/entry-content/EntryTitle.tsx +++ b/apps/mobile/src/modules/entry-content/EntryTitle.tsx @@ -1,20 +1,27 @@ import { useEntry } from "@follow/store/entry/hooks" import { useFeedById } from "@follow/store/feed/hooks" import { useEntryTranslation } from "@follow/store/translation/hooks" -import { useSetAtom } from "jotai" +import { useAtomValue, useSetAtom } from "jotai" import { use } from "react" import { Text, View } from "react-native" -import { useActionLanguage } from "@/src/atoms/settings/general" +import { useActionLanguage, useGeneralSettingKey } from "@/src/atoms/settings/general" import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" -import { EntryContentContext } from "@/src/modules/entry-content/ctx" +import { EntryContentContext, useEntryContentContext } from "@/src/modules/entry-content/ctx" import { EntryTranslation } from "../entry-list/templates/EntryTranslation" export const EntryTitle = ({ title, entryId }: { title: string; entryId: string }) => { + const { showAITranslationAtom } = useEntryContentContext() + const showTranslationOnce = useAtomValue(showAITranslationAtom) + const enableTranslation = useGeneralSettingKey("translation") || showTranslationOnce const actionLanguage = useActionLanguage() - const translation = useEntryTranslation(entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) const { titleHeightAtom } = use(EntryContentContext) const setTitleHeight = useSetAtom(titleHeightAtom) diff --git a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx index 55161452f..d5b6eda81 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx @@ -62,7 +62,7 @@ export const EntryListContentArticle = ({ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [], language: actionLanguage, - translation, + setting: translation, checkLanguage, }) diff --git a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx index 32aafd64d..9f44768f0 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx @@ -40,7 +40,7 @@ export const EntryListContentPicture = ({ entryIds: active ? viewableItems.map((item) => item.key) : [], language: actionLanguage, checkLanguage, - translation, + setting: translation, }) const renderItem = useTypeScriptHappyCallback(({ item }: { item: string }) => { diff --git a/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx b/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx index 6799ba2d3..f71aabe8e 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx @@ -52,7 +52,7 @@ export const EntryListContentSocial = ({ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [], language: actionLanguage, - translation, + setting: translation, checkLanguage, }) diff --git a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx index 18c8b7526..958c4d145 100644 --- a/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx @@ -38,7 +38,7 @@ export const EntryListContentVideo = ({ usePrefetchEntryTranslation({ entryIds: active ? viewableItems.map((item) => item.key) : [], language: actionLanguage, - translation, + setting: translation, checkLanguage, }) diff --git a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx index 5660efc47..601d4c371 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx @@ -11,7 +11,7 @@ import { memo, useCallback, useMemo, useRef, useState } from "react" import type { ImageErrorEventData } from "react-native" import { Text, View } from "react-native" -import { useActionLanguage } from "@/src/atoms/settings/general" +import { useActionLanguage, useGeneralSettingKey } from "@/src/atoms/settings/general" import { useUISettingKey } from "@/src/atoms/settings/ui" import { preloadWebViewEntry } from "@/src/components/native/webview/EntryContentWebView" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" @@ -52,8 +52,13 @@ export const EntryNormalItem = memo( title: state.title, description: state.description, })) + const enableTranslation = useGeneralSettingKey("translation") const actionLanguage = useActionLanguage() - const translation = useEntryTranslation(entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) const from = getInboxFrom(entry) const feed = useFeedById(entry?.feedId as string) const navigation = useNavigation() diff --git a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx index 7a3dbbcf7..83af3a604 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx @@ -44,8 +44,13 @@ export const EntrySocialItem = memo( author: state.author, translation: state.settings?.translation, })) + const enableTranslation = useGeneralSettingKey("translation") const actionLanguage = useActionLanguage() - const translation = useEntryTranslation(entryId, actionLanguage) + const translation = useEntryTranslation({ + entryId, + language: actionLanguage, + setting: enableTranslation, + }) const { openLightbox } = useLightboxControls() const feed = useFeedById(entry?.feedId || "") diff --git a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx index 51b909d7e..6a3440f7b 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryTranslation.tsx @@ -20,7 +20,6 @@ export const EntryTranslation = ({ showTranslation?: boolean bilingual?: boolean } & TextProps) => { - const showTranslationFinal = useGeneralSettingKey("translation") || showTranslation const bilingualFinal = useGeneralSettingKey("translationMode") === "bilingual" || bilingual const nextSource = useMemo(() => { @@ -30,15 +29,11 @@ export const EntryTranslation = ({ return source.trim() }, [source]) const nextTarget = useMemo(() => { - if ( - !target || - !showTranslationFinal || - nextSource.replaceAll(/\s/g, "") === target.replaceAll(/\s/g, "") - ) { + if (!target || nextSource.replaceAll(/\s/g, "") === target.replaceAll(/\s/g, "")) { return "" } return target.trim() - }, [nextSource, target, showTranslationFinal]) + }, [nextSource, target]) if (!bilingualFinal) { return ( diff --git a/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx index 2a753ab81..802ec3c10 100644 --- a/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx +++ b/apps/mobile/src/screens/(stack)/entries/[entryId]/EntryDetailScreen.tsx @@ -130,9 +130,9 @@ export const EntryDetailScreen: NavigationControllerView<{ const EntryContentWebViewWithContext = ({ entryId }: { entryId: string }) => { const { showReadabilityAtom, showAITranslationAtom } = useEntryContentContext() - const showReadability = useAtomValue(showReadabilityAtom) + const showReadabilityOnce = useAtomValue(showReadabilityAtom) const translationSetting = useGeneralSettingKey("translation") - const showTranslation = useAtomValue(showAITranslationAtom) + const showTranslationOnce = useAtomValue(showAITranslationAtom) const actionLanguage = useActionLanguage() const translation = useGeneralSettingKey("translation") @@ -144,10 +144,10 @@ const EntryContentWebViewWithContext = ({ entryId }: { entryId: string }) => { usePrefetchEntryTranslation({ entryIds: [entryId], withContent: true, - target: showReadability && entry?.readabilityContent ? "readabilityContent" : "content", + target: showReadabilityOnce && entry?.readabilityContent ? "readabilityContent" : "content", language: actionLanguage, checkLanguage, - translation, + setting: translation, }) // Auto toggle readability when content is empty @@ -160,16 +160,16 @@ const EntryContentWebViewWithContext = ({ entryId }: { entryId: string }) => { }, [isPending, entry?.content, setShowReadability]) useEffect(() => { - if (showReadability) { + if (showReadabilityOnce) { entrySyncServices.fetchEntryReadabilityContent(entryId) } - }, [showReadability, entryId]) + }, [showReadabilityOnce, entryId]) return ( ) } diff --git a/packages/internal/store/src/translation/hooks.ts b/packages/internal/store/src/translation/hooks.ts index 98a72c2bc..3bd955787 100644 --- a/packages/internal/store/src/translation/hooks.ts +++ b/packages/internal/store/src/translation/hooks.ts @@ -3,7 +3,7 @@ import type { SupportedActionLanguage } from "@follow/shared" import { useQueries } from "@tanstack/react-query" import { useCallback } from "react" -import { useEntryList } from "../entry/hooks" +import { useEntry, useEntryList } from "../entry/hooks" import type { EntryModel } from "../entry/types" import { translationSyncService, useTranslationStore } from "./store" @@ -11,19 +11,19 @@ export const usePrefetchEntryTranslation = ({ entryIds, withContent, target = "content", - translation, + setting, language, checkLanguage, }: { entryIds: string[] withContent?: boolean target?: "content" | "readabilityContent" - translation: boolean + setting: boolean language: SupportedActionLanguage checkLanguage: (params: { content: string; language: SupportedActionLanguage }) => boolean }) => { const entryList = (useEntryList(entryIds)?.filter( - (entry) => entry !== null && (translation || !!entry?.settings?.translation), + (entry) => entry !== null && (setting || !!entry?.settings?.translation), ) || []) as EntryModel[] return useQueries({ @@ -48,13 +48,24 @@ export const usePrefetchEntryTranslation = ({ }) } -export const useEntryTranslation = (entryId: string, language: SupportedLanguages) => { +export const useEntryTranslation = ({ + entryId, + language, + setting, +}: { + entryId: string + language: SupportedLanguages + setting: boolean +}) => { + const actionSetting = useEntry(entryId, (state) => state.settings?.translation) + return useTranslationStore( useCallback( (state) => { + if (!setting && !actionSetting) return return state.data[entryId]?.[language] }, - [entryId, language], + [actionSetting, entryId, language, setting], ), ) }