about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--modules/expo-bluesky-translate/expo-module.config.json6
-rw-r--r--modules/expo-bluesky-translate/index.ts6
-rw-r--r--modules/expo-bluesky-translate/ios/Common/UIHostingControllerCompat.swift20
-rw-r--r--modules/expo-bluesky-translate/ios/ExpoBlueskyTranslate.podspec21
-rw-r--r--modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateModule.swift18
-rw-r--r--modules/expo-bluesky-translate/ios/ExpoBlueskyTranslateView.swift22
-rw-r--r--modules/expo-bluesky-translate/ios/TranslateView.swift31
-rw-r--r--modules/expo-bluesky-translate/src/ExpoBlueskyTranslate.types.ts3
-rw-r--r--modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.ios.tsx48
-rw-r--r--modules/expo-bluesky-translate/src/ExpoBlueskyTranslateView.tsx13
-rw-r--r--modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx2
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx27
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx21
-rw-r--r--src/view/shell/index.tsx2
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}> &middot; </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 />