diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 284ae423..12b2cab0 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1 +1,3 @@ -Enjoy using tooot \ No newline at end of file +Enjoy toooting! This version includes following improvements and fixes: +- Fix toot attribution of favourites etc. +- Fix switching language \ 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 5729003f..51453bd1 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1 +1,3 @@ -tooot使用愉快 \ No newline at end of file +toooting愉快!此版本包括以下改进和修复: +- 修复嘟文收藏等显示 +- 修复不能切换语言 \ No newline at end of file diff --git a/package.json b/package.json index 12581c74..b822761c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.6.5", + "version": "4.6.6", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", diff --git a/src/App.tsx b/src/App.tsx index 6575b3e1..92dcc448 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { ActionSheetProvider } from '@expo/react-native-action-sheet' +import getLanguage from '@helpers/getLanguage' import queryClient from '@helpers/queryClient' import i18n from '@root/i18n/i18n' import Screens from '@root/Screens' @@ -12,7 +13,7 @@ import timezone from '@root/startup/timezone' import { persistor, store } from '@root/store' import * as Sentry from '@sentry/react-native' import AccessibilityManager from '@utils/accessibility/AccessibilityManager' -import { changeLanguage, getSettingsLanguage } from '@utils/slices/settingsSlice' +import { changeLanguage } from '@utils/slices/settingsSlice' import ThemeManager from '@utils/styles/ThemeManager' import * as Localization from 'expo-localization' import * as SplashScreen from 'expo-splash-screen' @@ -85,7 +86,7 @@ const App: React.FC = () => { if (bootstrapped) { log('log', 'App', 'loading actual app :)') log('log', 'App', `Locale: ${Localization.locale}`) - const language = getSettingsLanguage(store.getState()) + const language = getLanguage() if (!language) { if (Platform.OS !== 'ios') { store.dispatch(changeLanguage('en')) diff --git a/src/api/general.ts b/src/api/general.ts index 09eb3966..81e993f5 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -22,13 +22,13 @@ const apiGeneral = async ({ }: Params): Promise<{ body: T }> => { console.log( ctx.bgGreen.bold(' API general ') + - ' ' + - domain + - ' ' + - method + - ctx.green(' -> ') + - `/${url}` + - (params ? ctx.green(' -> ') : ''), + ' ' + + domain + + ' ' + + method + + ctx.green(' -> ') + + `/${url}` + + (params ? ctx.green(' -> ') : ''), params ? params : '' ) @@ -39,10 +39,7 @@ const apiGeneral = async ({ url, params, headers: { - 'Content-Type': - body && body instanceof FormData - ? 'multipart/form-data' - : 'application/json', + 'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json', Accept: '*/*', ...userAgent, ...headers @@ -54,7 +51,7 @@ const apiGeneral = async ({ body: response.data }) }) - .catch(handleError) + .catch(handleError()) } export default apiGeneral diff --git a/src/api/helpers/index.ts b/src/api/helpers/index.ts index 2f057967..55871b18 100644 --- a/src/api/helpers/index.ts +++ b/src/api/helpers/index.ts @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/react-native' import chalk from 'chalk' import Constants from 'expo-constants' import { Platform } from 'react-native' @@ -7,30 +8,59 @@ const userAgent = { } const ctx = new chalk.Instance({ level: 3 }) -const handleError = (error: any) => { - if (error?.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - console.error( - ctx.bold(' API '), - ctx.bold('response'), - error.response.status, - error?.response.data?.error || error?.response.message || 'Unknown error' - ) - return Promise.reject({ - status: error?.response.status, - message: error?.response.data?.error || error?.response.message || 'Unknown 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 - // http.ClientRequest in node.js - console.error(ctx.bold(' API '), ctx.bold('request'), error) - return Promise.reject() - } else { - console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message) - return Promise.reject() +const handleError = + ( + config: { + message: string + captureRequest?: { url: string; params: any; body: any } + captureResponse?: boolean + } | void + ) => + (error: any) => { + const shouldReportToSentry = config && (config.captureRequest || config.captureResponse) + shouldReportToSentry && Sentry.setContext('Error object', error) + + if (config?.captureRequest) { + Sentry.setContext('Error request', config.captureRequest) + } + + if (error?.response) { + if (config?.captureResponse) { + Sentry.setContext('Error response', { + data: error.response.data, + status: error.response.status, + headers: error.response.headers + }) + } + + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.error( + ctx.bold(' API '), + ctx.bold('response'), + error.response.status, + error?.response.data?.error || error?.response.message || 'Unknown error' + ) + + shouldReportToSentry && Sentry.captureMessage(config.message) + return Promise.reject({ + status: error?.response.status, + message: error?.response.data?.error || error?.response.message || 'Unknown 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 + // http.ClientRequest in node.js + console.error(ctx.bold(' API '), ctx.bold('request'), error) + + shouldReportToSentry && Sentry.captureMessage(config.message) + return Promise.reject() + } else { + console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message) + + shouldReportToSentry && Sentry.captureMessage(config.message) + return Promise.reject() + } } -} export { ctx, handleError, userAgent } diff --git a/src/api/instance.ts b/src/api/instance.ts index 270f88df..a178a42c 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -86,7 +86,7 @@ const apiInstance = async ({ links: { prev, next } }) }) - .catch(handleError) + .catch(handleError()) } export default apiInstance diff --git a/src/api/tooot.ts b/src/api/tooot.ts index bfe6d8e3..7d96c622 100644 --- a/src/api/tooot.ts +++ b/src/api/tooot.ts @@ -55,16 +55,13 @@ const apiTooot = async ({ body: response.data }) }) - .catch(error => { - Sentry.setContext('API request', { url, params, body }) - Sentry.setContext('Error response', { - ...(error?.response && { response: error.response?._response }) + .catch( + handleError({ + message: 'API error', + captureRequest: { url, params, body }, + captureResponse: true }) - Sentry.setContext('Error object', { error }) - Sentry.captureMessage('API error') - - return handleError(error) - }) + ) } export default apiTooot diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 9676ad6b..cf57785b 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -120,7 +120,7 @@ const TimelineDefault: React.FC = ({ queryKey, rootQueryKey, status, - isReblog: !!item.reblog, + reblogStatus: item.reblog ? item : undefined, ownAccount, spoilerHidden, copiableContent, diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index 802ae3d5..04204965 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -38,7 +38,7 @@ const TimelineNotifications: React.FC = ({ }) => { const instanceAccount = useSelector(getInstanceAccount, () => true) - const status = notification.status + const status = notification.status?.reblog ? notification.status.reblog : notification.status const account = notification.status ? notification.status.account : notification.account const ownAccount = notification.account?.id === instanceAccount?.id const [spoilerExpanded, setSpoilerExpanded] = useState( @@ -78,7 +78,11 @@ const TimelineNotifications: React.FC = ({ return ( <> {notification.type !== 'mention' ? ( - + ) : null} = ({ value={{ queryKey, status, - isReblog: !!status?.reblog, ownAccount, spoilerHidden, copiableContent, diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx index 2a5c8f86..fcc3a002 100644 --- a/src/components/Timeline/Shared/Actioned.tsx +++ b/src/components/Timeline/Shared/Actioned.tsx @@ -13,12 +13,12 @@ import StatusContext from './Context' export interface Props { action: Mastodon.Notification['type'] | 'reblog' | 'pinned' isNotification?: boolean - account?: Mastodon.Account + account?: Mastodon.Account // For notification } const TimelineActioned: React.FC = ({ action, isNotification, ...rest }) => { - const { status } = useContext(StatusContext) - const account = isNotification ? rest.account : status?.account + const { status, reblogStatus } = useContext(StatusContext) + const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account) if (!status || !account) return null const { t } = useTranslation('componentTimeline') diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx index 511cbaa9..0991ee89 100644 --- a/src/components/Timeline/Shared/Actions.tsx +++ b/src/components/Timeline/Shared/Actions.tsx @@ -22,7 +22,7 @@ import { useSelector } from 'react-redux' import StatusContext from './Context' const TimelineActions: React.FC = () => { - const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } = + const { queryKey, rootQueryKey, status, reblogStatus, ownAccount, highlighted, disableDetails } = useContext(StatusContext) if (!queryKey || !status || disableDetails) return null @@ -109,7 +109,7 @@ const TimelineActions: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'reblogged', currentValue: status.reblogged, @@ -125,7 +125,7 @@ const TimelineActions: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'reblogged', currentValue: status.reblogged, @@ -144,7 +144,7 @@ const TimelineActions: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'reblogged', currentValue: status.reblogged, @@ -161,7 +161,7 @@ const TimelineActions: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'favourited', currentValue: status.favourited, @@ -176,7 +176,7 @@ const TimelineActions: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'bookmarked', currentValue: status.bookmarked, diff --git a/src/components/Timeline/Shared/Context.tsx b/src/components/Timeline/Shared/Context.tsx index 5447643b..5dafee11 100644 --- a/src/components/Timeline/Shared/Context.tsx +++ b/src/components/Timeline/Shared/Context.tsx @@ -7,7 +7,7 @@ type ContextType = { status?: Mastodon.Status - isReblog?: boolean + reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status ownAccount?: boolean spoilerHidden?: boolean copiableContent?: React.MutableRefObject<{ diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index 339b2fc5..6a21fcae 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -20,8 +20,15 @@ import { useQueryClient } from 'react-query' import StatusContext from './Context' const TimelinePoll: React.FC = () => { - const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } = - useContext(StatusContext) + const { + queryKey, + rootQueryKey, + status, + reblogStatus, + ownAccount, + spoilerHidden, + disableDetails + } = useContext(StatusContext) if (!queryKey || !status || !status.poll) return null const poll = status.poll @@ -78,7 +85,7 @@ const TimelinePoll: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'poll', id: poll.id, @@ -104,7 +111,7 @@ const TimelinePoll: React.FC = () => { queryKey, rootQueryKey, id: status.id, - isReblog, + isReblog: !!reblogStatus, payload: { property: 'poll', id: poll.id, diff --git a/src/i18n/ca/components/contextMenu.json b/src/i18n/ca/components/contextMenu.json index 8a4a90a7..a9cda06e 100644 --- a/src/i18n/ca/components/contextMenu.json +++ b/src/i18n/ca/components/contextMenu.json @@ -20,8 +20,8 @@ } }, "at": { - "direct": "", - "public": "" + "direct": "Missatge directe", + "public": "Missatge públic" }, "copy": { "action": "Copia la publicació", diff --git a/src/i18n/ca/screens/tabs.json b/src/i18n/ca/screens/tabs.json index 6f38f91d..bdd3f0e0 100644 --- a/src/i18n/ca/screens/tabs.json +++ b/src/i18n/ca/screens/tabs.json @@ -352,7 +352,7 @@ } }, "trending": { - "tags": "" + "tags": "Etiquetes en tendència" } }, "sections": { diff --git a/src/i18n/es/components/contextMenu.json b/src/i18n/es/components/contextMenu.json index fe8f71a9..149a2687 100644 --- a/src/i18n/es/components/contextMenu.json +++ b/src/i18n/es/components/contextMenu.json @@ -20,8 +20,8 @@ } }, "at": { - "direct": "", - "public": "" + "direct": "Mensaje directo", + "public": "Mensaje público" }, "copy": { "action": "Copiar toot", diff --git a/src/i18n/es/screens/tabs.json b/src/i18n/es/screens/tabs.json index 9103b01e..aa984450 100644 --- a/src/i18n/es/screens/tabs.json +++ b/src/i18n/es/screens/tabs.json @@ -352,7 +352,7 @@ } }, "trending": { - "tags": "" + "tags": "Etiquetas en tendencia" } }, "sections": { diff --git a/src/i18n/ja/components/contextMenu.json b/src/i18n/ja/components/contextMenu.json index 560b7e92..861a4043 100644 --- a/src/i18n/ja/components/contextMenu.json +++ b/src/i18n/ja/components/contextMenu.json @@ -20,8 +20,8 @@ } }, "at": { - "direct": "", - "public": "" + "direct": "ダイレクトメッセージ", + "public": "パブリックメッセージ" }, "copy": { "action": "トゥートをコピー", diff --git a/src/i18n/nl/components/contextMenu.json b/src/i18n/nl/components/contextMenu.json index 9bb9f208..52c73fe9 100644 --- a/src/i18n/nl/components/contextMenu.json +++ b/src/i18n/nl/components/contextMenu.json @@ -6,7 +6,7 @@ "action_false": "Volg gebruiker", "action_true": "Ontvolg" }, - "inLists": "Gebruiker van lijsten beheren", + "inLists": "Gebruiker op lijsten beheren", "mute": { "action_false": "Gebruiker dempen", "action_true": "Dempen opheffen voor gebruiker" @@ -20,8 +20,8 @@ } }, "at": { - "direct": "", - "public": "" + "direct": "Direct bericht", + "public": "Openbaar bericht" }, "copy": { "action": "Toot kopiëren", diff --git a/src/i18n/nl/components/mediaSelector.json b/src/i18n/nl/components/mediaSelector.json index 1c79a4c3..5f7596a6 100644 --- a/src/i18n/nl/components/mediaSelector.json +++ b/src/i18n/nl/components/mediaSelector.json @@ -1,6 +1,6 @@ { "title": "Selecteer mediabron", - "message": "Media EXIF-gegevens zijn niet geüpload", + "message": "Media EXIF gegevens worden niet geüpload", "options": { "image": "Foto's uploaden", "image_max": "Foto's uploaden (max {{max}})", diff --git a/src/i18n/nl/screens.json b/src/i18n/nl/screens.json index 6fab9103..264a63ba 100644 --- a/src/i18n/nl/screens.json +++ b/src/i18n/nl/screens.json @@ -1,7 +1,7 @@ { "screenshot": { "title": "Privacy Bescherming", - "message": "Gelieve de identiteit van een andere gebruiker niet openbaar te maken, zoals gebruikersnaam, avatar enz. Bedankt!", + "message": "Gelieve de identiteit van een andere gebruiker niet openbaar te maken, zoals gebruikersnaam of avatar en meer. Bedankt!", "button": "Bevestig" }, "localCorrupt": { diff --git a/src/i18n/nl/screens/tabs.json b/src/i18n/nl/screens/tabs.json index 307902d3..f17ece04 100644 --- a/src/i18n/nl/screens/tabs.json +++ b/src/i18n/nl/screens/tabs.json @@ -290,7 +290,7 @@ "heading": "Tooot beoordelen" }, "contact": { - "heading": "Contacteer tooot" + "heading": "Tooot contacteren" }, "version": "Version v{{version}}", "instanceVersion": "Mastodon versie v{{version}}" @@ -341,7 +341,7 @@ "placeholder": "naar..." }, "empty": { - "general": "Enter keyword to search for $t(screenTabs:shared.search.sections.accounts)$t(screenTabs:shared.search.sections.hashtags) or $t(screenTabs:shared.search.sections.statuses)", + "general": "Voer trefwoord in om te zoeken naar $t(screenTabs:shared.search.sections.accounts), $t(screenTabs:shared.search.sections.hashtags) of $t(screenTabs:shared.search.sections.statuses)", "advanced": { "header": "Geavanceerd zoeken", "example": { @@ -352,7 +352,7 @@ } }, "trending": { - "tags": "" + "tags": "Trending tags" } }, "sections": { @@ -363,7 +363,7 @@ "notFound": "Kan {{searchTerm}} niet vinden gerelateerd aan {{type}}" }, "toot": { - "name": "Discussies" + "name": "Gesprek" }, "users": { "accounts": { diff --git a/src/i18n/vi/common.json b/src/i18n/vi/common.json index d1eefbd6..421903ba 100644 --- a/src/i18n/vi/common.json +++ b/src/i18n/vi/common.json @@ -6,7 +6,7 @@ "discard": "Bỏ qua", "continue": "Tiếp tục", "delete": "Xóa", - "done": "" + "done": "Xong" }, "customEmoji": { "accessibilityLabel": "Tùy chỉnh emoji {{emoji}}" diff --git a/src/i18n/vi/components/contextMenu.json b/src/i18n/vi/components/contextMenu.json index cadfc401..fab9fb7d 100644 --- a/src/i18n/vi/components/contextMenu.json +++ b/src/i18n/vi/components/contextMenu.json @@ -6,7 +6,7 @@ "action_false": "Theo dõi người này", "action_true": "Ngưng theo dõi người này" }, - "inLists": "", + "inLists": "Quản lý người trong danh sách", "mute": { "action_false": "Ẩn người này", "action_true": "Bỏ ẩn người dùng" @@ -20,8 +20,8 @@ } }, "at": { - "direct": "", - "public": "" + "direct": "Nhắn riêng", + "public": "Công khai" }, "copy": { "action": "Sao chép tút", diff --git a/src/i18n/vi/screens/tabs.json b/src/i18n/vi/screens/tabs.json index 65775503..3f730a7e 100644 --- a/src/i18n/vi/screens/tabs.json +++ b/src/i18n/vi/screens/tabs.json @@ -321,9 +321,9 @@ "suspended": "Người này đã bị vô hiệu hóa" }, "accountInLists": { - "name": "", - "inLists": "", - "notInLists": "" + "name": "Danh sách của @{{username}}", + "inLists": "Trong danh sách", + "notInLists": "Danh sách khác" }, "attachments": { "name": "<0 /><1>'s media" @@ -352,7 +352,7 @@ } }, "trending": { - "tags": "" + "tags": "Hashtag xu hướng" } }, "sections": { diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 9928c062..a6856d45 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -1,3 +1,4 @@ +import { handleError } from '@api/helpers' import { ComponentEmojis } from '@components/Emojis' import { EmojisState } from '@components/Emojis/helpers/EmojisContext' import { HeaderLeft, HeaderRight } from '@components/Header' @@ -6,7 +7,6 @@ import haptics from '@root/components/haptics' import { useAppDispatch } from '@root/store' import ComposeRoot from '@screens/Compose/Root' import formatText from '@screens/Compose/utils/formatText' -import * as Sentry from '@sentry/react-native' import { RootStackScreenProps } from '@utils/navigation/navigators' import { useTimelineMutation } from '@utils/queryHooks/timeline' import { updateStoreReview } from '@utils/slices/contextsSlice' @@ -319,9 +319,8 @@ const ScreenCompose: React.FC> = ({ ] ) } else { - Sentry.setContext('Error object', { error }) - Sentry.captureMessage('Posting error') haptics('Error') + handleError({ message: 'Posting error', captureResponse: true }) composeDispatch({ type: 'posting', payload: false }) Alert.alert(t('heading.right.alert.default.title'), undefined, [ { diff --git a/src/screens/Tabs/Shared/Account/Information/Note.tsx b/src/screens/Tabs/Shared/Account/Information/Note.tsx index 1a133ad2..b1f4eda8 100644 --- a/src/screens/Tabs/Shared/Account/Information/Note.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Note.tsx @@ -23,7 +23,13 @@ const AccountInformationNote = React.memo( return ( - + ) }, diff --git a/src/startup/audio.ts b/src/startup/audio.ts index 3055cbfd..f059a6af 100644 --- a/src/startup/audio.ts +++ b/src/startup/audio.ts @@ -4,8 +4,8 @@ import log from './log' const audio = () => { log('log', 'audio', 'setting audio playback default options') Audio.setAudioModeAsync({ - interruptionModeIOS: InterruptionModeIOS.DuckOthers, - interruptionModeAndroid: InterruptionModeAndroid.DuckOthers, + interruptionModeIOS: InterruptionModeIOS.DoNotMix, + interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, playsInSilentModeIOS: true, staysActiveInBackground: false }) diff --git a/src/utils/push/useConnect.ts b/src/utils/push/useConnect.ts index a34df781..1d470e69 100644 --- a/src/utils/push/useConnect.ts +++ b/src/utils/push/useConnect.ts @@ -1,4 +1,5 @@ import apiGeneral from '@api/general' +import { handleError } from '@api/helpers' import apiTooot from '@api/tooot' import { displayMessage } from '@components/Message' import navigationRef from '@helpers/navigationRef' @@ -33,11 +34,8 @@ const pushUseConnect = () => { }) .then(() => Notifications.setBadgeCountAsync(0)) .catch(error => { - Sentry.setContext('Error response', { - ...(error?.response && { response: error.response?._response }) - }) - Sentry.setContext('Error object', { error }) - Sentry.captureMessage('Push connect error') + handleError({ message: 'Push connect error', captureResponse: true }) + Notifications.setBadgeCountAsync(0) if (error?.status == 404) { displayMessage({