mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Fixed #529
This commit is contained in:
		| @@ -1,2 +1,3 @@ | |||||||
| Enjoy toooting! This version includes following improvements and fixes: | Enjoy toooting! This version includes following improvements and fixes: | ||||||
|  | - Automatic setting detected language when tooting | ||||||
| - Fix whole word filter matching | - Fix whole word filter matching | ||||||
| @@ -1,2 +1,3 @@ | |||||||
| toooting愉快!此版本包括以下改进和修复: | toooting愉快!此版本包括以下改进和修复: | ||||||
|  | - 自动识别发嘟语言 | ||||||
| - 修复过滤整词功能 | - 修复过滤整词功能 | ||||||
| @@ -301,7 +301,7 @@ PODS: | |||||||
|     - React-Core |     - React-Core | ||||||
|   - react-native-ios-context-menu (1.15.1): |   - react-native-ios-context-menu (1.15.1): | ||||||
|     - React-Core |     - React-Core | ||||||
|   - react-native-language-detection (0.1.0): |   - react-native-language-detection (0.2.2): | ||||||
|     - React |     - React | ||||||
|   - react-native-live-text-image-view (0.4.0): |   - react-native-live-text-image-view (0.4.0): | ||||||
|     - React-Core |     - React-Core | ||||||
| @@ -739,7 +739,7 @@ SPEC CHECKSUMS: | |||||||
|   react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8 |   react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8 | ||||||
|   react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea |   react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea | ||||||
|   react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac |   react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac | ||||||
|   react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d |   react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0 | ||||||
|   react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c |   react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c | ||||||
|   react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 |   react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 | ||||||
|   react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 |   react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ | |||||||
|     "react-native-htmlview": "^0.16.0", |     "react-native-htmlview": "^0.16.0", | ||||||
|     "react-native-image-picker": "^4.10.2", |     "react-native-image-picker": "^4.10.2", | ||||||
|     "react-native-ios-context-menu": "^1.15.1", |     "react-native-ios-context-menu": "^1.15.1", | ||||||
|     "react-native-language-detection": "^0.1.0", |     "react-native-language-detection": "^0.2.2", | ||||||
|     "react-native-live-text-image-view": "^0.4.0", |     "react-native-live-text-image-view": "^0.4.0", | ||||||
|     "react-native-pager-view": "^6.1.2", |     "react-native-pager-view": "^6.1.2", | ||||||
|     "react-native-reanimated": "^2.13.0", |     "react-native-reanimated": "^2.13.0", | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { ParseHTML } from '@components/Parse' | import { ParseHTML } from '@components/Parse' | ||||||
| import CustomText from '@components/Text' | import CustomText from '@components/Text' | ||||||
|  | import detectLanguage from '@helpers/detectLanguage' | ||||||
| import getLanguage from '@helpers/getLanguage' | import getLanguage from '@helpers/getLanguage' | ||||||
| import { useTranslateQuery } from '@utils/queryHooks/translate' | import { useTranslateQuery } from '@utils/queryHooks/translate' | ||||||
| import { StyleConstants } from '@utils/styles/constants' | import { StyleConstants } from '@utils/styles/constants' | ||||||
| @@ -7,38 +8,44 @@ import { useTheme } from '@utils/styles/ThemeManager' | |||||||
| import * as Localization from 'expo-localization' | import * as Localization from 'expo-localization' | ||||||
| import React, { useContext, useEffect, useState } from 'react' | import React, { useContext, useEffect, useState } from 'react' | ||||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||||
| import { Pressable } from 'react-native' | import { Platform, Pressable } from 'react-native' | ||||||
| import { Circle } from 'react-native-animated-spinkit' | import { Circle } from 'react-native-animated-spinkit' | ||||||
| import detectLanguage from 'react-native-language-detection' |  | ||||||
| import StatusContext from './Context' | import StatusContext from './Context' | ||||||
|  |  | ||||||
| const TimelineTranslate = () => { | const TimelineTranslate = () => { | ||||||
|   const { status, highlighted } = useContext(StatusContext) |   const { status, highlighted, copiableContent } = useContext(StatusContext) | ||||||
|   if (!status || !highlighted) return null |   if (!status || !highlighted) return null | ||||||
|  |  | ||||||
|   const { t } = useTranslation('componentTimeline') |   const { t } = useTranslation('componentTimeline') | ||||||
|   const { colors } = useTheme() |   const { colors } = useTheme() | ||||||
|  |  | ||||||
|   const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content] |   const backupTextProcessing = (): string[] => { | ||||||
|  |     const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content] | ||||||
|  |  | ||||||
|   for (const i in text) { |     for (const i in text) { | ||||||
|     for (const emoji of status.emojis) { |       for (const emoji of status.emojis) { | ||||||
|       text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ') |         text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ') | ||||||
|  |       } | ||||||
|  |       text[i] = text[i] | ||||||
|  |         .replace(/(<([^>]+)>)/gi, ' ') | ||||||
|  |         .replace(/@.*? /gi, ' ') | ||||||
|  |         .replace(/#.*? /gi, ' ') | ||||||
|  |         .replace(/http(s):\/\/.*? /gi, ' ') | ||||||
|     } |     } | ||||||
|     text[i] = text[i] |     return text | ||||||
|       .replace(/(<([^>]+)>)/gi, ' ') |  | ||||||
|       .replace(/@.*? /gi, ' ') |  | ||||||
|       .replace(/#.*? /gi, ' ') |  | ||||||
|       .replace(/http(s):\/\/.*? /gi, ' ') |  | ||||||
|   } |   } | ||||||
|  |   const text = copiableContent?.current.content | ||||||
|  |     ? [copiableContent?.current.content] | ||||||
|  |     : backupTextProcessing() | ||||||
|  |  | ||||||
|   const [detectedLanguage, setDetectedLanguage] = useState<string>('') |   const [detectedLanguage, setDetectedLanguage] = useState<{ | ||||||
|  |     language: string | ||||||
|  |     confidence: number | ||||||
|  |   }>({ language: status.language || '', confidence: 0 }) | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const detect = async () => { |     const detect = async () => { | ||||||
|       const result = await detectLanguage(text.join(`\n\n`)).catch(() => { |       const result = await detectLanguage(text.join('\n\n')) | ||||||
|         // No need to log language detection failure |       result && setDetectedLanguage(result) | ||||||
|       }) |  | ||||||
|       result?.detected && setDetectedLanguage(result.detected.slice(0, 2)) |  | ||||||
|     } |     } | ||||||
|     detect() |     detect() | ||||||
|   }, []) |   }, []) | ||||||
| @@ -50,20 +57,36 @@ const TimelineTranslate = () => { | |||||||
|  |  | ||||||
|   const [enabled, setEnabled] = useState(false) |   const [enabled, setEnabled] = useState(false) | ||||||
|   const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ |   const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({ | ||||||
|     source: detectedLanguage, |     source: detectedLanguage.language, | ||||||
|     target: targetLanguage, |     target: targetLanguage, | ||||||
|     text, |     text, | ||||||
|     options: { enabled } |     options: { enabled } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   const devView = () => { | ||||||
|  |     return __DEV__ ? ( | ||||||
|  |       <CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${ | ||||||
|  |         detectedLanguage?.language | ||||||
|  |       }; Confidence: ${ | ||||||
|  |         detectedLanguage?.confidence.toString().slice(0, 5) || 'null' | ||||||
|  |       }; Target: ${targetLanguage}`}</CustomText> | ||||||
|  |     ) : null | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (!detectedLanguage) { |   if (!detectedLanguage) { | ||||||
|     return null |     return devView() | ||||||
|   } |   } | ||||||
|   if (Localization.locale.slice(0, 2).includes(detectedLanguage)) { |   if ( | ||||||
|     return null |     Platform.OS === 'ios' && | ||||||
|  |     Localization.locale.slice(0, 2).includes(detectedLanguage.language.slice(0, 2)) | ||||||
|  |   ) { | ||||||
|  |     return devView() | ||||||
|   } |   } | ||||||
|   if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) { |   if ( | ||||||
|     return null |     Platform.OS === 'android' && | ||||||
|  |     settingsLanguage?.slice(0, 2).includes(detectedLanguage.language.slice(0, 2)) | ||||||
|  |   ) { | ||||||
|  |     return devView() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -102,9 +125,6 @@ const TimelineTranslate = () => { | |||||||
|                 }) |                 }) | ||||||
|             : t('shared.translate.default')} |             : t('shared.translate.default')} | ||||||
|         </CustomText> |         </CustomText> | ||||||
|         <CustomText> |  | ||||||
|           {__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined} |  | ||||||
|         </CustomText> |  | ||||||
|         {isLoading ? ( |         {isLoading ? ( | ||||||
|           <Circle |           <Circle | ||||||
|             size={StyleConstants.Font.Size.M} |             size={StyleConstants.Font.Size.M} | ||||||
| @@ -113,6 +133,7 @@ const TimelineTranslate = () => { | |||||||
|           /> |           /> | ||||||
|         ) : null} |         ) : null} | ||||||
|       </Pressable> |       </Pressable> | ||||||
|  |       {devView()} | ||||||
|       {data && data.error === undefined |       {data && data.error === undefined | ||||||
|         ? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />) |         ? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />) | ||||||
|         : null} |         : null} | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								src/helpers/detectLanguage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/helpers/detectLanguage.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | import detect from 'react-native-language-detection' | ||||||
|  |  | ||||||
|  | const detectLanguage = async ( | ||||||
|  |   text: string | ||||||
|  | ): Promise<{ language: string; confidence: number } | null> => { | ||||||
|  |   const possibleLanguages = await detect(text).catch(() => {}) | ||||||
|  |   return possibleLanguages ? possibleLanguages.filter(lang => lang.confidence > 0.5)?.[0] : null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default detectLanguage | ||||||
| @@ -6,7 +6,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' | |||||||
| import haptics from '@root/components/haptics' | import haptics from '@root/components/haptics' | ||||||
| import { useAppDispatch } from '@root/store' | import { useAppDispatch } from '@root/store' | ||||||
| import ComposeRoot from '@screens/Compose/Root' | import ComposeRoot from '@screens/Compose/Root' | ||||||
| import formatText from '@screens/Compose/utils/formatText' | import { formatText } from '@screens/Compose/utils/processText' | ||||||
| import { RootStackScreenProps } from '@utils/navigation/navigators' | import { RootStackScreenProps } from '@utils/navigation/navigators' | ||||||
| import { useTimelineMutation } from '@utils/queryHooks/timeline' | import { useTimelineMutation } from '@utils/queryHooks/timeline' | ||||||
| import { updateStoreReview } from '@utils/slices/contextsSlice' | import { updateStoreReview } from '@utils/slices/contextsSlice' | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import { PanGestureHandler } from 'react-native-gesture-handler' | |||||||
| import { SwipeListView } from 'react-native-swipe-list-view' | import { SwipeListView } from 'react-native-swipe-list-view' | ||||||
| import { useSelector } from 'react-redux' | import { useSelector } from 'react-redux' | ||||||
| import ComposeContext from '../utils/createContext' | import ComposeContext from '../utils/createContext' | ||||||
| import formatText from '../utils/formatText' | import { formatText } from '../utils/processText' | ||||||
| import { ComposeStateDraft, ExtendedAttachment } from '../utils/types' | import { ComposeStateDraft, ExtendedAttachment } from '../utils/types' | ||||||
|  |  | ||||||
| export interface Props { | export interface Props { | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' | |||||||
| import { TextInput } from 'react-native' | import { TextInput } from 'react-native' | ||||||
| import { useSelector } from 'react-redux' | import { useSelector } from 'react-redux' | ||||||
| import ComposeContext from '../../utils/createContext' | import ComposeContext from '../../utils/createContext' | ||||||
| import formatText from '../../utils/formatText' | import { formatText } from '../../utils/processText' | ||||||
|  |  | ||||||
| const ComposeSpoilerInput: React.FC = () => { | const ComposeSpoilerInput: React.FC = () => { | ||||||
|   const { composeState, composeDispatch } = useContext(ComposeContext) |   const { composeState, composeDispatch } = useContext(ComposeContext) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next' | |||||||
| import { Alert } from 'react-native' | import { Alert } from 'react-native' | ||||||
| import { useSelector } from 'react-redux' | import { useSelector } from 'react-redux' | ||||||
| import ComposeContext from '../../utils/createContext' | import ComposeContext from '../../utils/createContext' | ||||||
| import formatText from '../../utils/formatText' | import { formatText } from '../../utils/processText' | ||||||
| import { uploadAttachment } from '../Footer/addAttachment' | import { uploadAttachment } from '../Footer/addAttachment' | ||||||
|  |  | ||||||
| const ComposeTextInput: React.FC = () => { | const ComposeTextInput: React.FC = () => { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import haptics from '@components/haptics' | |||||||
| import ComponentHashtag from '@components/Hashtag' | import ComponentHashtag from '@components/Hashtag' | ||||||
| import React, { useContext, useEffect } from 'react' | import React, { useContext, useEffect } from 'react' | ||||||
| import ComposeContext from '../utils/createContext' | import ComposeContext from '../utils/createContext' | ||||||
| import formatText from '../utils/formatText' | import { formatText } from '../utils/processText' | ||||||
|  |  | ||||||
| type Props = { item: Mastodon.Account & Mastodon.Tag } | type Props = { item: Mastodon.Account & Mastodon.Tag } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| import apiInstance, { InstanceResponse } from '@api/instance' | import apiInstance, { InstanceResponse } from '@api/instance' | ||||||
|  | import detectLanguage from '@helpers/detectLanguage' | ||||||
| import { ComposeState } from '@screens/Compose/utils/types' | import { ComposeState } from '@screens/Compose/utils/types' | ||||||
| import { RootStackParamList } from '@utils/navigation/navigators' | import { RootStackParamList } from '@utils/navigation/navigators' | ||||||
| import * as Crypto from 'expo-crypto' | import * as Crypto from 'expo-crypto' | ||||||
|  | import { getPureContent } from './processText' | ||||||
|  |  | ||||||
| const composePost = async ( | const composePost = async ( | ||||||
|   params: RootStackParamList['Screen-Compose'], |   params: RootStackParamList['Screen-Compose'], | ||||||
| @@ -9,6 +11,13 @@ const composePost = async ( | |||||||
| ): Promise<InstanceResponse<Mastodon.Status>> => { | ): Promise<InstanceResponse<Mastodon.Status>> => { | ||||||
|   const formData = new FormData() |   const formData = new FormData() | ||||||
|  |  | ||||||
|  |   const detectedLanguage = await detectLanguage( | ||||||
|  |     getPureContent([composeState.spoiler.raw, composeState.text.raw].join('\n\n')) | ||||||
|  |   ) | ||||||
|  |   if (detectedLanguage) { | ||||||
|  |     formData.append('language', detectedLanguage.language) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (composeState.replyToStatus) { |   if (composeState.replyToStatus) { | ||||||
|     try { |     try { | ||||||
|       await apiInstance<Mastodon.Status>({ |       await apiInstance<Mastodon.Status>({ | ||||||
|   | |||||||
| @@ -150,7 +150,7 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal | |||||||
|   }) |   }) | ||||||
|   children.push(_content) |   children.push(_content) | ||||||
|   contentLength = contentLength + _content.length |   contentLength = contentLength + _content.length | ||||||
| 
 |   getPureContent(content) | ||||||
|   composeDispatch({ |   composeDispatch({ | ||||||
|     type: textInput, |     type: textInput, | ||||||
|     payload: { |     payload: { | ||||||
| @@ -161,4 +161,18 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default formatText | const getPureContent = (content: string): string => { | ||||||
|  |   const tags = linkify.match(content) | ||||||
|  |   if (!tags) { | ||||||
|  |     return content | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let _content = content | ||||||
|  |   for (const tag of tags) { | ||||||
|  |     _content = _content.replace(tag.raw, '') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return _content.replace(/\s\s+/g, ' ') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { formatText, getPureContent } | ||||||
| @@ -10853,10 +10853,10 @@ react-native-iphone-x-helper@^1.3.1: | |||||||
|   resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" |   resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" | ||||||
|   integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== |   integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== | ||||||
|  |  | ||||||
| react-native-language-detection@^0.1.0: | react-native-language-detection@^0.2.2: | ||||||
|   version "0.1.0" |   version "0.2.2" | ||||||
|   resolved "https://registry.yarnpkg.com/react-native-language-detection/-/react-native-language-detection-0.1.0.tgz#06b5d20bffb60dbbd599c8e62b6acf500952afa8" |   resolved "https://registry.yarnpkg.com/react-native-language-detection/-/react-native-language-detection-0.2.2.tgz#4cc94177aa1c4575c4656f6d42456fa6c72ed5db" | ||||||
|   integrity sha512-26CLndVMmMbVp40Y9Herza73nfR08JFTcYkJ3MX5MIQbGRoqgNAG89z8pA1y7dPHHK1Nfa6AWKAYpNv7tMRCaw== |   integrity sha512-6u1JBgr+UG/GX/xMmT4K8CaBlSep4XfM91jwUzRA/Y3bMCHDx7bNVxGQvqvzkmOchby9h66XD8F5Eo+kV01CAA== | ||||||
|  |  | ||||||
| react-native-live-text-image-view@^0.4.0: | react-native-live-text-image-view@^0.4.0: | ||||||
|   version "0.4.0" |   version "0.4.0" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user