From c59690fcb99ebc52cc323f2e7209c153d8da125b Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 17 Dec 2022 23:21:56 +0100 Subject: [PATCH 01/29] Fixed #566 --- fastlane/metadata/en-US/release_notes.txt | 9 ---- fastlane/metadata/zh-Hans/release_notes.txt | 9 ---- package.json | 2 +- src/components/Timeline/Default.tsx | 2 + src/components/Timeline/Shared/Context.tsx | 1 + src/components/Timeline/Shared/Feedback.tsx | 9 +++- src/components/Timeline/Shared/Translate.tsx | 21 +++++--- src/screens/Tabs/Shared/History.tsx | 52 +++++++++++++++----- src/utils/navigation/navigators.ts | 1 + 9 files changed, 65 insertions(+), 41 deletions(-) diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index ca79b784..862623d2 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,10 +1 @@ Enjoy toooting! This version includes following improvements and fixes: -- Added Ukrainian (Slava Ukraini) -- Automatic setting detected language when tooting -- Remember public timeline type selection -- Show diffing of edit history -- Allow hiding boosts and replies in home timeline -- Support toot in RTL languages -- Added notification for admins -- Fix whole word filter matching -- Fix tablet cannot delete toot drafts diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 35846c68..d9488dcd 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,10 +1 @@ toooting愉快!此版本包括以下改进和修复: -- 增加乌克兰语(Slava Ukraini) -- 自动识别发嘟语言 -- 记住上次公共时间轴选项 -- 显示编辑历史的差异 -- 关注列表可隐藏转嘟和回复 -- 新增管理员推送通知 -- 支持嘟文右到左文字 -- 修复过滤整词功能 -- 修复平板不能删除草稿 diff --git a/package.json b/package.json index b10cfe1c..82ef675d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.7.0", + "version": "4.7.1", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 0284cf07..8d209d14 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -64,6 +64,7 @@ const TimelineDefault: React.FC = ({ content: '', complete: false }) + const detectedLanguage = useRef(status.language || '') const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey }) if (queryKey && filtered && !highlighted) { @@ -139,6 +140,7 @@ const TimelineDefault: React.FC = ({ ownAccount, spoilerHidden, copiableContent, + detectedLanguage, highlighted, inThread: queryKey?.[1].page === 'Toot', disableDetails, diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index f00668d1..218379d1 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -14,6 +14,7 @@ type ContextType = { content: string complete: boolean }> + detectedLanguage?: React.MutableRefObject highlighted?: boolean inThread?: boolean diff --git a/src/components/Timeline/Shared/Feedback.tsx b/src/components/Timeline/Shared/Feedback.tsx index 40d647e7..ec5bd793 100644 --- a/src/components/Timeline/Shared/Feedback.tsx +++ b/src/components/Timeline/Shared/Feedback.tsx @@ -11,7 +11,7 @@ import { StyleSheet, View } from 'react-native' import StatusContext from './Context' const TimelineFeedback = () => { - const { status, highlighted } = useContext(StatusContext) + const { status, highlighted, detectedLanguage } = useContext(StatusContext) if (!status || !highlighted) return null const { t } = useTranslation('componentTimeline') @@ -80,7 +80,12 @@ const TimelineFeedback = () => { accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')} accessibilityRole='button' style={[styles.text, { marginRight: 0, color: colors.blue }]} - onPress={() => navigation.push('Tab-Shared-History', { id: status.id })} + onPress={() => + navigation.push('Tab-Shared-History', { + id: status.id, + detectedLanguage: detectedLanguage?.current || status.language || '' + }) + } > {t('shared.actionsUsers.history.text', { count: data.length - 1 diff --git a/src/components/Timeline/Shared/Translate.tsx b/src/components/Timeline/Shared/Translate.tsx index d55369dd..f8c62192 100644 --- a/src/components/Timeline/Shared/Translate.tsx +++ b/src/components/Timeline/Shared/Translate.tsx @@ -13,7 +13,7 @@ import { Circle } from 'react-native-animated-spinkit' import StatusContext from './Context' const TimelineTranslate = () => { - const { status, highlighted, copiableContent } = useContext(StatusContext) + const { status, highlighted, copiableContent, detectedLanguage } = useContext(StatusContext) if (!status || !highlighted) return null const { t } = useTranslation('componentTimeline') @@ -38,14 +38,19 @@ const TimelineTranslate = () => { ? [copiableContent?.current.content] : backupTextProcessing() - const [detectedLanguage, setDetectedLanguage] = useState<{ + const [detected, setDetected] = useState<{ language: string confidence: number }>({ language: status.language || '', confidence: 0 }) useEffect(() => { const detect = async () => { const result = await detectLanguage(text.join('\n\n')) - result && setDetectedLanguage(result) + if (result) { + setDetected(result) + if (detectedLanguage) { + detectedLanguage.current = result.language + } + } } detect() }, []) @@ -57,7 +62,7 @@ const TimelineTranslate = () => { const [enabled, setEnabled] = useState(false) const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({ - source: detectedLanguage.language, + source: detected.language, target: targetLanguage, text, options: { enabled } @@ -66,9 +71,9 @@ const TimelineTranslate = () => { const devView = () => { return __DEV__ ? ( {` Source: ${ - detectedLanguage?.language + detected?.language }; Confidence: ${ - detectedLanguage?.confidence.toString().slice(0, 5) || 'null' + detected?.confidence.toString().slice(0, 5) || 'null' }; Target: ${targetLanguage}`} ) : null } @@ -78,13 +83,13 @@ const TimelineTranslate = () => { } if ( Platform.OS === 'ios' && - Localization.locale.slice(0, 2).includes(detectedLanguage.language.slice(0, 2)) + Localization.locale.slice(0, 2).includes(detected.language.slice(0, 2)) ) { return devView() } if ( Platform.OS === 'android' && - settingsLanguage?.slice(0, 2).includes(detectedLanguage.language.slice(0, 2)) + settingsLanguage?.slice(0, 2).includes(detected.language.slice(0, 2)) ) { return devView() } diff --git a/src/screens/Tabs/Shared/History.tsx b/src/screens/Tabs/Shared/History.tsx index 1c59cfd8..26325ed2 100644 --- a/src/screens/Tabs/Shared/History.tsx +++ b/src/screens/Tabs/Shared/History.tsx @@ -11,25 +11,45 @@ import { TabSharedStackScreenProps } from '@utils/navigation/navigators' import { useStatusHistory } from '@utils/queryHooks/statusesHistory' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import { diffWords } from 'diff' +import { diffChars, diffWords } from 'diff' import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, View } from 'react-native' +const SCRIPTS_WITHOUT_BOUNDARIES = [ + 'my', + 'zh', + 'ja', + 'kar', + 'km', + 'lp', + 'phag', + 'pwo', + 'kar', + 'lana', + 'th', + 'bo' +] + const ContentView: React.FC<{ + withoutBoundary: boolean item: Mastodon.StatusHistory prevItem?: Mastodon.StatusHistory -}> = ({ item, prevItem }) => { +}> = ({ withoutBoundary, item, prevItem }) => { const { colors } = useTheme() - const changesSpoiler = diffWords( - removeHTML(prevItem?.spoiler_text || item.spoiler_text || ''), - removeHTML(item.spoiler_text || '') - ) - const changesContent = diffWords( - removeHTML(prevItem?.content || item.content), - removeHTML(item.content) - ) + const changesSpoiler = withoutBoundary + ? diffChars( + removeHTML(prevItem?.spoiler_text || item.spoiler_text || ''), + removeHTML(item.spoiler_text || '') + ) + : diffWords( + removeHTML(prevItem?.spoiler_text || item.spoiler_text || ''), + removeHTML(item.spoiler_text || '') + ) + const changesContent = withoutBoundary + ? diffChars(removeHTML(prevItem?.content || item.content), removeHTML(item.content)) + : diffWords(removeHTML(prevItem?.content || item.content), removeHTML(item.content)) return ( // @ts-ignore @@ -91,7 +111,7 @@ const ContentView: React.FC<{ const TabSharedHistory: React.FC> = ({ navigation, route: { - params: { id } + params: { id, detectedLanguage } } }) => { const { t } = useTranslation('screenTabs') @@ -106,12 +126,20 @@ const TabSharedHistory: React.FC const dataReversed = data ? [...data].reverse() : [] + const withoutBoundary = !!SCRIPTS_WITHOUT_BOUNDARIES.filter(script => + detectedLanguage?.toLocaleLowerCase().startsWith(script) + ).length + return ( ( - + )} ItemSeparatorComponent={ComponentSeparator} /> diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index 0e5d2822..866cea67 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -103,6 +103,7 @@ export type TabSharedStackParamList = { } 'Tab-Shared-History': { id: Mastodon.Status['id'] + detectedLanguage: string } 'Tab-Shared-Search': undefined 'Tab-Shared-Toot': { From ef80ab895e7112c17b7a09316df73ba805734d25 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 17 Dec 2022 23:31:46 +0100 Subject: [PATCH 02/29] Remove min height of cards --- src/components/Timeline/Shared/Card.tsx | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx index 412bd22a..7951057d 100644 --- a/src/components/Timeline/Shared/Card.tsx +++ b/src/components/Timeline/Shared/Card.tsx @@ -145,19 +145,21 @@ const TimelineCard: React.FC = () => { /> ) : null} - - {status.card?.title} - - {status.card?.description ? ( + {status.card?.title.length ? ( + + {status.card.title} + + ) : null} + {status.card?.description.length ? ( { {status.card.description} ) : null} - - {status.card?.url} - + {status.card?.url.length ? ( + + {status.card.url} + + ) : null} ) @@ -187,10 +191,6 @@ const TimelineCard: React.FC = () => { style={{ flex: 1, flexDirection: 'row', - minHeight: - (isStatus && foundStatus) || (isAccount && foundAccount) - ? undefined - : StyleConstants.Font.LineHeight.M * 5, marginTop: StyleConstants.Spacing.M, borderWidth: StyleSheet.hairlineWidth, borderRadius: StyleConstants.Spacing.S, From a5315501fd6b25eab616b1775e7f7474709a1f2d Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 18 Dec 2022 00:00:58 +0100 Subject: [PATCH 03/29] Fixed #565 --- src/components/Instance/index.tsx | 107 +++++++++++++++++++-------- src/i18n/en/components/instance.json | 1 + 2 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/components/Instance/index.tsx b/src/components/Instance/index.tsx index eb7fbddc..f0fe1b3a 100644 --- a/src/components/Instance/index.tsx +++ b/src/components/Instance/index.tsx @@ -15,6 +15,7 @@ import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'r import { ScrollView } from 'react-native-gesture-handler' import { useSelector } from 'react-redux' import { Placeholder } from 'rn-placeholder' +import validUrl from 'valid-url' import InstanceInfo from './Info' import CustomText from '../Text' import { useNavigation } from '@react-navigation/native' @@ -39,12 +40,26 @@ const ComponentInstance: React.FC = ({ const navigation = useNavigation>() const [domain, setDomain] = useState('') + const [errorCode, setErrorCode] = useState(null) + const whitelisted: boolean = + !!domain.length && + !!errorCode && + !!validUrl.isHttpsUri(`https://${domain}`) && + errorCode === 401 const dispatch = useAppDispatch() const instances = useSelector(getInstances, () => true) const instanceQuery = useInstanceQuery({ domain, - options: { enabled: !!domain, retry: false } + options: { + enabled: !!domain, + retry: false, + onError: err => { + if (err.status) { + setErrorCode(err.status) + } + } + } }) const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow')) @@ -146,7 +161,11 @@ const ComponentInstance: React.FC = ({ borderBottomWidth: 1, ...StyleConstants.FontStyle.M, color: colors.primaryDefault, - borderBottomColor: instanceQuery.isError ? colors.red : colors.border, + borderBottomColor: instanceQuery.isError + ? whitelisted + ? colors.yellow + : colors.red + : colors.border, ...(Platform.OS === 'android' && { paddingRight: 0 }) }} editable={false} @@ -159,12 +178,23 @@ const ComponentInstance: React.FC = ({ ...StyleConstants.FontStyle.M, marginRight: StyleConstants.Spacing.M, color: colors.primaryDefault, - borderBottomColor: instanceQuery.isError ? colors.red : colors.border, + borderBottomColor: instanceQuery.isError + ? whitelisted + ? colors.yellow + : colors.red + : colors.border, ...(Platform.OS === 'android' && { paddingLeft: 0 }) }} - onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, { - trailing: true - })} + onChangeText={debounce( + text => { + setDomain(text.replace(/^http(s)?\:\/\//i, '')) + setErrorCode(null) + }, + 1000, + { + trailing: true + } + )} autoCapitalize='none' clearButtonMode='never' keyboardType='url' @@ -194,39 +224,52 @@ const ComponentInstance: React.FC = ({ type='text' content={t('server.button')} onPress={processUpdate} - disabled={!instanceQuery.data?.uri} + disabled={!instanceQuery.data?.uri && !whitelisted} loading={instanceQuery.isFetching || appsMutation.isLoading} /> - - - + {whitelisted ? ( + + {t('server.whitelisted')} + + ) : ( + - - - - + + + + + + + )} Date: Sun, 18 Dec 2022 00:41:49 +0100 Subject: [PATCH 04/29] Make search transition smoother? --- src/screens/Tabs/Shared/Search/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/screens/Tabs/Shared/Search/index.tsx b/src/screens/Tabs/Shared/Search/index.tsx index 9c70098b..f31a3e14 100644 --- a/src/screens/Tabs/Shared/Search/index.tsx +++ b/src/screens/Tabs/Shared/Search/index.tsx @@ -49,7 +49,6 @@ const TabSharedSearch: React.FC> > borderBottomColor: colors.border, borderBottomWidth: 1 }} - autoFocus onChangeText={debounce( text => { setSearchTerm(text) @@ -82,6 +80,13 @@ const TabSharedSearch: React.FC> } }) }, [mode]) + useEffect(() => { + const unsubscribe = navigation.addListener('transitionEnd', e => { + inputRef.current?.focus() + }) + + return unsubscribe + }, [navigation]) const mapKeyToTranslations = { accounts: t('shared.search.sections.accounts'), From fea2e82bdd195433ab71d04850255e58aaf7a633 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sun, 18 Dec 2022 01:12:58 +0100 Subject: [PATCH 05/29] Fixed #568 --- src/@types/mastodon.d.ts | 2 + src/components/Relationship/Outgoing.tsx | 175 +++++++++++++---------- src/helpers/features.json | 4 + src/utils/queryHooks/relationship.ts | 45 +++--- 4 files changed, 129 insertions(+), 97 deletions(-) diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 699cbd16..fc61ed10 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -406,6 +406,8 @@ declare namespace Mastodon { id: string following: boolean showing_reblogs: boolean + notifying?: boolean + languages?: string[] followed_by: boolean blocking: boolean blocked_by: boolean diff --git a/src/components/Relationship/Outgoing.tsx b/src/components/Relationship/Outgoing.tsx index 70a643e0..fc082097 100644 --- a/src/components/Relationship/Outgoing.tsx +++ b/src/components/Relationship/Outgoing.tsx @@ -6,120 +6,144 @@ import { useRelationshipMutation, useRelationshipQuery } from '@utils/queryHooks/relationship' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' import { useQueryClient } from '@tanstack/react-query' +import { useSelector } from 'react-redux' +import { checkInstanceFeature } from '@utils/slices/instancesSlice' +import { StyleConstants } from '@utils/styles/constants' export interface Props { id: Mastodon.Account['id'] } -const RelationshipOutgoing = React.memo( - ({ id }: Props) => { - const { theme } = useTheme() - const { t } = useTranslation('componentRelationship') +const RelationshipOutgoing: React.FC = ({ id }: Props) => { + const { theme } = useTheme() + const { t } = useTranslation('componentRelationship') - const query = useRelationshipQuery({ id }) + const canFollowNotify = useSelector(checkInstanceFeature('account_follow_notify')) - const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] - const queryClient = useQueryClient() - const mutation = useRelationshipMutation({ - onSuccess: (res, { payload: { action } }) => { - haptics('Success') - queryClient.setQueryData(queryKeyRelationship, [res]) - if (action === 'block') { - const queryKey = ['Timeline', { page: 'Following' }] - queryClient.invalidateQueries({ queryKey, exact: false }) - } - }, - onError: (err: any, { payload: { action } }) => { - displayMessage({ - theme, - type: 'error', - message: t('common:message.error.message', { - function: t(`${action}.function`) - }), - ...(err.status && - typeof err.status === 'number' && - err.data && - err.data.error && - typeof err.data.error === 'string' && { - description: err.data.error - }) - }) + const query = useRelationshipQuery({ id }) + + const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] + const queryClient = useQueryClient() + const mutation = useRelationshipMutation({ + onSuccess: (res, { payload: { action } }) => { + haptics('Success') + queryClient.setQueryData(queryKeyRelationship, [res]) + if (action === 'block') { + const queryKey = ['Timeline', { page: 'Following' }] + queryClient.invalidateQueries({ queryKey, exact: false }) } - }) + }, + onError: (err: any, { payload: { action } }) => { + displayMessage({ + theme, + type: 'error', + message: t('common:message.error.message', { + function: t(`${action}.function`) + }), + ...(err.status && + typeof err.status === 'number' && + err.data && + err.data.error && + typeof err.data.error === 'string' && { + description: err.data.error + }) + }) + } + }) - let content: string - let onPress: () => void + let content: string + let onPress: () => void - if (query.isError) { - content = t('button.error') + if (query.isError) { + content = t('button.error') + onPress = () => {} + } else { + if (query.data?.blocked_by) { + content = t('button.blocked_by') onPress = () => {} } else { - if (query.data?.blocked_by) { - content = t('button.blocked_by') - onPress = () => {} + if (query.data?.blocking) { + content = t('button.blocking') + onPress = () => { + mutation.mutate({ + id, + type: 'outgoing', + payload: { + action: 'block', + state: query.data?.blocking + } + }) + } } else { - if (query.data?.blocking) { - content = t('button.blocking') + if (query.data?.following) { + content = t('button.following') onPress = () => { mutation.mutate({ id, type: 'outgoing', payload: { - action: 'block', - state: query.data?.blocking + action: 'follow', + state: query.data?.following } }) } } else { - if (query.data?.following) { - content = t('button.following') + if (query.data?.requested) { + content = t('button.requested') onPress = () => { mutation.mutate({ id, type: 'outgoing', payload: { action: 'follow', - state: query.data?.following + state: query.data?.requested } }) } } else { - if (query.data?.requested) { - content = t('button.requested') - onPress = () => { - mutation.mutate({ - id, - type: 'outgoing', - payload: { - action: 'follow', - state: query.data?.requested - } - }) - } - } else { - content = t('button.default') - onPress = () => { - mutation.mutate({ - id, - type: 'outgoing', - payload: { - action: 'follow', - state: false - } - }) - } + content = t('button.default') + onPress = () => { + mutation.mutate({ + id, + type: 'outgoing', + payload: { + action: 'follow', + state: false + } + }) } } } } } + } - return ( + return ( + <> + {canFollowNotify && query.data?.following ? ( +