From 0190b35b5796cbd28e56841b6f6d6235bff45901 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 23 May 2021 22:40:42 +0200 Subject: [PATCH] Fixed #119 and translation --- README.md | 4 +- .../app/tooot/generated/BasePackageList.java | 1 + ios/Podfile.lock | 2 +- ios/tooot.xcodeproj/project.pbxproj | 4 +- src/@types/translate.d.ts | 15 --- src/api/general.ts | 6 +- src/components/Menu/Row.tsx | 2 +- src/components/Parse/HTML.tsx | 5 +- src/components/Timeline/Shared/Content.tsx | 3 + src/components/Timeline/Shared/Translate.tsx | 99 ++++++++----------- src/i18n/en/components/timeline.json | 4 +- src/screens/Announcements.tsx | 1 + src/screens/Tabs/Me/Profile/Root.tsx | 45 ++++++++- src/screens/Tabs/Me/Push.tsx | 32 ++++-- .../Shared/Account/Information/Fields.tsx | 2 + .../Tabs/Shared/Account/Information/Note.tsx | 2 +- src/utils/queryHooks/translate.ts | 54 ++++++---- 17 files changed, 168 insertions(+), 113 deletions(-) delete mode 100644 src/@types/translate.d.ts diff --git a/README.md b/README.md index a3292129..6303e61e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java b/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java index 6124677e..b7aecbc5 100644 --- a/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java +++ b/android/app/src/main/java/com/xmflsct/app/tooot/generated/BasePackageList.java @@ -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(), diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a5c95d9c..e52bd82b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -825,7 +825,7 @@ SPEC CHECKSUMS: EXVideoThumbnails: cd257fc6e07884a704a5674d362a6410933acb68 EXWebBrowser: 0b466c50e5ff61c9758095d49d5081e3229d77ac FBLazyVector: 7b423f9e248eae65987838148c36eec1dbfe0b53 - FBReactNativeSpec: be55984d4a593b4ef281ead81139cdfb1812d259 + FBReactNativeSpec: 5058d1917c80dca4b9ed89bdf94385315939ab80 Firebase: cd2ab85eec8170dc260186159f21072ecb679ad5 FirebaseAnalytics: f3f8f75de34fe04141a69bb1c4bd7e24a80178e1 FirebaseCore: ac35d680a0bf32319a59966a1478e0741536b97b diff --git a/ios/tooot.xcodeproj/project.pbxproj b/ios/tooot.xcodeproj/project.pbxproj index 6c0f8b0b..0e2771ea 100644 --- a/ios/tooot.xcodeproj/project.pbxproj +++ b/ios/tooot.xcodeproj/project.pbxproj @@ -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; diff --git a/src/@types/translate.d.ts b/src/@types/translate.d.ts deleted file mode 100644 index 747f3a89..00000000 --- a/src/@types/translate.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare namespace Translate { - type Detect = { - confidence: number - language: string - } - - type Language = { - code: string - name: string - } - - type Translate = { - translatedText: string - } -} diff --git a/src/api/general.ts b/src/api/general.ts index 63393fd2..2761f2b4 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -74,7 +74,11 @@ const apiGeneral = async ({ // 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( diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index ed8a4d35..04040091 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -76,7 +76,7 @@ const MenuRow: React.FC = ({ } }} > - + {iconFront && ( diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index dc42c6d7..5c9f90e5 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -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 ? ( ) : ( @@ -56,6 +58,7 @@ const TimelineContent = React.memo( tags={status.tags} numberOfLines={highlighted ? 999 : numberOfLines} disableDetails={disableDetails} + selectable={highlighted} /> )} diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index e7183e88..6dba7b7f 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -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 ( - - {t('shared.translate.unavailable')} - - ) - } - 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} {isLoading ? ( ) : null} - {data ? ( - - ) : null} + {data + ? data.text.map((d, i) => ( + + )) + : null} ) }, diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index 21582d52..fe7aed9c 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -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": { diff --git a/src/screens/Announcements.tsx b/src/screens/Announcements.tsx index 4b5dc2ad..9a3f8429 100644 --- a/src/screens/Announcements.tsx +++ b/src/screens/Announcements.tsx @@ -87,6 +87,7 @@ const ScreenAnnouncements: React.FC = ({ emojis={item.emojis} mentions={item.mentions} numberOfLines={999} + selectable /> {item.reactions?.length ? ( diff --git a/src/screens/Tabs/Me/Profile/Root.tsx b/src/screens/Tabs/Me/Profile/Root.tsx index 72da7ef7..a4caa510 100644 --- a/src/screens/Tabs/Me/Profile/Root.tsx +++ b/src/screens/Tabs/Me/Profile/Root.tsx @@ -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 { 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 dispatch(updateAccountPreferences())) .catch(err => @@ -70,6 +83,12 @@ const TabMeProfileRoot: React.FC dispatch(updateAccountPreferences())) .catch(err => @@ -87,10 +106,14 @@ const TabMeProfileRoot: React.FC { 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 { 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 displayMessage({ ref: messageRef, @@ -154,6 +189,10 @@ const TabMeProfileRoot: React.FC { 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 displayMessage({ ref: messageRef, diff --git a/src/screens/Tabs/Me/Push.tsx b/src/screens/Tabs/Me/Push.tsx index 7f87f9ba..75e6e26f 100644 --- a/src/screens/Tabs/Me/Push.tsx +++ b/src/screens/Tabs/Me/Push.tsx @@ -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)) - } + }} /> @@ -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)) - } + }} /> + onPress={() => { + analytics('me_push_howitworks') WebBrowser.openBrowserAsync('https://tooot.app/how-push-works') - } + }} /> {alerts} diff --git a/src/screens/Tabs/Shared/Account/Information/Fields.tsx b/src/screens/Tabs/Shared/Account/Information/Fields.tsx index 7bd61127..8d86c645 100644 --- a/src/screens/Tabs/Shared/Account/Information/Fields.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Fields.tsx @@ -34,6 +34,7 @@ const AccountInformationFields = React.memo( emojis={account.emojis} showFullLink numberOfLines={5} + selectable /> {field.verified_at ? ( diff --git a/src/screens/Tabs/Shared/Account/Information/Note.tsx b/src/screens/Tabs/Shared/Account/Information/Note.tsx index 41678740..b87beaaa 100644 --- a/src/screens/Tabs/Shared/Account/Information/Note.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Note.tsx @@ -28,7 +28,7 @@ const AccountInformationNote = React.memo( return ( - + ) }, diff --git a/src/utils/queryHooks/translate.ts b/src/utils/queryHooks/translate.ts index 571a19e0..8d1c81e7 100644 --- a/src/utils/queryHooks/translate.ts +++ b/src/utils/queryHooks/translate.ts @@ -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({ - 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({ + 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 }) => { const queryKey: QueryKeyTranslate = ['Translate', { ...queryKeyParams }] return useQuery(queryKey, queryFunction, options)