diff options
-rw-r--r-- | app.config.js | 3 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | plugins/withAndroidManifestIntentQueriesPlugin.js | 30 | ||||
-rw-r--r-- | plugins/withAndroidManifestLargeHeapPlugin.js (renamed from plugins/withAndroidManifestPlugin.js) | 0 | ||||
-rw-r--r-- | src/components/PostControls/PostMenu/PostMenuItems.tsx | 12 | ||||
-rw-r--r-- | src/components/dms/MessageContextMenu.tsx | 13 | ||||
-rw-r--r-- | src/lib/hooks/useTranslate.ts | 54 | ||||
-rw-r--r-- | src/screens/PostThread/components/ThreadItemAnchor.tsx | 19 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 22 | ||||
-rw-r--r-- | yarn.lock | 19 |
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" |