From 20e4ef69ea73977943ab29d228a3f44039cc610e Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Thu, 27 May 2021 12:14:49 +0200 Subject: [PATCH 1/6] Ready to release --- fastlane/Deliverfile | 5 ----- fastlane/metadata/en-US/release_notes.txt | 3 ++- fastlane/metadata/zh-Hans/release_notes.txt | 3 ++- src/utils/queryHooks/profile.ts | 2 -- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/fastlane/Deliverfile b/fastlane/Deliverfile index 76ebd1fa..966e0dd0 100644 --- a/fastlane/Deliverfile +++ b/fastlane/Deliverfile @@ -27,9 +27,4 @@ submission_information({ add_id_info_tracks_action: false, add_id_info_tracks_install: false, add_id_info_uses_idfa: true -}) - -release_notes({ - 'zh-Hans' => "添加支持修改账户信息", - 'en-US' => "Added the possibility to update account information" }) \ No newline at end of file diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 8b137891..bd3b8146 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1 +1,2 @@ - +Added translation option, translation service is provided by various providers +When updating profile, now avatar and banner can be uploaded diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 8b137891..74a22c55 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1 +1,2 @@ - +加入翻译嘟文支持,翻译服务由多个服务商提供 +修改个人信息里可以上传头像及横幅 diff --git a/src/utils/queryHooks/profile.ts b/src/utils/queryHooks/profile.ts index a02bb57d..a6b1a1dc 100644 --- a/src/utils/queryHooks/profile.ts +++ b/src/utils/queryHooks/profile.ts @@ -148,8 +148,6 @@ const useProfileMutation = () => { mode: variables.mode, type: 'success' }) - } else { - haptics('Light') } }, onSettled: () => { From 8df45475d8a127bef49b5d53a369b1f12ab088d6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 30 May 2021 15:40:06 +0200 Subject: [PATCH 2/6] Fix bugs --- src/api/general.ts | 5 ++++- src/api/instance.ts | 5 ++++- src/components/Timeline/Shared/Translate.tsx | 2 +- src/startup/netInfo.ts | 6 +----- src/utils/slices/settingsSlice.ts | 8 ++++---- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/api/general.ts b/src/api/general.ts index 2761f2b4..5784c521 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -69,7 +69,10 @@ const apiGeneral = async ({ error.response.status, error.response.data.error ) - return Promise.reject(error.response.data.error) + return Promise.reject({ + status: error.response.status, + message: error.response.data.error + }) } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of diff --git a/src/api/instance.ts b/src/api/instance.ts index af1d7bef..a928f01c 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -98,7 +98,10 @@ const apiInstance = async ({ error.response.status, error.response.data.error ) - return Promise.reject(error.response.data.error) + return Promise.reject({ + status: error.response.status, + message: error.response.data.error + }) } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index d99e6bf1..3745ef04 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -31,7 +31,7 @@ const TimelineTranslate = React.memo( const settingsLanguage = useSelector(getSettingsLanguage) - if (settingsLanguage.includes(tootLanguage)) { + if (settingsLanguage?.includes(tootLanguage)) { return null } diff --git a/src/startup/netInfo.ts b/src/startup/netInfo.ts index 568d0396..8470e550 100644 --- a/src/startup/netInfo.ts +++ b/src/startup/netInfo.ts @@ -43,11 +43,7 @@ const netInfo = async (): Promise<{ }) .catch(error => { log('error', 'netInfo', 'local credential check failed') - if ( - error.status && - typeof error.status === 'number' && - error.status === 401 - ) { + if (error.status && error.status == 401) { store.dispatch(removeInstance(instance)) } return Promise.resolve({ diff --git a/src/utils/slices/settingsSlice.ts b/src/utils/slices/settingsSlice.ts index 2fa84d55..11c88fda 100644 --- a/src/utils/slices/settingsSlice.ts +++ b/src/utils/slices/settingsSlice.ts @@ -4,7 +4,7 @@ import * as Analytics from 'expo-firebase-analytics' import * as Localization from 'expo-localization' import { pickBy } from 'lodash' -enum availableLanguages { +enum AvailableLanguages { 'zh-Hans', 'en' } @@ -19,7 +19,7 @@ export const changeAnalytics = createAsyncThunk( export type SettingsState = { fontsize: -1 | 0 | 1 | 2 | 3 - language: keyof availableLanguages + language: string theme: 'light' | 'dark' | 'auto' browser: 'internal' | 'external' analytics: boolean @@ -31,10 +31,10 @@ export const settingsInitialState = { enabled: false }, language: Object.keys( - pickBy(availableLanguages, (_, key) => Localization.locale.includes(key)) + pickBy(AvailableLanguages, (_, key) => Localization.locale.includes(key)) ) ? Object.keys( - pickBy(availableLanguages, (_, key) => + pickBy(AvailableLanguages, (_, key) => Localization.locale.includes(key) ) )[0] From ecb9ce0c3aadadb547aebad85b99d50e82178346 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 30 May 2021 15:41:55 +0200 Subject: [PATCH 3/6] Fixed #136 --- src/screens/Compose/Root/Header/TextInput.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/screens/Compose/Root/Header/TextInput.tsx b/src/screens/Compose/Root/Header/TextInput.tsx index 8e9d3049..670e134f 100644 --- a/src/screens/Compose/Root/Header/TextInput.tsx +++ b/src/screens/Compose/Root/Header/TextInput.tsx @@ -21,8 +21,6 @@ const ComposeTextInput: React.FC = () => { borderBottomColor: theme.border } ]} - autoCapitalize='none' - autoCorrect={false} autoFocus enablesReturnKeyAutomatically multiline From 8aa84f756879bd664666a3cbcd0ca056905f417b Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 30 May 2021 22:12:22 +0200 Subject: [PATCH 4/6] Improve loading remote images --- src/components/GracefullyImage.tsx | 36 +++++++++---------- src/components/Parse/Emojis.tsx | 9 +++-- src/components/Timeline/Shared/Attachment.tsx | 12 ++++--- .../ImageViewer/components/ImageItem.ios.tsx | 4 +-- src/screens/ImageViewer/save.ts | 4 +-- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx index 62027dfb..2e1257d0 100644 --- a/src/components/GracefullyImage.tsx +++ b/src/components/GracefullyImage.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useMemo, useRef, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { AccessibilityProps, Image, @@ -52,32 +52,30 @@ const GracefullyImage = React.memo( setImageDimensions }: Props) => { const { theme } = useTheme() - const originalFailed = useRef(false) + const [originalFailed, setOriginalFailed] = useState(false) const [imageLoaded, setImageLoaded] = useState(false) const source = useMemo(() => { - if (originalFailed.current) { + if (originalFailed) { return { uri: uri.remote || undefined } } else { return { uri: uri.original } } - }, [originalFailed.current]) - const onLoad = useCallback( - ({ nativeEvent }) => { - setImageLoaded(true) - setImageDimensions && - setImageDimensions({ - width: nativeEvent.source.width, - height: nativeEvent.source.height - }) - }, - [source.uri] - ) - const onError = useCallback(() => { - if (!originalFailed.current) { - originalFailed.current = true + }, [originalFailed]) + + const onLoad = useCallback(() => { + setImageLoaded(true) + if (setImageDimensions && source.uri) { + Image.getSize(source.uri, (width, height) => + setImageDimensions({ width, height }) + ) } - }, [originalFailed.current]) + }, [source.uri]) + const onError = useCallback(() => { + if (!originalFailed) { + setOriginalFailed(true) + } + }, [originalFailed]) const previewView = useMemo( () => diff --git a/src/components/Parse/Emojis.tsx b/src/components/Parse/Emojis.tsx index d98f4ba1..79457eb5 100644 --- a/src/components/Parse/Emojis.tsx +++ b/src/components/Parse/Emojis.tsx @@ -76,11 +76,10 @@ const ParseEmojis = React.memo( : emojis[emojiIndex].url if (validUrl.isHttpsUri(uri)) { return ( - + + {i === 0 ? ' ' : undefined} + + ) } else { return null diff --git a/src/components/Timeline/Shared/Attachment.tsx b/src/components/Timeline/Shared/Attachment.tsx index e6925ae5..3ff78547 100644 --- a/src/components/Timeline/Shared/Attachment.tsx +++ b/src/components/Timeline/Shared/Attachment.tsx @@ -8,7 +8,7 @@ import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video' import { useNavigation } from '@react-navigation/native' import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' -import React, { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, StyleSheet, View } from 'react-native' @@ -33,11 +33,13 @@ const TimelineAttachment = React.memo( haptics('Light') }, []) - let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = [] + const imageUrls = useRef< + Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] + >([]) const navigation = useNavigation() const navigateToImagesViewer = (id: string) => navigation.navigate('Screen-ImagesViewer', { - imageUrls, + imageUrls: imageUrls.current, id }) const attachments = useMemo( @@ -45,7 +47,7 @@ const TimelineAttachment = React.memo( status.media_attachments.map((attachment, index) => { switch (attachment.type) { case 'image': - imageUrls.push({ + imageUrls.current.push({ id: attachment.id, preview_url: attachment.preview_url, url: attachment.url, @@ -106,7 +108,7 @@ const TimelineAttachment = React.memo( attachment.remote_url?.endsWith('.png') || attachment.remote_url?.endsWith('.gif') ) { - imageUrls.push({ + imageUrls.current.push({ id: attachment.id, preview_url: attachment.preview_url, url: attachment.url, diff --git a/src/screens/ImageViewer/components/ImageItem.ios.tsx b/src/screens/ImageViewer/components/ImageItem.ios.tsx index 971888f9..edced941 100644 --- a/src/screens/ImageViewer/components/ImageItem.ios.tsx +++ b/src/screens/ImageViewer/components/ImageItem.ios.tsx @@ -52,8 +52,8 @@ const ImageItem = ({ const scrollViewRef = useRef(null) const [scaled, setScaled] = useState(false) const [imageDimensions, setImageDimensions] = useState({ - width: imageSrc.width || 0, - height: imageSrc.height || 0 + width: imageSrc.width || 1, + height: imageSrc.height || 1 }) const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN) diff --git a/src/screens/ImageViewer/save.ts b/src/screens/ImageViewer/save.ts index 97aaac20..0b98a290 100644 --- a/src/screens/ImageViewer/save.ts +++ b/src/screens/ImageViewer/save.ts @@ -58,7 +58,7 @@ const saveIos = async ({ messageRef, mode, image }: CommonProps) => { } const saveAndroid = async ({ messageRef, mode, image }: CommonProps) => { - const fileUri: string = `${FileSystem.documentDirectory}test.jpg` + const fileUri: string = `${FileSystem.documentDirectory}${image.id}.jpg` const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync( image.url, fileUri @@ -80,7 +80,7 @@ const saveAndroid = async ({ messageRef, mode, image }: CommonProps) => { ref: messageRef, mode, type: 'success', - message: 'test' + message: i18next.t('screenImageViewer:content.save.succeed') }) }) .catch(() => { From a023ad58f1a84fba05b7001ccb7c6b81fbc400e7 Mon Sep 17 00:00:00 2001 From: Zhiyuan Zheng Date: Sun, 30 May 2021 23:39:07 +0200 Subject: [PATCH 5/6] Fixed #118 --- package.json | 2 +- src/@types/mastodon.d.ts | 9 ++ src/Screens.tsx | 2 + src/components/Timeline/Default.tsx | 16 ++- src/components/Timeline/Notifications.tsx | 9 ++ src/components/Timeline/Shared/Filtered.tsx | 105 ++++++++++++++++++++ src/i18n/en/components/timeline.json | 1 + src/utils/slices/instances/updateFilters.ts | 12 +++ src/utils/slices/instancesSlice.ts | 11 ++ 9 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 src/components/Timeline/Shared/Filtered.tsx create mode 100644 src/utils/slices/instances/updateFilters.ts diff --git a/package.json b/package.json index cf04a80d..de994fe4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "native": "210511", "major": 2, "minor": 0, - "patch": 1, + "patch": 2, "expo": "41.0.0" }, "description": "tooot app for Mastodon", diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 5a0e88fa..c0154742 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -261,6 +261,15 @@ declare namespace Mastodon { verified_at: string | null } + type Filter = { + id: string + phrase: string + context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] + expires_at?: string + irreversible: boolean + whole_word: boolean + } + type List = { id: string title: string diff --git a/src/Screens.tsx b/src/Screens.tsx index 5cf8cf5b..955f9a31 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -13,6 +13,7 @@ import pushUseReceive from '@utils/push/useReceive' import pushUseRespond from '@utils/push/useRespond' import { updatePreviousTab } from '@utils/slices/contextsSlice' import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences' +import { updateFilters } from '@utils/slices/instances/updateFilters' import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { themes } from '@utils/styles/themes' @@ -106,6 +107,7 @@ const Screens: React.FC = ({ localCorrupt }) => { // Lazily update users's preferences, for e.g. composing default visibility useEffect(() => { if (instanceActive !== -1) { + dispatch(updateFilters()) dispatch(updateAccountPreferences()) } }, [instanceActive]) diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 89eb759d..a628aee3 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -10,14 +10,16 @@ import TimelinePoll from '@components/Timeline/Shared/Poll' import { useNavigation } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { getInstanceAccount } from '@utils/slices/instancesSlice' +import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' +import htmlparser2 from 'htmlparser2-without-node-native' import { uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, StyleSheet, View } from 'react-native' import { useSelector } from 'react-redux' import TimelineActionsUsers from './Shared/ActionsUsers' +import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' import TimelineTranslate from './Shared/Translate' @@ -49,6 +51,16 @@ const TimelineDefault: React.FC = ({ let actualStatus = item.reblog ? item.reblog : item + const ownAccount = actualStatus.account.id === instanceAccount?.id + + if ( + !highlighted && + queryKey && + shouldFilter({ status: actualStatus, queryKey }) + ) { + return + } + const onPress = useCallback(() => { analytics('timeline_default_press', { page: queryKey ? queryKey[1].page : origin @@ -118,7 +130,7 @@ const TimelineDefault: React.FC = ({ statusId={actualStatus.id} poll={actualStatus.poll} reblog={item.reblog ? true : false} - sameAccount={actualStatus.account.id === instanceAccount?.id} + sameAccount={ownAccount} /> ) : null} {!disableDetails && diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index b458a111..94f1ad70 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -17,6 +17,7 @@ import { uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, StyleSheet, View } from 'react-native' import { useSelector } from 'react-redux' +import TimelineFiltered, { shouldFilter } from './Shared/Filtered' import TimelineFullConversation from './Shared/FullConversation' export interface Props { @@ -30,6 +31,13 @@ const TimelineNotifications: React.FC = ({ queryKey, highlighted = false }) => { + if ( + notification.status && + shouldFilter({ status: notification.status, queryKey }) + ) { + return + } + const { theme } = useTheme() const instanceAccount = useSelector( getInstanceAccount, @@ -38,6 +46,7 @@ const TimelineNotifications: React.FC = ({ const navigation = useNavigation< StackNavigationProp >() + const actualAccount = notification.status ? notification.status.account : notification.account diff --git a/src/components/Timeline/Shared/Filtered.tsx b/src/components/Timeline/Shared/Filtered.tsx new file mode 100644 index 00000000..df71435a --- /dev/null +++ b/src/components/Timeline/Shared/Filtered.tsx @@ -0,0 +1,105 @@ +import { store } from '@root/store' +import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice' +import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' +import htmlparser2 from 'htmlparser2-without-node-native' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Text, View } from 'react-native' + +const TimelineFiltered = React.memo( + () => { + const { theme } = useTheme() + const { t } = useTranslation('componentTimeline') + + return ( + + + {t('shared.filtered')} + + + ) + }, + () => true +) + +export const shouldFilter = ({ + status, + queryKey +}: { + status: Mastodon.Status + queryKey: QueryKeyTimeline +}) => { + const instance = getInstance(store.getState()) + const ownAccount = + getInstanceAccount(store.getState())?.id === status.account.id + + let shouldFilter = false + if (queryKey && !ownAccount) { + const parser = new htmlparser2.Parser({ + ontext (text: string) { + const checkFilter = (filter: Mastodon.Filter) => { + switch (filter.whole_word) { + case true: + if (new RegExp('\\b' + filter.phrase + '\\b').test(text)) { + shouldFilter = true + } + break + case false: + if (new RegExp(filter.phrase).test(text)) { + shouldFilter = true + } + break + } + } + instance?.filters.forEach(filter => { + if (filter.expires_at) { + if (new Date().getTime() > new Date(filter.expires_at).getTime()) { + return + } + } + + switch (queryKey[1].page) { + case 'Following': + case 'Local': + case 'List': + case 'Account_Default': + if (filter.context.includes('home')) { + checkFilter(filter) + } + break + case 'Notifications': + if (filter.context.includes('notifications')) { + checkFilter(filter) + } + break + case 'LocalPublic': + if (filter.context.includes('public')) { + checkFilter(filter) + } + break + case 'Toot': + if (filter.context.includes('thread')) { + checkFilter(filter) + } + } + }) + } + }) + parser.write(status.content) + parser.end() + } + + return shouldFilter +} + +export default TimelineFiltered diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index fe7aed9c..d301f76a 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -73,6 +73,7 @@ "content": { "expandHint": "hidden content" }, + "filtered": "Filtered", "fullConversation": "Read conversations", "translate": { "default": "Translate", diff --git a/src/utils/slices/instances/updateFilters.ts b/src/utils/slices/instances/updateFilters.ts new file mode 100644 index 00000000..5c722ac8 --- /dev/null +++ b/src/utils/slices/instances/updateFilters.ts @@ -0,0 +1,12 @@ +import apiInstance from '@api/instance' +import { createAsyncThunk } from '@reduxjs/toolkit' + +export const updateFilters = createAsyncThunk( + 'instances/updateFilters', + async (): Promise => { + return apiInstance({ + method: 'get', + url: `filters` + }).then(res => res.body) + } +) diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts index 9a52c899..1183184c 100644 --- a/src/utils/slices/instancesSlice.ts +++ b/src/utils/slices/instancesSlice.ts @@ -6,6 +6,7 @@ import { findIndex } from 'lodash' import addInstance from './instances/add' import removeInstance from './instances/remove' import { updateAccountPreferences } from './instances/updateAccountPreferences' +import { updateFilters } from './instances/updateFilters' import { updateInstancePush } from './instances/updatePush' import { updateInstancePushAlert } from './instances/updatePushAlert' import { updateInstancePushDecode } from './instances/updatePushDecode' @@ -29,6 +30,7 @@ export type Instance = { avatarStatic: Mastodon.Account['avatar_static'] preferences: Mastodon.Preferences } + filters: Mastodon.Filter[] notifications_filter: { follow: boolean favourite: boolean @@ -236,6 +238,15 @@ const instancesSlice = createSlice({ console.error(action.error) }) + // Update Instance Account Filters + .addCase(updateFilters.fulfilled, (state, action) => { + const activeIndex = findInstanceActive(state.instances) + state.instances[activeIndex].filters = action.payload + }) + .addCase(updateFilters.rejected, (_, action) => { + console.error(action.error) + }) + // Update Instance Account Preferences .addCase(updateAccountPreferences.fulfilled, (state, action) => { const activeIndex = findInstanceActive(state.instances) From 2e541529f6f4a49a49bcdd895d0ca3f8089eff56 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 30 May 2021 23:41:19 +0200 Subject: [PATCH 6/6] New translations timeline.json (Chinese Simplified) --- src/i18n/zh-Hans/components/timeline.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/zh-Hans/components/timeline.json b/src/i18n/zh-Hans/components/timeline.json index 6a255318..7910bf49 100644 --- a/src/i18n/zh-Hans/components/timeline.json +++ b/src/i18n/zh-Hans/components/timeline.json @@ -73,6 +73,7 @@ "content": { "expandHint": "隐藏内容" }, + "filtered": "已过滤", "fullConversation": "阅读全部对话", "translate": { "default": "翻译",