diff --git a/README.md b/README.md index b1bcf7e3..7d29999d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Please **do not** create a pull request to update translation. tooot's translati [@janlindblom](https://github.com/janlindblom) for Swedish +[@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian + [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation [@jimmyorz](https://github.com/jimmyorz) for Traditional Chinese translation diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 6ea61e2c..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 🇺🇦 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 \ 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 4b29b0a7..d9488dcd 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,10 +1 @@ toooting愉快!此版本包括以下改进和修复: -- 增加 🇺🇦 Slava Ukraini -- 自动识别发嘟语言 -- 记住上次公共时间轴选项 -- 显示编辑历史的差异 -- 关注列表可隐藏转嘟和回复 -- 新增管理员推送通知 -- 支持嘟文右到左文字 -- 修复过滤整词功能 -- 修复平板不能删除草稿 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0b15ae60..1ad566ac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -303,8 +303,6 @@ PODS: - React-Core - react-native-language-detection (0.2.2): - React - - react-native-live-text-image-view (0.4.0): - - React-Core - react-native-menu (0.7.2): - React - react-native-netinfo (9.3.7): @@ -428,9 +426,9 @@ PODS: - RNScreens (3.18.2): - React-Core - React-RCTImage - - RNSentry (4.11.0): + - RNSentry (4.12.0): - React-Core - - Sentry/HybridSDK (= 7.31.2) + - Sentry/HybridSDK (= 7.31.3) - RNShareMenu (6.0.0): - React - RNSVG (13.6.0): @@ -441,7 +439,7 @@ PODS: - SDWebImageWebPCoder (0.9.1): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.13) - - Sentry/HybridSDK (7.31.2) + - Sentry/HybridSDK (7.31.3) - Swime (3.0.6) - Yoga (1.14.0) @@ -495,7 +493,6 @@ DEPENDENCIES: - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`) - react-native-language-detection (from `../node_modules/react-native-language-detection`) - - react-native-live-text-image-view (from `../node_modules/react-native-live-text-image-view`) - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -630,8 +627,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-ios-context-menu" react-native-language-detection: :path: "../node_modules/react-native-language-detection" - react-native-live-text-image-view: - :path: "../node_modules/react-native-live-text-image-view" react-native-menu: :path: "../node_modules/@react-native-menu/menu" react-native-netinfo: @@ -740,7 +735,6 @@ SPEC CHECKSUMS: react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0 - react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 @@ -765,12 +759,12 @@ SPEC CHECKSUMS: RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d - RNSentry: f052387ebe939949c74c2cae0c850e76f6d14ddb + RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 - Sentry: b15765d11769852fe78c9add942f7df60ed5dbf5 + Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc diff --git a/package.json b/package.json index 7ca20343..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", @@ -34,14 +34,14 @@ "@react-native-community/netinfo": "9.3.7", "@react-native-community/segmented-control": "^2.2.2", "@react-native-menu/menu": "^0.7.2", - "@react-navigation/bottom-tabs": "^6.5.0", - "@react-navigation/native": "^6.1.0", - "@react-navigation/native-stack": "^6.9.5", - "@react-navigation/stack": "^6.3.8", + "@react-navigation/bottom-tabs": "^6.5.1", + "@react-navigation/native": "^6.1.1", + "@react-navigation/native-stack": "^6.9.6", + "@react-navigation/stack": "^6.3.9", "@reduxjs/toolkit": "^1.9.1", - "@sentry/react-native": "4.11.0", + "@sentry/react-native": "4.12.0", "@sharcoux/slider": "^6.1.1", - "@tanstack/react-query": "^4.19.1", + "@tanstack/react-query": "^4.20.4", "axios": "^1.2.1", "diff": "^5.1.0", "expo": "^47.0.8", @@ -61,7 +61,7 @@ "expo-store-review": "^6.0.0", "expo-video-thumbnails": "^7.0.0", "expo-web-browser": "~12.0.0", - "i18next": "^22.4.1", + "i18next": "^22.4.5", "linkify-it": "^4.0.1", "lodash": "^4.17.21", "react": "^18.2.0", @@ -80,7 +80,6 @@ "react-native-image-picker": "^4.10.2", "react-native-ios-context-menu": "^1.15.1", "react-native-language-detection": "^0.2.2", - "react-native-live-text-image-view": "^0.4.0", "react-native-pager-view": "^6.1.2", "react-native-reanimated": "^2.13.0", "react-native-reanimated-zoom": "^0.3.3", @@ -89,7 +88,7 @@ "react-native-share-menu": "^6.0.0", "react-native-svg": "^13.6.0", "react-native-swipe-list-view": "^3.2.9", - "react-native-tab-view": "^3.3.3", + "react-native-tab-view": "^3.3.4", "react-redux": "^8.0.5", "redux-persist": "^6.0.0", "rn-placeholder": "^3.0.3", diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 7fec5552..fc61ed10 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -395,6 +395,7 @@ declare namespace Mastodon { mention: boolean poll: boolean status: boolean + update: boolean 'admin.sign_up': boolean 'admin.report': boolean } @@ -405,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/api/general.ts b/src/api/general.ts index e29637d3..f13e698f 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { ctx, handleError, userAgent } from './helpers' +import { ctx, handleError, PagedResponse, userAgent } from './helpers' export type Params = { method: 'get' | 'post' | 'put' | 'delete' @@ -19,7 +19,7 @@ const apiGeneral = async ({ params, headers, body -}: Params): Promise<{ body: T }> => { +}: Params): Promise> => { console.log( ctx.bgGreen.bold(' API general ') + ' ' + diff --git a/src/api/helpers/index.ts b/src/api/helpers/index.ts index 55871b18..566280af 100644 --- a/src/api/helpers/index.ts +++ b/src/api/helpers/index.ts @@ -54,13 +54,19 @@ const handleError = console.error(ctx.bold(' API '), ctx.bold('request'), error) shouldReportToSentry && Sentry.captureMessage(config.message) - return Promise.reject() + return Promise.reject(error) } else { console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message) shouldReportToSentry && Sentry.captureMessage(config.message) - return Promise.reject() + return Promise.reject(error) } } +type LinkFormat = { id: string; isOffset: boolean } +export type PagedResponse = { + body: T + links: { prev?: LinkFormat; next?: LinkFormat } +} + export { ctx, handleError, userAgent } diff --git a/src/api/instance.ts b/src/api/instance.ts index ca094b4d..d6446e8f 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -1,6 +1,6 @@ import { RootState } from '@root/store' import axios, { AxiosRequestConfig } from 'axios' -import { ctx, handleError, userAgent } from './helpers' +import { ctx, handleError, PagedResponse, userAgent } from './helpers' export type Params = { method: 'get' | 'post' | 'put' | 'delete' | 'patch' @@ -14,12 +14,6 @@ export type Params = { extras?: Omit } -type LinkFormat = { id: string; isOffset: boolean } -export type InstanceResponse = { - body: T - links: { prev?: LinkFormat; next?: LinkFormat } -} - const apiInstance = async ({ method, version = 'v1', @@ -28,7 +22,7 @@ const apiInstance = async ({ headers, body, extras -}: Params): Promise> => { +}: Params): Promise> => { const { store } = require('@root/store') const state = store.getState() as RootState const instanceActive = state.instances.instances.findIndex(instance => instance.active) diff --git a/src/api/tooot.ts b/src/api/tooot.ts index 7d96c622..0ebb5b57 100644 --- a/src/api/tooot.ts +++ b/src/api/tooot.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/react-native' import { mapEnvironment } from '@utils/checkEnvironment' import axios from 'axios' import { ctx, handleError, userAgent } from './helpers' @@ -37,7 +36,7 @@ const apiTooot = async ({ ) return axios({ - timeout: method === 'post' ? 1000 * 60 : 1000 * 15, + timeout: method === 'post' ? 1000 * 60 : 1000 * 30, method, baseURL: `https://${TOOOT_API_DOMAIN}/`, url: `${url}`, diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx index 5afcbff6..f4c393e7 100644 --- a/src/components/GracefullyImage.tsx +++ b/src/components/GracefullyImage.tsx @@ -4,15 +4,13 @@ import React, { useMemo, useState } from 'react' import { AccessibilityProps, Image, - ImageStyle, - Platform, Pressable, StyleProp, StyleSheet, View, ViewStyle } from 'react-native' -import FastImage from 'react-native-fast-image' +import FastImage, { ImageStyle } from 'react-native-fast-image' import { Blurhash } from 'react-native-blurhash' // blurhas -> if blurhash, show before any loading succeed @@ -97,30 +95,17 @@ const GracefullyImage = ({ {...(onPress ? (hidden ? { disabled: true } : { onPress }) : { disabled: true })} > {uri.preview && !imageLoaded ? ( - ) : null} - {Platform.OS === 'ios' ? ( - - ) : ( - - )} + {blurhashView} ) diff --git a/src/components/Instance/index.tsx b/src/components/Instance/index.tsx index e21c49de..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')} + + ) : ( + - - - - + + + + + + + )} = ({ style={{ color: colors.blue }} onPress={async () => WebBrowser.openBrowserAsync('https://tooot.app/privacy-policy', { - browserPackage: await browserPackage() + ...(await browserPackage()) }) } />, @@ -285,7 +328,7 @@ const ComponentInstance: React.FC = ({ style={{ color: colors.blue }} onPress={async () => WebBrowser.openBrowserAsync('https://tooot.app/terms-of-service', { - browserPackage: await browserPackage() + ...(await browserPackage()) }) } /> diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index b3b8ca6e..2bfe1cdd 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -65,7 +65,6 @@ const MenuRow: React.FC = ({ > { - if (typeof iconBack !== 'string') return // Let icon back handles the gesture if (nativeEvent.state === State.ACTIVE && !loading) { if (screenReaderEnabled && switchOnValueChange) { switchOnValueChange() @@ -86,7 +85,7 @@ const MenuRow: React.FC = ({ > { - 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 ? ( +