diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt new file mode 120000 index 00000000..5305c810 --- /dev/null +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -0,0 +1 @@ +../../it/description.txt \ No newline at end of file diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt new file mode 120000 index 00000000..20fae545 --- /dev/null +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -0,0 +1 @@ +../../it/subtitle.txt \ No newline at end of file diff --git a/fastlane/metadata/en-US/description.txt b/fastlane/metadata/en-US/description.txt index 9238b9bb..0e3deca8 100644 --- a/fastlane/metadata/en-US/description.txt +++ b/fastlane/metadata/en-US/description.txt @@ -1,5 +1,10 @@ -tooot is an open source, simple yet elegant Mastodon mobile client. +tooot is an open source, simple yet elegant Mastodon mobile client. A Mastodon (https://joinmastodon.org/) account is required to use this app. -A Mastodon (https://joinmastodon.org/) account is required to use this app. +tooot supports: +- Cross platform, including iPadOS and MacOS +- Multiple accounts +- Dark mode or adapt to system +- Adjustable toot font size +- Push notification -If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.ap. \ No newline at end of file +If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.app. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index ca79b784..699973d8 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,10 +1,5 @@ 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 +- Align filter experience with v4.0 and above +- Supports enlarging user's avatar and banner +- Fix iPad weird sizing (not optimisation) +- Experiment (!) support of Pleroma diff --git a/fastlane/metadata/it/description.txt b/fastlane/metadata/it/description.txt new file mode 100644 index 00000000..a001cce3 --- /dev/null +++ b/fastlane/metadata/it/description.txt @@ -0,0 +1,10 @@ +tooot è un client Mastodon semplice e open source. Per utilizzare questo client, devi disporre di un account Mastodon. (https://joinmastodon.org/). + +Tooot supporta: +- Multipiattaforma, inclusi iPadOS e MacOS +- Accesso a più account +- Modalità scura o adattiva +- Dimensione del carattere del testo regolabile +- Notifiche push e altre funzioni + +Per suggerimenti o commenti sull'utilizzo, contattare @tooot@xmflsct.com o support@tooot.app. \ No newline at end of file diff --git a/fastlane/metadata/it/subtitle.txt b/fastlane/metadata/it/subtitle.txt new file mode 100644 index 00000000..efd0d0ae --- /dev/null +++ b/fastlane/metadata/it/subtitle.txt @@ -0,0 +1 @@ +Client open source per Mastodon \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/description.txt b/fastlane/metadata/zh-Hans/description.txt index b6a299e0..1f990e77 100644 --- a/fastlane/metadata/zh-Hans/description.txt +++ b/fastlane/metadata/zh-Hans/description.txt @@ -1,11 +1,10 @@ -tooot是一个专门为中文用户社区所打造的开源、简洁长毛象客户端。使用此客户端需要已经拥有一个长毛象(https://joinmastodon.org/)账号。 +tooot起始于专注中文社区的简洁、开源长毛象手机客户端。使用此客户端需要已经拥有一个长毛象(https://joinmastodon.org/)账号。 tooot支持: -- iPad +- 跨平台,及iPadOS、MacOS - 多账号登录 - 黑暗或自适应模式 -- 可调整正文字体大小 +- 可调正文字体尺寸 - 消息推送 -等功能。 如有使用建议或意见,请联系@tooot@xmflsct.com或者support@tooot.app。 \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 35846c68..ee10414f 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,10 +1,5 @@ toooting愉快!此版本包括以下改进和修复: -- 增加乌克兰语(Slava Ukraini) -- 自动识别发嘟语言 -- 记住上次公共时间轴选项 -- 显示编辑历史的差异 -- 关注列表可隐藏转嘟和回复 -- 新增管理员推送通知 -- 支持嘟文右到左文字 -- 修复过滤整词功能 -- 修复平板不能删除草稿 +- 改进过滤体验,与v4.0以上版本一致 +- 支持查看用户的头像和横幅图片 +- 修复iPad部分尺寸问题(非优化) +- 试验性(!)支持Pleroma 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/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 699cbd16..102739ca 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -263,7 +263,8 @@ declare namespace Mastodon { verified_at: string | null } - type Filter = { + type Filter = T extends 'v2' ? Filter_V2 : Filter_V1 + type Filter_V1 = { id: string phrase: string context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] @@ -271,6 +272,25 @@ declare namespace Mastodon { irreversible: boolean whole_word: boolean } + type Filter_V2 = { + id: string + title: string + context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[] + expires_at?: string + filter_action: 'warn' | 'hide' + keywords: FilterKeyword[] + statuses: FilterStatus[] + } + + type FilterKeyword = { id: string; keyword: string; whole_word: boolean } + + type FilterStatus = { id: string; status_id: string } + + type FilterResult = { + filter: Filter<'v2'> + keyword_matches?: FilterKeyword['keyword'][] + status_matches?: FilterStatus['id'][] + } type List = { id: string @@ -406,6 +426,8 @@ declare namespace Mastodon { id: string following: boolean showing_reblogs: boolean + notifying?: boolean + languages?: string[] followed_by: boolean blocking: boolean blocked_by: boolean @@ -459,7 +481,7 @@ declare namespace Mastodon { sensitive: boolean spoiler_text?: string media_attachments: Attachment[] - application: Application + application?: Application // Attributes mentions: Mention[] @@ -470,7 +492,7 @@ declare namespace Mastodon { reblogs_count: number favourites_count: number replies_count: number - edited_at?: string // FEATURE edit_post + edited_at?: string favourited: boolean reblogged: boolean muted: boolean @@ -486,6 +508,7 @@ declare namespace Mastodon { card?: Card language?: string text?: string + filtered?: FilterResult[] } type StatusHistory = { diff --git a/src/Screens.tsx b/src/Screens.tsx index e7430d1f..6febfbde 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -56,7 +56,7 @@ const Screens: React.FC = ({ localCorrupt }) => { useEffect(() => { const screenshotListener = addScreenshotListener(() => Alert.alert(t('screenshot.title'), t('screenshot.message'), [ - { text: t('screenshot.button'), style: 'destructive' } + { text: t('common:buttons.confirm'), style: 'destructive' } ]) ) Platform.select({ ios: screenshotListener }) diff --git a/src/components/Hashtag.tsx b/src/components/Hashtag.tsx index 1402d7ac..90ab7ef7 100644 --- a/src/components/Hashtag.tsx +++ b/src/components/Hashtag.tsx @@ -53,7 +53,7 @@ const ComponentHashtag: React.FC = ({ #{hashtag.name} = ({ }) => setHeight(height)} > parseInt(h.uses)).reverse()} + data={hashtag.history?.map(h => parseInt(h.uses)).reverse()} width={width} height={height} margin={children ? StyleConstants.Spacing.S : undefined} 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')} + + ) : ( + - - - - + + + + + + + )} { - return StyleSheet.create({ - text: { - color: colors.primaryDefault, - fontSize: adaptedFontsize, - lineHeight: adaptedLineheight - }, - image: { - width: adaptedFontsize, - height: adaptedFontsize, - ...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] }) - } - }) - }, [theme, adaptiveFontsize]) return ( - + {emojis ? ( content .split(regexEmoji) @@ -73,7 +69,14 @@ const ParseEmojis = React.memo( return ( {i === 0 ? ' ' : undefined} - + ) } else { diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index e651a66e..4652fa10 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -102,7 +102,7 @@ const renderNode = ({ ) } } else { - const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/)) + const domain = href?.split(new RegExp(/:\/\/(.[^\/]+\/.{3})/)) // Need example here const content = node.children && node.children[0] && node.children[0].data const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0 @@ -128,17 +128,7 @@ const renderNode = ({ }} > {content && content !== href ? content : showFullLink ? href : domain?.[1]} - {!shouldBeTag ? ( - - ) : null} + {!shouldBeTag ? '...' : null} ) } 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 ? ( +