about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app.config.js3
-rw-r--r--package.json1
-rw-r--r--plugins/withAndroidManifestIntentQueriesPlugin.js30
-rw-r--r--plugins/withAndroidManifestLargeHeapPlugin.js (renamed from plugins/withAndroidManifestPlugin.js)0
-rw-r--r--src/components/PostControls/PostMenu/PostMenuItems.tsx12
-rw-r--r--src/components/dms/MessageContextMenu.tsx13
-rw-r--r--src/lib/hooks/useTranslate.ts54
-rw-r--r--src/screens/PostThread/components/ThreadItemAnchor.tsx19
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx22
-rw-r--r--yarn.lock19
10 files changed, 120 insertions, 53 deletions
diff --git a/app.config.js b/app.config.js
index 057806f79..5cad620db 100644
--- a/app.config.js
+++ b/app.config.js
@@ -234,8 +234,9 @@ module.exports = function (_config) {
         ],
         './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
         './plugins/withGradleJVMHeapSizeIncrease.js',
-        './plugins/withAndroidManifestPlugin.js',
+        './plugins/withAndroidManifestLargeHeapPlugin.js',
         './plugins/withAndroidManifestFCMIconPlugin.js',
+        './plugins/withAndroidManifestIntentQueriesPlugin.js',
         './plugins/withAndroidStylesAccentColorPlugin.js',
         './plugins/withAndroidDayNightThemePlugin.js',
         './plugins/withAndroidNoJitpackPlugin.js',
diff --git a/package.json b/package.json
index 2b44ead9f..b0c0b3682 100644
--- a/package.json
+++ b/package.json
@@ -146,6 +146,7 @@
     "expo-image-crop-tool": "^0.1.8",
     "expo-image-manipulator": "~13.1.7",
     "expo-image-picker": "~16.1.4",
+    "expo-intent-launcher": "^12.1.5",
     "expo-linear-gradient": "~14.1.5",
     "expo-linking": "~7.1.5",
     "expo-localization": "~16.1.5",
diff --git a/plugins/withAndroidManifestIntentQueriesPlugin.js b/plugins/withAndroidManifestIntentQueriesPlugin.js
new file mode 100644
index 000000000..04237b059
--- /dev/null
+++ b/plugins/withAndroidManifestIntentQueriesPlugin.js
@@ -0,0 +1,30 @@
+const {withAndroidManifest} = require('@expo/config-plugins')
+
+const withProcessTextQuery = config =>
+  // eslint-disable-next-line no-shadow
+  withAndroidManifest(config, config => {
+    const manifest = config.modResults.manifest
+
+    // Ensure <queries> stub exists
+    if (!manifest.queries) manifest.queries = [{}]
+    const queries = manifest.queries[0]
+
+    queries.intent = queries.intent || []
+
+    const exists = queries.intent.some(
+      i =>
+        i.action?.[0]?.$?.['android:name'] ===
+        'android.intent.action.PROCESS_TEXT',
+    )
+
+    if (!exists) {
+      queries.intent.push({
+        action: [{$: {'android:name': 'android.intent.action.PROCESS_TEXT'}}],
+        data: [{$: {'android:mimeType': 'text/plain'}}],
+      })
+    }
+
+    return config
+  })
+
+module.exports = withProcessTextQuery
diff --git a/plugins/withAndroidManifestPlugin.js b/plugins/withAndroidManifestLargeHeapPlugin.js
index 55fd3f5ca..55fd3f5ca 100644
--- a/plugins/withAndroidManifestPlugin.js
+++ b/plugins/withAndroidManifestLargeHeapPlugin.js
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index ecc3d0174..3fd919cd3 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -19,6 +19,7 @@ import {useNavigation} from '@react-navigation/native'
 
 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
 import {useOpenLink} from '#/lib/hooks/useOpenLink'
+import {useTranslate} from '#/lib/hooks/useTranslate'
 import {getCurrentRoute} from '#/lib/routes/helpers'
 import {makeProfileLink} from '#/lib/routes/links'
 import {
@@ -28,7 +29,6 @@ import {
 import {logEvent, useGate} from '#/lib/statsig/statsig'
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
 import {toShareUrl} from '#/lib/strings/url-helpers'
-import {getTranslatorLink} from '#/locale/helpers'
 import {logger} from '#/logger'
 import {type Shadow} from '#/state/cache/post-shadow'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -118,6 +118,7 @@ let PostMenuItems = ({
   const {hidePost} = useHiddenPostsApi()
   const feedFeedback = useFeedFeedbackContext()
   const openLink = useOpenLink()
+  const translate = useTranslate()
   const navigation = useNavigation<NavigationProp>()
   const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
   const blockPromptControl = useDialogControl()
@@ -172,11 +173,6 @@ let PostMenuItems = ({
     return makeProfileLink(postAuthor, 'post', urip.rkey)
   }, [postUri, postAuthor])
 
-  const translatorUrl = getTranslatorLink(
-    record.text,
-    langPrefs.primaryLanguage,
-  )
-
   const onDeletePost = () => {
     deletePostMutate({uri: postUri}).then(
       () => {
@@ -234,8 +230,8 @@ let PostMenuItems = ({
     Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
   }
 
-  const onPressTranslate = async () => {
-    await openLink(translatorUrl, true)
+  const onPressTranslate = () => {
+    translate(record.text, langPrefs.primaryLanguage)
 
     if (
       bsky.dangerousIsType<AppBskyFeedPost.Record>(
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index d1771659d..670e677db 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -5,9 +5,8 @@ import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
+import {useTranslate} from '#/lib/hooks/useTranslate'
 import {richTextToString} from '#/lib/strings/rich-text-helpers'
-import {getTranslatorLink} from '#/locale/helpers'
 import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {useConvoActive} from '#/state/messages/convo'
@@ -39,7 +38,7 @@ export let MessageContextMenu = ({
   const deleteControl = usePromptControl()
   const reportControl = usePromptControl()
   const langPrefs = useLanguagePrefs()
-  const openLink = useOpenLink()
+  const translate = useTranslate()
 
   const isFromSelf = message.sender?.did === currentAccount?.did
 
@@ -57,11 +56,7 @@ export let MessageContextMenu = ({
   }, [_, message.text, message.facets])
 
   const onPressTranslateMessage = useCallback(() => {
-    const translatorUrl = getTranslatorLink(
-      message.text,
-      langPrefs.primaryLanguage,
-    )
-    openLink(translatorUrl, true)
+    translate(message.text, langPrefs.primaryLanguage)
 
     logger.metric(
       'translate',
@@ -72,7 +67,7 @@ export let MessageContextMenu = ({
       },
       {statsig: false},
     )
-  }, [langPrefs.primaryLanguage, message.text, openLink])
+  }, [langPrefs.primaryLanguage, message.text, translate])
 
   const onDelete = useCallback(() => {
     LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
diff --git a/src/lib/hooks/useTranslate.ts b/src/lib/hooks/useTranslate.ts
new file mode 100644
index 000000000..7824a682c
--- /dev/null
+++ b/src/lib/hooks/useTranslate.ts
@@ -0,0 +1,54 @@
+import {useCallback} from 'react'
+import * as IntentLauncher from 'expo-intent-launcher'
+
+import {getTranslatorLink} from '#/locale/helpers'
+import {isAndroid} from '#/platform/detection'
+import {useOpenLink} from './useOpenLink'
+
+export function useTranslate() {
+  const openLink = useOpenLink()
+
+  return useCallback(
+    async (text: string, language: string) => {
+      const translateUrl = getTranslatorLink(text, language)
+      if (isAndroid) {
+        try {
+          // use getApplicationIconAsync to determine if the translate app is installed
+          if (
+            !(await IntentLauncher.getApplicationIconAsync(
+              'com.google.android.apps.translate',
+            ))
+          ) {
+            throw new Error('Translate app not installed')
+          }
+
+          // TODO: this should only be called one at a time, use something like
+          // RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn
+          await IntentLauncher.startActivityAsync(
+            'android.intent.action.PROCESS_TEXT',
+            {
+              type: 'text/plain',
+              extra: {
+                'android.intent.extra.PROCESS_TEXT': text,
+                'android.intent.extra.PROCESS_TEXT_READONLY': true,
+              },
+              // note: to skip the intermediate app select, we need to specify a
+              // `className`. however, this isn't safe to hardcode, we'd need to query the
+              // package manager for the correct activity. this requires native code, so
+              // skip for now -sfn
+              // packageName: 'com.google.android.apps.translate',
+              // className: 'com.google.android.apps.translate.TranslateActivity',
+            },
+          )
+        } catch (err) {
+          if (__DEV__) console.error(err)
+          // most likely means they don't have the translate app
+          await openLink(translateUrl)
+        }
+      } else {
+        await openLink(translateUrl)
+      }
+    },
+    [openLink],
+  )
+}
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index 481c335db..39e84d383 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -12,7 +12,7 @@ import {useLingui} from '@lingui/react'
 
 import {useActorStatus} from '#/lib/actor-status'
 import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
+import {useTranslate} from '#/lib/hooks/useTranslate'
 import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
@@ -509,14 +509,10 @@ function ExpandedPostDetails({
 }) {
   const t = useTheme()
   const {_, i18n} = useLingui()
-  const openLink = useOpenLink()
+  const translate = useTranslate()
   const isRootPost = !('reply' in post.record)
   const langPrefs = useLanguagePrefs()
 
-  const translatorUrl = getTranslatorLink(
-    post.record?.text || '',
-    langPrefs.primaryLanguage,
-  )
   const needsTranslation = useMemo(
     () =>
       Boolean(
@@ -529,7 +525,7 @@ function ExpandedPostDetails({
   const onTranslatePress = useCallback(
     (e: GestureResponderEvent) => {
       e.preventDefault()
-      openLink(translatorUrl, true)
+      translate(post.record.text || '', langPrefs.primaryLanguage)
 
       if (
         bsky.dangerousIsType<AppBskyFeedPost.Record>(
@@ -546,7 +542,7 @@ function ExpandedPostDetails({
 
       return false
     },
-    [openLink, translatorUrl, langPrefs, post],
+    [translate, langPrefs, post],
   )
 
   return (
@@ -566,7 +562,12 @@ function ExpandedPostDetails({
             </Text>
 
             <InlineLinkText
-              to={translatorUrl}
+              // overridden to open an intent on android, but keep
+              // as anchor tag for accessibility
+              to={getTranslatorLink(
+                post.record.text,
+                langPrefs.primaryLanguage,
+              )}
               label={_(msg`Translate`)}
               style={[a.text_sm]}
               onPress={onTranslatePress}>
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 54eea0493..97a1aa8ed 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -19,8 +19,8 @@ import {useLingui} from '@lingui/react'
 import {useActorStatus} from '#/lib/actor-status'
 import {MAX_POST_LINES} from '#/lib/constants'
 import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {useOpenLink} from '#/lib/hooks/useOpenLink'
 import {usePalette} from '#/lib/hooks/usePalette'
+import {useTranslate} from '#/lib/hooks/useTranslate'
 import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
@@ -273,10 +273,6 @@ let PostThreadItemLoaded = ({
   const showFollowButton =
     currentAccount?.did !== post.author.did && !onlyFollowersCanReply
 
-  const translatorUrl = getTranslatorLink(
-    record?.text || '',
-    langPrefs.primaryLanguage,
-  )
   const needsTranslation = useMemo(
     () =>
       Boolean(
@@ -477,8 +473,8 @@ let PostThreadItemLoaded = ({
             </ContentHider>
             <ExpandedPostDetails
               post={post}
+              record={record}
               isThreadAuthor={isThreadAuthor}
-              translatorUrl={translatorUrl}
               needsTranslation={needsTranslation}
             />
             {post.repostCount !== 0 ||
@@ -824,26 +820,26 @@ function PostOuterWrapper({
 
 function ExpandedPostDetails({
   post,
+  record,
   isThreadAuthor,
   needsTranslation,
-  translatorUrl,
 }: {
   post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
   isThreadAuthor: boolean
   needsTranslation: boolean
-  translatorUrl: string
 }) {
   const t = useTheme()
   const pal = usePalette('default')
   const {_, i18n} = useLingui()
-  const openLink = useOpenLink()
+  const translate = useTranslate()
   const isRootPost = !('reply' in post.record)
   const langPrefs = useLanguagePrefs()
 
   const onTranslatePress = useCallback(
     (e: GestureResponderEvent) => {
       e.preventDefault()
-      openLink(translatorUrl, true)
+      translate(record.text || '', langPrefs.primaryLanguage)
 
       if (
         bsky.dangerousIsType<AppBskyFeedPost.Record>(
@@ -864,7 +860,7 @@ function ExpandedPostDetails({
 
       return false
     },
-    [openLink, translatorUrl, langPrefs, post],
+    [translate, record.text, langPrefs, post],
   )
 
   return (
@@ -884,7 +880,9 @@ function ExpandedPostDetails({
             </Text>
 
             <InlineLinkText
-              to={translatorUrl}
+              // overridden to open an intent on android, but keep
+              // as anchor tag for accessibility
+              to={getTranslatorLink(record.text, langPrefs.primaryLanguage)}
               label={_(msg`Translate`)}
               style={[a.text_sm, pal.link]}
               onPress={onTranslatePress}>
diff --git a/yarn.lock b/yarn.lock
index c6ef295ea..d5c203fa4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -77,20 +77,6 @@
     tlds "^1.234.0"
     zod "^3.23.8"
 
-"@atproto/api@^0.16.2":
-  version "0.16.2"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.2.tgz#1b2870e9a03d88f00a27602281755fa82ec824dd"
-  integrity sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==
-  dependencies:
-    "@atproto/common-web" "^0.4.2"
-    "@atproto/lexicon" "^0.4.12"
-    "@atproto/syntax" "^0.4.0"
-    "@atproto/xrpc" "^0.7.1"
-    await-lock "^2.2.2"
-    multiformats "^9.9.0"
-    tlds "^1.234.0"
-    zod "^3.23.8"
-
 "@atproto/aws@^0.2.25":
   version "0.2.25"
   resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.25.tgz#d07265a656db990ffd54b254cae54388468d1dca"
@@ -11321,6 +11307,11 @@ expo-image@^2.4.0:
   resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
   integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
 
+expo-intent-launcher@^12.1.5:
+  version "12.1.5"
+  resolved "https://registry.yarnpkg.com/expo-intent-launcher/-/expo-intent-launcher-12.1.5.tgz#ed3051292b33e131535d9b35ca20b48cf56d1364"
+  integrity sha512-KmCc/dJHTnVf2ZdrZhYSkvQ588K7qQW+nBGfJj5woCwhEXwYz1xOLQcShnPQgQWRf8conAvQDkI3pbjYNPcECw==
+
 expo-json-utils@~0.15.0:
   version "0.15.0"
   resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.15.0.tgz#6723574814b9e6b0a90e4e23662be76123ab6ae9"