diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-05-29 07:08:46 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-28 21:08:46 -0700 |
commit | b59c8e22af0d92988d2145ea28299230f3b71180 (patch) | |
tree | 3bfc697a5e709a29ee4f71fc538e9104855f9d07 | |
parent | a60f9933d8c5734391b9f5b14c1bdb0d17ac0468 (diff) | |
download | voidsky-b59c8e22af0d92988d2145ea28299230f3b71180.tar.zst |
Native translation expo module (#4098)
* translation expo module * add `onClose` and `onReplacementAction` * rm onReplacementAction * make all props published * make translation api available globally w/o wrapper (#4110) * conditionally import the translation module * only use native translation if language is probably supported * open native translation via dropdown menu --------- Co-authored-by: Hailey <me@haileyok.com> Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
14 files changed, 232 insertions, 8 deletions
diff --git a/modules/expo-bluesky-translate/expo-module.config.json b/modules/expo-bluesky-translate/expo-module.config.json new file mode 100644 index 000000000..28c5dd878 --- /dev/null +++ b/modules/expo-bluesky-translate/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["ios"], + "ios": { + "modules": ["ExpoBlueskyTranslateModule"] + } +} diff --git a/modules/expo-bluesky-translate/index.ts b/modules/expo-bluesky-translate/index.ts new file mode 100644 index 000000000..f3e1d3b11 --- /dev/null +++ b/modules/expo-bluesky-translate/index.ts @@ -0,0 +1,6 @@ +export { + isAvailable, + isLanguageSupported, + NativeTranslationModule, + NativeTranslationView, +} from './src/ExpoBlueskyTranslateView' diff --git a/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift b/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift new file mode 100644 index 000000000..c8ca3e027 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift @@ -0,0 +1,20 @@ +import ExpoModulesCore +import SwiftUI + +// Thanks to Andrew Levy for this code snippet +// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift + +extension ExpoView { + func setupHostingController(_ hostingController: UIHostingController<some View>) { + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = .clear + + addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: self.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor), + hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor), + hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor), + ]) + } +} diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec new file mode 100644 index 000000000..45f86a605 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskyTranslate' + s.version = '1.0.0' + s.summary = 'Uses SwiftUI translation to translate text.' + s.description = 'Uses SwiftUI translation to translate text.' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { :ios => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift new file mode 100644 index 000000000..afa813722 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift @@ -0,0 +1,18 @@ +import ExpoModulesCore +import Foundation +import SwiftUI + +public class ExpoBlueskyTranslateModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyTranslate") + + AsyncFunction("presentAsync") { (text: String) in + DispatchQueue.main.async { [weak state = TranslateViewState.shared] in + state?.isPresented = true + state?.text = text + } + } + + View(ExpoBlueskyTranslateView.self) {} + } +} diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift new file mode 100644 index 000000000..ca6e3be69 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift @@ -0,0 +1,22 @@ +import ExpoModulesCore +import Foundation +import SwiftUI + +class TranslateViewState: ObservableObject { + static var shared = TranslateViewState() + + @Published var isPresented = false + @Published var text = "" +} + +class ExpoBlueskyTranslateView: ExpoView { + required init(appContext: AppContext? = nil) { + if #available(iOS 14.0, *) { + let hostingController = UIHostingController(rootView: TranslateView()) + super.init(appContext: appContext) + setupHostingController(hostingController) + } else { + super.init(appContext: appContext) + } + } +} diff --git a/modules/expo-bluesky-translate/ios/TranslateView.swift b/modules/expo-bluesky-translate/ios/TranslateView.swift new file mode 100644 index 000000000..e2886dc84 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/TranslateView.swift @@ -0,0 +1,31 @@ +import SwiftUI +// conditionally import the Translation module +#if canImport(Translation) +import Translation +#endif + +struct TranslateView: View { + @ObservedObject var state = TranslateViewState.shared + + var body: some View { + if #available(iOS 17.4, *) { + VStack { + UIViewRepresentableWrapper(view: UIView(frame: .zero)) + } + .translationPresentation( + isPresented: $state.isPresented, + text: state.text + ) + } + } +} + +struct UIViewRepresentableWrapper: UIViewRepresentable { + let view: UIView + + func makeUIView(context: Context) -> UIView { + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts new file mode 100644 index 000000000..a01d4d479 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts @@ -0,0 +1,3 @@ +export type ExpoBlueskyTranslateModule = { + presentAsync: (text: string) => Promise<void> +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx new file mode 100644 index 000000000..daddfa028 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import {Platform} from 'react-native' +import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' + +import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types' + +export const NativeTranslationModule = + requireNativeModule<ExpoBlueskyTranslateModule>('ExpoBlueskyTranslate') + +const NativeView: React.ComponentType = requireNativeViewManager( + 'ExpoBlueskyTranslate', +) + +export function NativeTranslationView() { + return <NativeView /> +} + +export const isAvailable = Number(Platform.Version) >= 17.4 + +// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages +const SUPPORTED_LANGUAGES = [ + 'ar', + 'zh', + 'zh', + 'nl', + 'en', + 'en', + 'fr', + 'de', + 'id', + 'it', + 'ja', + 'ko', + 'pl', + 'pt', + 'ru', + 'es', + 'th', + 'tr', + 'uk', + 'vi', +] + +export function isLanguageSupported(lang?: string) { + // If the language is not provided, we assume it is supported + if (!lang) return true + return SUPPORTED_LANGUAGES.includes(lang) +} diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx new file mode 100644 index 000000000..16ff9d600 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx @@ -0,0 +1,13 @@ +export const NativeTranslationModule = { + presentAsync: async (_: string) => {}, +} + +export function NativeTranslationView() { + return null +} + +export const isAvailable = false + +export function isLanguageSupported(_lang?: string) { + return false +} diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx index 93e69333f..0f5d01c13 100644 --- a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx +++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx @@ -1,5 +1,7 @@ import React from 'react' + import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' + export function ExpoScrollForwarderView({ children, }: React.PropsWithChildren<ExpoScrollForwarderViewProps>) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index c44875b37..548b73af6 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -30,6 +30,11 @@ import {useSession} from 'state/session' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {atoms as a} from '#/alf' import {RichText} from '#/components/RichText' +import { + isAvailable as isNativeTranslationAvailable, + isLanguageSupported, + NativeTranslationModule, +} from '../../../../modules/expo-bluesky-translate' import {ContentHider} from '../../../components/moderation/ContentHider' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' @@ -317,6 +322,7 @@ let PostThreadItemLoaded = ({ </ContentHider> <ExpandedPostDetails post={post} + record={record} translatorUrl={translatorUrl} needsTranslation={needsTranslation} /> @@ -620,26 +626,39 @@ function PostOuterWrapper({ function ExpandedPostDetails({ post, + record, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView + record?: AppBskyFeedPost.Record needsTranslation: boolean translatorUrl: string }) { const pal = usePalette('default') const {_} = useLingui() const openLink = useOpenLink() - const onTranslatePress = React.useCallback( - () => openLink(translatorUrl), - [openLink, translatorUrl], - ) + + const text = record?.text || '' + + const onTranslatePress = React.useCallback(() => { + if ( + isNativeTranslationAvailable && + isLanguageSupported(record?.langs?.at(0)) + ) { + NativeTranslationModule.presentAsync(text) + } else { + openLink(translatorUrl) + } + }, [openLink, text, translatorUrl, record]) + return ( <View style={[s.flexRow, s.mt2, s.mb10]}> <Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text> {needsTranslation && ( <> <Text style={pal.textLight}> · </Text> + <Text style={pal.link} title={_(msg`Translate`)} diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 7a62ce7cb..50677ee8a 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -50,6 +50,11 @@ import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/War import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' +import { + isAvailable as isNativeTranslationAvailable, + isLanguageSupported, + NativeTranslationModule, +} from '../../../../../modules/expo-bluesky-translate' import {EventStopper} from '../EventStopper' import * as Toast from '../Toast' @@ -172,9 +177,17 @@ let PostDropdownBtn = ({ Toast.show(_(msg`Copied to clipboard`)) }, [_, richText]) - const onOpenTranslate = React.useCallback(() => { - openLink(translatorUrl) - }, [openLink, translatorUrl]) + const onPressTranslate = React.useCallback(() => { + if ( + isNativeTranslationAvailable && + isLanguageSupported(record?.langs?.at(0)) + ) { + const text = richTextToString(richText, true) + NativeTranslationModule.presentAsync(text) + } else { + openLink(translatorUrl) + } + }, [openLink, record?.langs, richText, translatorUrl]) const onHidePost = React.useCallback(() => { hidePost({uri: postUri}) @@ -246,7 +259,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownTranslateBtn" label={_(msg`Translate`)} - onPress={onOpenTranslate}> + onPress={onPressTranslate}> <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> <Menu.ItemIcon icon={Translate} position="right" /> </Menu.Item> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 7d080e57b..317ac0bde 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -33,6 +33,7 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' import {Outlet as PortalOutlet} from '#/components/Portal' +import {NativeTranslationView} from '../../../modules/expo-bluesky-translate' import {RoutesContainer, TabsNavigator} from '../../Navigation' import {Composer} from './Composer' import {DrawerContent} from './Drawer' @@ -93,6 +94,7 @@ function ShellInner() { </Drawer> </ErrorBoundary> </Animated.View> + <NativeTranslationView /> <Composer winHeight={winDim.height} /> <ModalsContainer /> <MutedWordsDialog /> |