Fixed #119 and translation

This commit is contained in:
Zhiyuan Zheng 2021-05-23 22:40:42 +02:00
parent 0517d2fae2
commit 0190b35b57
17 changed files with 168 additions and 113 deletions

View File

@ -1,5 +1,5 @@
# [tooot](https://tooot.app/) app for Mastodon
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push?style=flat-square)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app?style=flat-square) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases&style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app?style=flat-square) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases&style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app) [![Crowdin](https://badges.crowdin.net/tooot/localized.svg)](https://crowdin.tooot.app/project/tooot)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build?style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate&style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release&style=flat-square)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate&style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release&style=flat-square)

View File

@ -19,6 +19,7 @@ public class BasePackageList {
new expo.modules.font.FontLoaderPackage(),
new expo.modules.haptics.HapticsPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.imagemanipulator.ImageManipulatorPackage(),
new expo.modules.imagepicker.ImagePickerPackage(),
new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.localization.LocalizationPackage(),

View File

@ -825,7 +825,7 @@ SPEC CHECKSUMS:
EXVideoThumbnails: cd257fc6e07884a704a5674d362a6410933acb68
EXWebBrowser: 0b466c50e5ff61c9758095d49d5081e3229d77ac
FBLazyVector: 7b423f9e248eae65987838148c36eec1dbfe0b53
FBReactNativeSpec: be55984d4a593b4ef281ead81139cdfb1812d259
FBReactNativeSpec: 5058d1917c80dca4b9ed89bdf94385315939ab80
Firebase: cd2ab85eec8170dc260186159f21072ecb679ad5
FirebaseAnalytics: f3f8f75de34fe04141a69bb1c4bd7e24a80178e1
FirebaseCore: ac35d680a0bf32319a59966a1478e0741536b97b

View File

@ -346,7 +346,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = tooot/tooot.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2102022230;
DEVELOPMENT_TEAM = 8EGBLQ2MA6;
@ -366,7 +366,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.xmflsct.app.tooot;
PRODUCT_NAME = tooot;
PROVISIONING_PROFILE_SPECIFIER = "match Development com.xmflsct.app.tooot";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc com.xmflsct.app.tooot";
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;

View File

@ -1,15 +0,0 @@
declare namespace Translate {
type Detect = {
confidence: number
language: string
}
type Language = {
code: string
name: string
}
type Translate = {
translatedText: string
}
}

View File

@ -74,7 +74,11 @@ const apiGeneral = async <T = unknown>({
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API general '), ctx.bold('request'), error)
console.error(
ctx.bold(' API general '),
ctx.bold('request'),
error.request
)
return Promise.reject()
} else {
console.error(

View File

@ -76,7 +76,7 @@ const MenuRow: React.FC<Props> = ({
}
}}
>
<View>
<View style={{ flex: 1 }}>
<View style={styles.core}>
<View style={styles.front}>
{iconFront && (

View File

@ -164,6 +164,7 @@ export interface Props {
expandHint?: string
highlighted?: boolean
disableDetails?: boolean
selectable?: boolean
}
const ParseHTML = React.memo(
@ -178,7 +179,8 @@ const ParseHTML = React.memo(
numberOfLines = 10,
expandHint,
highlighted = false,
disableDetails = false
disableDetails = false,
selectable = false
}: Props) => {
const adaptiveFontsize = useSelector(getSettingsFontsize)
const adaptedFontsize = adaptiveScale(
@ -255,6 +257,7 @@ const ParseHTML = React.memo(
numberOfLines={
expandAllow ? (expanded ? 999 : numberOfLines) : undefined
}
selectable={selectable}
/>
{expandAllow ? (
<Pressable

View File

@ -32,6 +32,7 @@ const TimelineContent = React.memo(
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
selectable={highlighted}
/>
<ParseHTML
content={status.content}
@ -44,6 +45,7 @@ const TimelineContent = React.memo(
expandHint={t('shared.content.expandHint')}
highlighted={highlighted}
disableDetails={disableDetails}
selectable={highlighted}
/>
</>
) : (
@ -56,6 +58,7 @@ const TimelineContent = React.memo(
tags={status.tags}
numberOfLines={highlighted ? 999 : numberOfLines}
disableDetails={disableDetails}
selectable={highlighted}
/>
)}
</>

View File

@ -1,32 +1,16 @@
import analytics from '@components/analytics'
import { ParseHTML } from '@components/Parse'
import { useTranslateQuery } from '@utils/queryHooks/translate'
import { getInstanceUri } from '@utils/slices/instancesSlice'
import { getSettingsLanguage } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import htmlparser2 from 'htmlparser2-without-node-native'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import { useSelector } from 'react-redux'
const availableLanguages = [
'en',
'ar',
'zh',
'fr',
'de',
'hi',
'ga',
'it',
'ja',
'ko',
'pl',
'pt',
'ru',
'es',
'tr'
]
export interface Props {
highlighted: boolean
status: Mastodon.Status
@ -45,48 +29,31 @@ const TimelineTranslate = React.memo(
const { theme } = useTheme()
const tootLanguage = status.language.slice(0, 2)
if (!availableLanguages.includes(tootLanguage)) {
return (
<Text
style={{
...StyleConstants.FontStyle.M,
color: theme.disabled
}}
>
{t('shared.translate.unavailable')}
</Text>
)
}
const settingsLanguage = useSelector(getSettingsLanguage, () => true)
const settingsLanguage = useSelector(getSettingsLanguage)
if (settingsLanguage.includes(tootLanguage)) {
return null
}
let emojisRemoved = status.spoiler_text
? status.spoiler_text.concat(status.content)
: status.content
if (status.emojis) {
let text = status.spoiler_text
? [status.spoiler_text, status.content]
: [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
emojisRemoved = emojisRemoved.replaceAll(`:${emoji.shortcode}:`, '')
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, '')
}
}
let cleaned = ''
const parser = new htmlparser2.Parser({
ontext (text: string) {
cleaned = cleaned.concat(text)
}
})
parser.write(emojisRemoved)
parser.end()
const instanceUri = useSelector(getInstanceUri)
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
toot: cleaned,
instance: instanceUri!,
id: status.id,
source: status.language,
target: settingsLanguage.slice(0, 2),
target: settingsLanguage,
text,
options: { enabled }
})
@ -97,9 +64,15 @@ const TimelineTranslate = React.memo(
onPress={() => {
if (enabled) {
if (!isSuccess) {
analytics('timeline_shared_translate_retry', {
language: status.language
})
refetch()
}
} else {
analytics('timeline_shared_translate', {
language: status.language
})
setEnabled(true)
}
}}
@ -109,15 +82,21 @@ const TimelineTranslate = React.memo(
...StyleConstants.FontStyle.M,
color:
isLoading || isSuccess
? theme.disabled
? theme.secondary
: isError
? theme.red
: theme.blue
}}
>
{isError
? t('shared.translate.error')
? t('shared.translate.failed')
: isSuccess
? t('shared.translate.succeed', {
provider: data?.provider,
source: data?.sourceLanguage
})
: t('shared.translate.default')}
{__DEV__ ? ` Source: ${status.language}` : undefined}
</Text>
{isLoading ? (
<Circle
@ -127,15 +106,17 @@ const TimelineTranslate = React.memo(
/>
) : null}
</Pressable>
{data ? (
<Text
style={{
...StyleConstants.FontStyle.M,
color: theme.primaryDefault
}}
children={data}
/>
) : null}
{data
? data.text.map((d, i) => (
<ParseHTML
key={i}
content={d}
size={'M'}
numberOfLines={999}
selectable
/>
))
: null}
</>
)
},

View File

@ -76,8 +76,8 @@
"fullConversation": "Read conversations",
"translate": {
"default": "Translate",
"error": "Try to translate again",
"unavailable": "Language not supported"
"succeed": "Translated by {{provider}} from {{source}}",
"failed": "Translation failed"
},
"header": {
"shared": {

View File

@ -87,6 +87,7 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({
emojis={item.emojis}
mentions={item.mentions}
numberOfLines={999}
selectable
/>
</ScrollView>
{item.reactions?.length ? (

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import { MenuContainer, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet'
@ -40,6 +41,12 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
async buttonIndex => {
switch (buttonIndex) {
case 0:
analytics('me_profile_visibility', {
current: t(
`me.profile.root.visibility.options.${data?.source.privacy}`
),
new: 'public'
})
mutateAsync({ type: 'source[privacy]', data: 'public' })
.then(() => dispatch(updateAccountPreferences()))
.catch(err =>
@ -55,6 +62,12 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
)
break
case 1:
analytics('me_profile_visibility', {
current: t(
`me.profile.root.visibility.options.${data?.source.privacy}`
),
new: 'unlisted'
})
mutateAsync({ type: 'source[privacy]', data: 'unlisted' })
.then(() => dispatch(updateAccountPreferences()))
.catch(err =>
@ -70,6 +83,12 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
)
break
case 2:
analytics('me_profile_visibility', {
current: t(
`me.profile.root.visibility.options.${data?.source.privacy}`
),
new: 'unlisted'
})
mutateAsync({ type: 'source[privacy]', data: 'private' })
.then(() => dispatch(updateAccountPreferences()))
.catch(err =>
@ -87,10 +106,14 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
}
}
)
}, [])
}, [data?.source.privacy])
const onPressSensitive = useCallback(() => {
if (data?.source.sensitive === undefined) {
analytics('me_profile_sensitive', {
current: undefined,
new: true
})
mutateAsync({ type: 'source[sensitive]', data: true })
.then(() => dispatch(updateAccountPreferences()))
.catch(err =>
@ -105,6 +128,10 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
})
)
} else {
analytics('me_profile_sensitive', {
current: data.source.sensitive,
new: !data.source.sensitive
})
mutateAsync({
type: 'source[sensitive]',
data: !data.source.sensitive
@ -126,6 +153,10 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
const onPressLock = useCallback(() => {
if (data?.locked === undefined) {
analytics('me_profile_lock', {
current: undefined,
new: true
})
mutateAsync({ type: 'locked', data: true }).catch(err =>
displayMessage({
ref: messageRef,
@ -138,6 +169,10 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
})
)
} else {
analytics('me_profile_lock', {
current: data.locked,
new: !data.locked
})
mutateAsync({ type: 'locked', data: !data.locked }).catch(err =>
displayMessage({
ref: messageRef,
@ -154,6 +189,10 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
const onPressBot = useCallback(() => {
if (data?.bot === undefined) {
analytics('me_profile_bot', {
current: undefined,
new: true
})
mutateAsync({ type: 'bot', data: true }).catch(err =>
displayMessage({
ref: messageRef,
@ -166,6 +205,10 @@ const TabMeProfileRoot: React.FC<StackScreenProps<
})
)
} else {
analytics('me_profile_bot', {
current: data.bot,
new: !data.bot
})
mutateAsync({ type: 'bot', data: !data?.bot }).catch(err =>
displayMessage({
ref: messageRef,

View File

@ -1,3 +1,4 @@
import analytics from '@components/analytics'
import Button from '@components/Button'
import { MenuContainer, MenuRow } from '@components/Menu'
import { updateInstancePush } from '@utils/slices/instances/updatePush'
@ -67,7 +68,11 @@ const TabMePush: React.FC = () => {
!pushEnabled || !instancePush.global.value || isLoading
}
switchValue={instancePush?.alerts[alert].value}
switchOnValueChange={() =>
switchOnValueChange={() => {
analytics(`me_push_${alert}`, {
current: instancePush?.alerts[alert].value,
new: !instancePush?.alerts[alert].value
})
dispatch(
updateInstancePushAlert({
changed: alert,
@ -80,7 +85,7 @@ const TabMePush: React.FC = () => {
}
})
)
}
}}
/>
))
: null
@ -103,10 +108,12 @@ const TabMePush: React.FC = () => {
}}
onPress={async () => {
if (pushCanAskAgain) {
analytics('me_push_enabled_dialogue')
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
analytics('me_push_enabled_setting')
Linking.openSettings()
}
}}
@ -124,9 +131,13 @@ const TabMePush: React.FC = () => {
switchValue={
pushEnabled === false ? false : instancePush?.global.value
}
switchOnValueChange={() =>
switchOnValueChange={() => {
analytics('me_push_global', {
current: instancePush?.global.value,
new: !instancePush?.global.value
})
dispatch(updateInstancePush(!instancePush?.global.value))
}
}}
/>
</MenuContainer>
<MenuContainer>
@ -138,16 +149,21 @@ const TabMePush: React.FC = () => {
!pushEnabled || !instancePush?.global.value || isLoading
}
switchValue={instancePush?.decode.value}
switchOnValueChange={() =>
switchOnValueChange={() => {
analytics('me_push_decode', {
current: instancePush?.decode.value,
new: !instancePush?.decode.value
})
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
}
}}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={() =>
onPress={() => {
analytics('me_push_howitworks')
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
}
}}
/>
</MenuContainer>
<MenuContainer>{alerts}</MenuContainer>

View File

@ -34,6 +34,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis}
showFullLink
numberOfLines={5}
selectable
/>
{field.verified_at ? (
<Icon
@ -51,6 +52,7 @@ const AccountInformationFields = React.memo(
emojis={account.emojis}
showFullLink
numberOfLines={5}
selectable
/>
</View>
</View>

View File

@ -28,7 +28,7 @@ const AccountInformationNote = React.memo(
return (
<View style={styles.note}>
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} />
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
</View>
)
},

View File

@ -1,39 +1,55 @@
import apiGeneral from '@api/general'
import { AxiosError } from 'axios'
import { Constants } from 'react-native-unimodules'
import { Buffer } from 'buffer'
import Constants from 'expo-constants'
import { useQuery, UseQueryOptions } from 'react-query'
type Translations = {
provider: string
sourceLanguage: string
text: string[]
}
export type QueryKeyTranslate = [
'Translate',
{ toot: string; source: string; target: string }
{
instance: string
id: string
source: string
target: string
text: string[]
}
]
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
const { toot, source, target } = queryKey[1]
export const TRANSLATE_SERVER = __DEV__
? 'testtranslate.tooot.app'
: 'translate.tooot.app'
const res = await apiGeneral<Translate.Translate>({
domain: 'translate.tooot.app',
method: 'post',
url: 'translate',
params: {
api_key: Constants.manifest?.extra?.translateKey,
q: toot,
source,
target
const queryFunction = async ({ queryKey }: { queryKey: QueryKeyTranslate }) => {
const key = Constants.manifest.extra?.translateKey
if (!key) {
return Promise.reject()
}
const { instance, id, source, target, text } = queryKey[1]
const res = await apiGeneral<Translations>({
domain: TRANSLATE_SERVER,
method: 'get',
url: `v1/translate/${instance}/${id}/${target}`,
headers: {
key,
original: Buffer.from(JSON.stringify({ source, text })).toString('base64')
}
})
return res.body.translatedText
return res.body
}
const useTranslateQuery = ({
options,
...queryKeyParams
}: QueryKeyTranslate[1] & {
options?: UseQueryOptions<
Translate.Translate['translatedText'],
AxiosError,
Translate.Translate['translatedText']
>
options?: UseQueryOptions<Translations, AxiosError, Translations>
}) => {
const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)