From 72637779f56233580067ea3ac5b4e4986361482c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 18 May 2024 23:50:05 +0100 Subject: [PATCH] translation expo module --- .../expo-module.config.json | 6 ++++ modules/expo-bluesky-translate/index.ts | 4 +++ .../Common/UIHostingControllerCompat.swift | 20 +++++++++++ .../ios/ExpoBlueskyTranslate.podspec | 21 ++++++++++++ .../ios/ExpoBlueskyTranslateModule.swift | 18 ++++++++++ .../ios/ExpoBlueskyTranslateView.swift | 30 +++++++++++++++++ .../ios/TranslateView.swift | 33 +++++++++++++++++++ .../src/ExpoBlueskyTranslate.types.ts | 7 ++++ .../src/ExpoBlueskyTranslateView.ios.tsx | 14 ++++++++ .../src/ExpoBlueskyTranslateView.tsx | 7 ++++ .../src/ExpoScrollForwarderView.tsx | 2 ++ src/view/com/post-thread/PostThreadItem.tsx | 33 +++++++++++++------ 12 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 modules/expo-bluesky-translate/expo-module.config.json create mode 100644 modules/expo-bluesky-translate/index.ts create mode 100644 modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift create mode 100644 modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift create mode 100644 modules/expo-bluesky-translate/ios/TranslateView.swift create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx create mode 100644 modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx 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 0000000000..28c5dd878d --- /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 0000000000..d75c667969 --- /dev/null +++ b/modules/expo-bluesky-translate/index.ts @@ -0,0 +1,4 @@ +export { + ExpoBlueskyTranslateView, + isAvailable, +} 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 0000000000..c8ca3e0273 --- /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) { + 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 0000000000..45f86a6056 --- /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 0000000000..ea5bd705ff --- /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") + View(ExpoBlueskyTranslateView.self) { + Events("onEvent") + Prop("text") { (view: ExpoBlueskyTranslateView, text: String) in + view.props.text = text + } + Prop("isPresented") { (view: ExpoBlueskyTranslateView, isPresented: Bool) in + view.props.isPresented = isPresented + } + } + } +} diff --git a/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift new file mode 100644 index 0000000000..6f4bb7f857 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift @@ -0,0 +1,30 @@ +import ExpoModulesCore +import Foundation +import SwiftUI + +class TranslateViewProps: ObservableObject { + @Published var text: String = "" + @Published var isPresented: Bool = false + @Published var children: [UIView]? + @Published var onEvent: EventDispatcher + init(onEvent: EventDispatcher) { + self.onEvent = onEvent + } +} + +class ExpoBlueskyTranslateView: ExpoView { + let props: TranslateViewProps + let onEvent = EventDispatcher() + + override func didUpdateReactSubviews() { + let subChildren = self.reactSubviews() + props.children = subChildren + } + + required init(appContext: AppContext? = nil) { + props = TranslateViewProps(onEvent: onEvent) + let hostingController = UIHostingController(rootView: TranslateView(props: props)) + super.init(appContext: appContext) + setupHostingController(hostingController) + } +} diff --git a/modules/expo-bluesky-translate/ios/TranslateView.swift b/modules/expo-bluesky-translate/ios/TranslateView.swift new file mode 100644 index 0000000000..94aea9dd64 --- /dev/null +++ b/modules/expo-bluesky-translate/ios/TranslateView.swift @@ -0,0 +1,33 @@ +import SwiftUI +import Translation + +struct TranslateView: View { + @ObservedObject var props: TranslateViewProps + + var body: some View { + if #available(iOS 17.4, *) { + VStack { + ForEach(props.children?.indices ?? 0..<0, id: \.self) { index in + UIViewRepresentableWrapper(view: props.children?[index] ?? UIView()) + .frame( + width: props.children?[index].frame.width, + height: props.children?[index].frame.height) + } + } + .translationPresentation( + isPresented: $props.isPresented, + text: props.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 0000000000..f070f47597 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts @@ -0,0 +1,7 @@ +import React from 'react' + +export type ExpoBlueskyTranslateProps = { + text: string + isPresented?: boolean + children: React.ReactNode +} 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 0000000000..c9434a9d5e --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import {Platform} from 'react-native' +import {requireNativeViewManager} from 'expo-modules-core' + +import {ExpoBlueskyTranslateProps} from './ExpoBlueskyTranslate.types' + +const NativeView: React.ComponentType = + requireNativeViewManager('ExpoBlueskyTranslate') + +export function ExpoBlueskyTranslateView(props: ExpoBlueskyTranslateProps) { + return +} + +export const isAvailable = Number(Platform.Version) >= 17.4 diff --git a/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx new file mode 100644 index 0000000000..3af08a1508 --- /dev/null +++ b/modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx @@ -0,0 +1,7 @@ +import {ExpoBlueskyTranslateProps} from './ExpoBlueskyTranslate.types' + +export function ExpoBlueskyTranslateView(_: ExpoBlueskyTranslateProps) { + return null +} + +export const isAvailable = false diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx index 93e69333fd..0f5d01c130 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) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index f644a5366d..31a85b6537 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -32,6 +32,10 @@ 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 { + ExpoBlueskyTranslateView, + isAvailable as isNativeTranslateAvailable, +} from '../../../../modules/expo-bluesky-translate' import {ContentHider} from '../../../components/moderation/ContentHider' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' @@ -318,6 +322,7 @@ let PostThreadItemLoaded = ({ @@ -620,32 +625,40 @@ function PostOuterWrapper({ function ExpandedPostDetails({ post, + text, needsTranslation, translatorUrl, }: { post: AppBskyFeedDefs.PostView + text: string needsTranslation: boolean translatorUrl: string }) { const pal = usePalette('default') const {_} = useLingui() const openLink = useOpenLink() - const onTranslatePress = React.useCallback( - () => openLink(translatorUrl), - [openLink, translatorUrl], - ) + const [presented, setPresented] = React.useState(false) + const onTranslatePress = React.useCallback(() => { + if (isNativeTranslateAvailable) { + setPresented(true) + } else { + openLink(translatorUrl) + } + }, [openLink, translatorUrl]) return ( {niceDate(post.indexedAt)} {needsTranslation && ( <> · - - Translate - + + + Translate + + )}