+2
-1
app.config.js
+2
-1
app.config.js
···
234
],
235
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
236
'./plugins/withGradleJVMHeapSizeIncrease.js',
237
-
'./plugins/withAndroidManifestPlugin.js',
238
'./plugins/withAndroidManifestFCMIconPlugin.js',
239
'./plugins/withAndroidStylesAccentColorPlugin.js',
240
'./plugins/withAndroidDayNightThemePlugin.js',
241
'./plugins/withAndroidNoJitpackPlugin.js',
···
234
],
235
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
236
'./plugins/withGradleJVMHeapSizeIncrease.js',
237
+
'./plugins/withAndroidManifestLargeHeapPlugin.js',
238
'./plugins/withAndroidManifestFCMIconPlugin.js',
239
+
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
240
'./plugins/withAndroidStylesAccentColorPlugin.js',
241
'./plugins/withAndroidDayNightThemePlugin.js',
242
'./plugins/withAndroidNoJitpackPlugin.js',
+1
package.json
+1
package.json
+30
plugins/withAndroidManifestIntentQueriesPlugin.js
+30
plugins/withAndroidManifestIntentQueriesPlugin.js
···
···
1
+
const {withAndroidManifest} = require('@expo/config-plugins')
2
+
3
+
const withProcessTextQuery = config =>
4
+
// eslint-disable-next-line no-shadow
5
+
withAndroidManifest(config, config => {
6
+
const manifest = config.modResults.manifest
7
+
8
+
// Ensure <queries> stub exists
9
+
if (!manifest.queries) manifest.queries = [{}]
10
+
const queries = manifest.queries[0]
11
+
12
+
queries.intent = queries.intent || []
13
+
14
+
const exists = queries.intent.some(
15
+
i =>
16
+
i.action?.[0]?.$?.['android:name'] ===
17
+
'android.intent.action.PROCESS_TEXT',
18
+
)
19
+
20
+
if (!exists) {
21
+
queries.intent.push({
22
+
action: [{$: {'android:name': 'android.intent.action.PROCESS_TEXT'}}],
23
+
data: [{$: {'android:mimeType': 'text/plain'}}],
24
+
})
25
+
}
26
+
27
+
return config
28
+
})
29
+
30
+
module.exports = withProcessTextQuery
plugins/withAndroidManifestPlugin.js
plugins/withAndroidManifestLargeHeapPlugin.js
plugins/withAndroidManifestPlugin.js
plugins/withAndroidManifestLargeHeapPlugin.js
+4
-8
src/components/PostControls/PostMenu/PostMenuItems.tsx
+4
-8
src/components/PostControls/PostMenu/PostMenuItems.tsx
···
19
20
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
21
import {useOpenLink} from '#/lib/hooks/useOpenLink'
22
import {getCurrentRoute} from '#/lib/routes/helpers'
23
import {makeProfileLink} from '#/lib/routes/links'
24
import {
···
28
import {logEvent, useGate} from '#/lib/statsig/statsig'
29
import {richTextToString} from '#/lib/strings/rich-text-helpers'
30
import {toShareUrl} from '#/lib/strings/url-helpers'
31
-
import {getTranslatorLink} from '#/locale/helpers'
32
import {logger} from '#/logger'
33
import {type Shadow} from '#/state/cache/post-shadow'
34
import {useProfileShadow} from '#/state/cache/profile-shadow'
···
118
const {hidePost} = useHiddenPostsApi()
119
const feedFeedback = useFeedFeedbackContext()
120
const openLink = useOpenLink()
121
const navigation = useNavigation<NavigationProp>()
122
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
123
const blockPromptControl = useDialogControl()
···
172
return makeProfileLink(postAuthor, 'post', urip.rkey)
173
}, [postUri, postAuthor])
174
175
-
const translatorUrl = getTranslatorLink(
176
-
record.text,
177
-
langPrefs.primaryLanguage,
178
-
)
179
-
180
const onDeletePost = () => {
181
deletePostMutate({uri: postUri}).then(
182
() => {
···
234
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
235
}
236
237
-
const onPressTranslate = async () => {
238
-
await openLink(translatorUrl, true)
239
240
if (
241
bsky.dangerousIsType<AppBskyFeedPost.Record>(
···
19
20
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
21
import {useOpenLink} from '#/lib/hooks/useOpenLink'
22
+
import {useTranslate} from '#/lib/hooks/useTranslate'
23
import {getCurrentRoute} from '#/lib/routes/helpers'
24
import {makeProfileLink} from '#/lib/routes/links'
25
import {
···
29
import {logEvent, useGate} from '#/lib/statsig/statsig'
30
import {richTextToString} from '#/lib/strings/rich-text-helpers'
31
import {toShareUrl} from '#/lib/strings/url-helpers'
32
import {logger} from '#/logger'
33
import {type Shadow} from '#/state/cache/post-shadow'
34
import {useProfileShadow} from '#/state/cache/profile-shadow'
···
118
const {hidePost} = useHiddenPostsApi()
119
const feedFeedback = useFeedFeedbackContext()
120
const openLink = useOpenLink()
121
+
const translate = useTranslate()
122
const navigation = useNavigation<NavigationProp>()
123
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
124
const blockPromptControl = useDialogControl()
···
173
return makeProfileLink(postAuthor, 'post', urip.rkey)
174
}, [postUri, postAuthor])
175
176
const onDeletePost = () => {
177
deletePostMutate({uri: postUri}).then(
178
() => {
···
230
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
231
}
232
233
+
const onPressTranslate = () => {
234
+
translate(record.text, langPrefs.primaryLanguage)
235
236
if (
237
bsky.dangerousIsType<AppBskyFeedPost.Record>(
+4
-9
src/components/dms/MessageContextMenu.tsx
+4
-9
src/components/dms/MessageContextMenu.tsx
···
5
import {msg} from '@lingui/macro'
6
import {useLingui} from '@lingui/react'
7
8
-
import {useOpenLink} from '#/lib/hooks/useOpenLink'
9
import {richTextToString} from '#/lib/strings/rich-text-helpers'
10
-
import {getTranslatorLink} from '#/locale/helpers'
11
import {logger} from '#/logger'
12
import {isNative} from '#/platform/detection'
13
import {useConvoActive} from '#/state/messages/convo'
···
39
const deleteControl = usePromptControl()
40
const reportControl = usePromptControl()
41
const langPrefs = useLanguagePrefs()
42
-
const openLink = useOpenLink()
43
44
const isFromSelf = message.sender?.did === currentAccount?.did
45
···
57
}, [_, message.text, message.facets])
58
59
const onPressTranslateMessage = useCallback(() => {
60
-
const translatorUrl = getTranslatorLink(
61
-
message.text,
62
-
langPrefs.primaryLanguage,
63
-
)
64
-
openLink(translatorUrl, true)
65
66
logger.metric(
67
'translate',
···
72
},
73
{statsig: false},
74
)
75
-
}, [langPrefs.primaryLanguage, message.text, openLink])
76
77
const onDelete = useCallback(() => {
78
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
···
5
import {msg} from '@lingui/macro'
6
import {useLingui} from '@lingui/react'
7
8
+
import {useTranslate} from '#/lib/hooks/useTranslate'
9
import {richTextToString} from '#/lib/strings/rich-text-helpers'
10
import {logger} from '#/logger'
11
import {isNative} from '#/platform/detection'
12
import {useConvoActive} from '#/state/messages/convo'
···
38
const deleteControl = usePromptControl()
39
const reportControl = usePromptControl()
40
const langPrefs = useLanguagePrefs()
41
+
const translate = useTranslate()
42
43
const isFromSelf = message.sender?.did === currentAccount?.did
44
···
56
}, [_, message.text, message.facets])
57
58
const onPressTranslateMessage = useCallback(() => {
59
+
translate(message.text, langPrefs.primaryLanguage)
60
61
logger.metric(
62
'translate',
···
67
},
68
{statsig: false},
69
)
70
+
}, [langPrefs.primaryLanguage, message.text, translate])
71
72
const onDelete = useCallback(() => {
73
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+54
src/lib/hooks/useTranslate.ts
+54
src/lib/hooks/useTranslate.ts
···
···
1
+
import {useCallback} from 'react'
2
+
import * as IntentLauncher from 'expo-intent-launcher'
3
+
4
+
import {getTranslatorLink} from '#/locale/helpers'
5
+
import {isAndroid} from '#/platform/detection'
6
+
import {useOpenLink} from './useOpenLink'
7
+
8
+
export function useTranslate() {
9
+
const openLink = useOpenLink()
10
+
11
+
return useCallback(
12
+
async (text: string, language: string) => {
13
+
const translateUrl = getTranslatorLink(text, language)
14
+
if (isAndroid) {
15
+
try {
16
+
// use getApplicationIconAsync to determine if the translate app is installed
17
+
if (
18
+
!(await IntentLauncher.getApplicationIconAsync(
19
+
'com.google.android.apps.translate',
20
+
))
21
+
) {
22
+
throw new Error('Translate app not installed')
23
+
}
24
+
25
+
// TODO: this should only be called one at a time, use something like
26
+
// RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn
27
+
await IntentLauncher.startActivityAsync(
28
+
'android.intent.action.PROCESS_TEXT',
29
+
{
30
+
type: 'text/plain',
31
+
extra: {
32
+
'android.intent.extra.PROCESS_TEXT': text,
33
+
'android.intent.extra.PROCESS_TEXT_READONLY': true,
34
+
},
35
+
// note: to skip the intermediate app select, we need to specify a
36
+
// `className`. however, this isn't safe to hardcode, we'd need to query the
37
+
// package manager for the correct activity. this requires native code, so
38
+
// skip for now -sfn
39
+
// packageName: 'com.google.android.apps.translate',
40
+
// className: 'com.google.android.apps.translate.TranslateActivity',
41
+
},
42
+
)
43
+
} catch (err) {
44
+
if (__DEV__) console.error(err)
45
+
// most likely means they don't have the translate app
46
+
await openLink(translateUrl)
47
+
}
48
+
} else {
49
+
await openLink(translateUrl)
50
+
}
51
+
},
52
+
[openLink],
53
+
)
54
+
}
+10
-9
src/screens/PostThread/components/ThreadItemAnchor.tsx
+10
-9
src/screens/PostThread/components/ThreadItemAnchor.tsx
···
12
13
import {useActorStatus} from '#/lib/actor-status'
14
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
15
-
import {useOpenLink} from '#/lib/hooks/useOpenLink'
16
import {makeProfileLink} from '#/lib/routes/links'
17
import {sanitizeDisplayName} from '#/lib/strings/display-names'
18
import {sanitizeHandle} from '#/lib/strings/handles'
···
509
}) {
510
const t = useTheme()
511
const {_, i18n} = useLingui()
512
-
const openLink = useOpenLink()
513
const isRootPost = !('reply' in post.record)
514
const langPrefs = useLanguagePrefs()
515
516
-
const translatorUrl = getTranslatorLink(
517
-
post.record?.text || '',
518
-
langPrefs.primaryLanguage,
519
-
)
520
const needsTranslation = useMemo(
521
() =>
522
Boolean(
···
529
const onTranslatePress = useCallback(
530
(e: GestureResponderEvent) => {
531
e.preventDefault()
532
-
openLink(translatorUrl, true)
533
534
if (
535
bsky.dangerousIsType<AppBskyFeedPost.Record>(
···
546
547
return false
548
},
549
-
[openLink, translatorUrl, langPrefs, post],
550
)
551
552
return (
···
566
</Text>
567
568
<InlineLinkText
569
-
to={translatorUrl}
570
label={_(msg`Translate`)}
571
style={[a.text_sm]}
572
onPress={onTranslatePress}>
···
12
13
import {useActorStatus} from '#/lib/actor-status'
14
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
15
+
import {useTranslate} from '#/lib/hooks/useTranslate'
16
import {makeProfileLink} from '#/lib/routes/links'
17
import {sanitizeDisplayName} from '#/lib/strings/display-names'
18
import {sanitizeHandle} from '#/lib/strings/handles'
···
509
}) {
510
const t = useTheme()
511
const {_, i18n} = useLingui()
512
+
const translate = useTranslate()
513
const isRootPost = !('reply' in post.record)
514
const langPrefs = useLanguagePrefs()
515
516
const needsTranslation = useMemo(
517
() =>
518
Boolean(
···
525
const onTranslatePress = useCallback(
526
(e: GestureResponderEvent) => {
527
e.preventDefault()
528
+
translate(post.record.text || '', langPrefs.primaryLanguage)
529
530
if (
531
bsky.dangerousIsType<AppBskyFeedPost.Record>(
···
542
543
return false
544
},
545
+
[translate, langPrefs, post],
546
)
547
548
return (
···
562
</Text>
563
564
<InlineLinkText
565
+
// overridden to open an intent on android, but keep
566
+
// as anchor tag for accessibility
567
+
to={getTranslatorLink(
568
+
post.record.text,
569
+
langPrefs.primaryLanguage,
570
+
)}
571
label={_(msg`Translate`)}
572
style={[a.text_sm]}
573
onPress={onTranslatePress}>
+10
-12
src/view/com/post-thread/PostThreadItem.tsx
+10
-12
src/view/com/post-thread/PostThreadItem.tsx
···
19
import {useActorStatus} from '#/lib/actor-status'
20
import {MAX_POST_LINES} from '#/lib/constants'
21
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
22
-
import {useOpenLink} from '#/lib/hooks/useOpenLink'
23
import {usePalette} from '#/lib/hooks/usePalette'
24
import {makeProfileLink} from '#/lib/routes/links'
25
import {sanitizeDisplayName} from '#/lib/strings/display-names'
26
import {sanitizeHandle} from '#/lib/strings/handles'
···
273
const showFollowButton =
274
currentAccount?.did !== post.author.did && !onlyFollowersCanReply
275
276
-
const translatorUrl = getTranslatorLink(
277
-
record?.text || '',
278
-
langPrefs.primaryLanguage,
279
-
)
280
const needsTranslation = useMemo(
281
() =>
282
Boolean(
···
477
</ContentHider>
478
<ExpandedPostDetails
479
post={post}
480
isThreadAuthor={isThreadAuthor}
481
-
translatorUrl={translatorUrl}
482
needsTranslation={needsTranslation}
483
/>
484
{post.repostCount !== 0 ||
···
824
825
function ExpandedPostDetails({
826
post,
827
isThreadAuthor,
828
needsTranslation,
829
-
translatorUrl,
830
}: {
831
post: AppBskyFeedDefs.PostView
832
isThreadAuthor: boolean
833
needsTranslation: boolean
834
-
translatorUrl: string
835
}) {
836
const t = useTheme()
837
const pal = usePalette('default')
838
const {_, i18n} = useLingui()
839
-
const openLink = useOpenLink()
840
const isRootPost = !('reply' in post.record)
841
const langPrefs = useLanguagePrefs()
842
843
const onTranslatePress = useCallback(
844
(e: GestureResponderEvent) => {
845
e.preventDefault()
846
-
openLink(translatorUrl, true)
847
848
if (
849
bsky.dangerousIsType<AppBskyFeedPost.Record>(
···
864
865
return false
866
},
867
-
[openLink, translatorUrl, langPrefs, post],
868
)
869
870
return (
···
884
</Text>
885
886
<InlineLinkText
887
-
to={translatorUrl}
888
label={_(msg`Translate`)}
889
style={[a.text_sm, pal.link]}
890
onPress={onTranslatePress}>
···
19
import {useActorStatus} from '#/lib/actor-status'
20
import {MAX_POST_LINES} from '#/lib/constants'
21
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
22
import {usePalette} from '#/lib/hooks/usePalette'
23
+
import {useTranslate} from '#/lib/hooks/useTranslate'
24
import {makeProfileLink} from '#/lib/routes/links'
25
import {sanitizeDisplayName} from '#/lib/strings/display-names'
26
import {sanitizeHandle} from '#/lib/strings/handles'
···
273
const showFollowButton =
274
currentAccount?.did !== post.author.did && !onlyFollowersCanReply
275
276
const needsTranslation = useMemo(
277
() =>
278
Boolean(
···
473
</ContentHider>
474
<ExpandedPostDetails
475
post={post}
476
+
record={record}
477
isThreadAuthor={isThreadAuthor}
478
needsTranslation={needsTranslation}
479
/>
480
{post.repostCount !== 0 ||
···
820
821
function ExpandedPostDetails({
822
post,
823
+
record,
824
isThreadAuthor,
825
needsTranslation,
826
}: {
827
post: AppBskyFeedDefs.PostView
828
+
record: AppBskyFeedPost.Record
829
isThreadAuthor: boolean
830
needsTranslation: boolean
831
}) {
832
const t = useTheme()
833
const pal = usePalette('default')
834
const {_, i18n} = useLingui()
835
+
const translate = useTranslate()
836
const isRootPost = !('reply' in post.record)
837
const langPrefs = useLanguagePrefs()
838
839
const onTranslatePress = useCallback(
840
(e: GestureResponderEvent) => {
841
e.preventDefault()
842
+
translate(record.text || '', langPrefs.primaryLanguage)
843
844
if (
845
bsky.dangerousIsType<AppBskyFeedPost.Record>(
···
860
861
return false
862
},
863
+
[translate, record.text, langPrefs, post],
864
)
865
866
return (
···
880
</Text>
881
882
<InlineLinkText
883
+
// overridden to open an intent on android, but keep
884
+
// as anchor tag for accessibility
885
+
to={getTranslatorLink(record.text, langPrefs.primaryLanguage)}
886
label={_(msg`Translate`)}
887
style={[a.text_sm, pal.link]}
888
onPress={onTranslatePress}>
+5
-14
yarn.lock
+5
-14
yarn.lock
···
77
tlds "^1.234.0"
78
zod "^3.23.8"
79
80
-
"@atproto/api@^0.16.2":
81
-
version "0.16.2"
82
-
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.16.2.tgz#1b2870e9a03d88f00a27602281755fa82ec824dd"
83
-
integrity sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==
84
-
dependencies:
85
-
"@atproto/common-web" "^0.4.2"
86
-
"@atproto/lexicon" "^0.4.12"
87
-
"@atproto/syntax" "^0.4.0"
88
-
"@atproto/xrpc" "^0.7.1"
89
-
await-lock "^2.2.2"
90
-
multiformats "^9.9.0"
91
-
tlds "^1.234.0"
92
-
zod "^3.23.8"
93
-
94
"@atproto/aws@^0.2.25":
95
version "0.2.25"
96
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.25.tgz#d07265a656db990ffd54b254cae54388468d1dca"
···
11320
version "2.4.0"
11321
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
11322
integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
11323
11324
expo-json-utils@~0.15.0:
11325
version "0.15.0"
···
77
tlds "^1.234.0"
78
zod "^3.23.8"
79
80
"@atproto/aws@^0.2.25":
81
version "0.2.25"
82
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.25.tgz#d07265a656db990ffd54b254cae54388468d1dca"
···
11306
version "2.4.0"
11307
resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.0.tgz#02f7fd743387206914cd431a6367f5be53509e3e"
11308
integrity sha512-TQ/LvrtJ9JBr+Tf198CAqflxcvdhuj7P24n0LQ1jHaWIVA7Z+zYKbYHnSMPSDMul/y0U46Z5bFLbiZiSidgcNw==
11309
+
11310
+
expo-intent-launcher@^12.1.5:
11311
+
version "12.1.5"
11312
+
resolved "https://registry.yarnpkg.com/expo-intent-launcher/-/expo-intent-launcher-12.1.5.tgz#ed3051292b33e131535d9b35ca20b48cf56d1364"
11313
+
integrity sha512-KmCc/dJHTnVf2ZdrZhYSkvQ588K7qQW+nBGfJj5woCwhEXwYz1xOLQcShnPQgQWRf8conAvQDkI3pbjYNPcECw==
11314
11315
expo-json-utils@~0.15.0:
11316
version "0.15.0"