1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge pull request #521 from tooot-app/main

Emergency release v4.6.6
This commit is contained in:
xmflsct
2022-12-05 20:00:23 +01:00
committed by GitHub
30 changed files with 147 additions and 105 deletions

View File

@@ -1 +1,3 @@
Enjoy using tooot Enjoy toooting! This version includes following improvements and fixes:
- Fix toot attribution of favourites etc.
- Fix switching language

View File

@@ -1 +1,3 @@
tooot使用愉快 toooting愉快此版本包括以下改进和修复
- 修复嘟文收藏等显示
- 修复不能切换语言

View File

@@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.6.5", "version": "4.6.6",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",

View File

@@ -1,4 +1,5 @@
import { ActionSheetProvider } from '@expo/react-native-action-sheet' import { ActionSheetProvider } from '@expo/react-native-action-sheet'
import getLanguage from '@helpers/getLanguage'
import queryClient from '@helpers/queryClient' import queryClient from '@helpers/queryClient'
import i18n from '@root/i18n/i18n' import i18n from '@root/i18n/i18n'
import Screens from '@root/Screens' import Screens from '@root/Screens'
@@ -12,7 +13,7 @@ import timezone from '@root/startup/timezone'
import { persistor, store } from '@root/store' import { persistor, store } from '@root/store'
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import AccessibilityManager from '@utils/accessibility/AccessibilityManager' 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 ThemeManager from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization' import * as Localization from 'expo-localization'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
@@ -85,7 +86,7 @@ const App: React.FC = () => {
if (bootstrapped) { if (bootstrapped) {
log('log', 'App', 'loading actual app :)') log('log', 'App', 'loading actual app :)')
log('log', 'App', `Locale: ${Localization.locale}`) log('log', 'App', `Locale: ${Localization.locale}`)
const language = getSettingsLanguage(store.getState()) const language = getLanguage()
if (!language) { if (!language) {
if (Platform.OS !== 'ios') { if (Platform.OS !== 'ios') {
store.dispatch(changeLanguage('en')) store.dispatch(changeLanguage('en'))

View File

@@ -39,10 +39,7 @@ const apiGeneral = async <T = unknown>({
url, url,
params, params,
headers: { headers: {
'Content-Type': 'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
body && body instanceof FormData
? 'multipart/form-data'
: 'application/json',
Accept: '*/*', Accept: '*/*',
...userAgent, ...userAgent,
...headers ...headers
@@ -54,7 +51,7 @@ const apiGeneral = async <T = unknown>({
body: response.data body: response.data
}) })
}) })
.catch(handleError) .catch(handleError())
} }
export default apiGeneral export default apiGeneral

View File

@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/react-native'
import chalk from 'chalk' import chalk from 'chalk'
import Constants from 'expo-constants' import Constants from 'expo-constants'
import { Platform } from 'react-native' import { Platform } from 'react-native'
@@ -7,8 +8,31 @@ const userAgent = {
} }
const ctx = new chalk.Instance({ level: 3 }) const ctx = new chalk.Instance({ level: 3 })
const handleError = (error: any) => { 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 (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 // The request was made and the server responded with a status code
// that falls out of the range of 2xx // that falls out of the range of 2xx
console.error( console.error(
@@ -17,6 +41,8 @@ const handleError = (error: any) => {
error.response.status, error.response.status,
error?.response.data?.error || error?.response.message || 'Unknown error' error?.response.data?.error || error?.response.message || 'Unknown error'
) )
shouldReportToSentry && Sentry.captureMessage(config.message)
return Promise.reject({ return Promise.reject({
status: error?.response.status, status: error?.response.status,
message: error?.response.data?.error || error?.response.message || 'Unknown error' message: error?.response.data?.error || error?.response.message || 'Unknown error'
@@ -26,9 +52,13 @@ const handleError = (error: any) => {
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(ctx.bold(' API '), ctx.bold('request'), error) console.error(ctx.bold(' API '), ctx.bold('request'), error)
shouldReportToSentry && Sentry.captureMessage(config.message)
return Promise.reject() return Promise.reject()
} else { } else {
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message) console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
shouldReportToSentry && Sentry.captureMessage(config.message)
return Promise.reject() return Promise.reject()
} }
} }

View File

@@ -86,7 +86,7 @@ const apiInstance = async <T = unknown>({
links: { prev, next } links: { prev, next }
}) })
}) })
.catch(handleError) .catch(handleError())
} }
export default apiInstance export default apiInstance

View File

@@ -55,16 +55,13 @@ const apiTooot = async <T = unknown>({
body: response.data body: response.data
}) })
}) })
.catch(error => { .catch(
Sentry.setContext('API request', { url, params, body }) handleError({
Sentry.setContext('Error response', { message: 'API error',
...(error?.response && { response: error.response?._response }) captureRequest: { url, params, body },
}) captureResponse: true
Sentry.setContext('Error object', { error })
Sentry.captureMessage('API error')
return handleError(error)
}) })
)
} }
export default apiTooot export default apiTooot

View File

@@ -120,7 +120,7 @@ const TimelineDefault: React.FC<Props> = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
status, status,
isReblog: !!item.reblog, reblogStatus: item.reblog ? item : undefined,
ownAccount, ownAccount,
spoilerHidden, spoilerHidden,
copiableContent, copiableContent,

View File

@@ -38,7 +38,7 @@ const TimelineNotifications: React.FC<Props> = ({
}) => { }) => {
const instanceAccount = useSelector(getInstanceAccount, () => true) 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 account = notification.status ? notification.status.account : notification.account
const ownAccount = notification.account?.id === instanceAccount?.id const ownAccount = notification.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState( const [spoilerExpanded, setSpoilerExpanded] = useState(
@@ -78,7 +78,11 @@ const TimelineNotifications: React.FC<Props> = ({
return ( return (
<> <>
{notification.type !== 'mention' ? ( {notification.type !== 'mention' ? (
<TimelineActioned action={notification.type} isNotification account={account} /> <TimelineActioned
action={notification.type}
isNotification
account={notification.account}
/>
) : null} ) : null}
<View <View
@@ -132,7 +136,6 @@ const TimelineNotifications: React.FC<Props> = ({
value={{ value={{
queryKey, queryKey,
status, status,
isReblog: !!status?.reblog,
ownAccount, ownAccount,
spoilerHidden, spoilerHidden,
copiableContent, copiableContent,

View File

@@ -13,12 +13,12 @@ import StatusContext from './Context'
export interface Props { export interface Props {
action: Mastodon.Notification['type'] | 'reblog' | 'pinned' action: Mastodon.Notification['type'] | 'reblog' | 'pinned'
isNotification?: boolean isNotification?: boolean
account?: Mastodon.Account account?: Mastodon.Account // For notification
} }
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => { const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
const { status } = useContext(StatusContext) const { status, reblogStatus } = useContext(StatusContext)
const account = isNotification ? rest.account : status?.account const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account)
if (!status || !account) return null if (!status || !account) return null
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')

View File

@@ -22,7 +22,7 @@ import { useSelector } from 'react-redux'
import StatusContext from './Context' import StatusContext from './Context'
const TimelineActions: React.FC = () => { const TimelineActions: React.FC = () => {
const { queryKey, rootQueryKey, status, isReblog, ownAccount, highlighted, disableDetails } = const { queryKey, rootQueryKey, status, reblogStatus, ownAccount, highlighted, disableDetails } =
useContext(StatusContext) useContext(StatusContext)
if (!queryKey || !status || disableDetails) return null if (!queryKey || !status || disableDetails) return null
@@ -109,7 +109,7 @@ const TimelineActions: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@@ -125,7 +125,7 @@ const TimelineActions: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@@ -144,7 +144,7 @@ const TimelineActions: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'reblogged', property: 'reblogged',
currentValue: status.reblogged, currentValue: status.reblogged,
@@ -161,7 +161,7 @@ const TimelineActions: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'favourited', property: 'favourited',
currentValue: status.favourited, currentValue: status.favourited,
@@ -176,7 +176,7 @@ const TimelineActions: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'bookmarked', property: 'bookmarked',
currentValue: status.bookmarked, currentValue: status.bookmarked,

View File

@@ -7,7 +7,7 @@ type ContextType = {
status?: Mastodon.Status status?: Mastodon.Status
isReblog?: boolean reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
ownAccount?: boolean ownAccount?: boolean
spoilerHidden?: boolean spoilerHidden?: boolean
copiableContent?: React.MutableRefObject<{ copiableContent?: React.MutableRefObject<{

View File

@@ -20,8 +20,15 @@ import { useQueryClient } from 'react-query'
import StatusContext from './Context' import StatusContext from './Context'
const TimelinePoll: React.FC = () => { const TimelinePoll: React.FC = () => {
const { queryKey, rootQueryKey, status, isReblog, ownAccount, spoilerHidden, disableDetails } = const {
useContext(StatusContext) queryKey,
rootQueryKey,
status,
reblogStatus,
ownAccount,
spoilerHidden,
disableDetails
} = useContext(StatusContext)
if (!queryKey || !status || !status.poll) return null if (!queryKey || !status || !status.poll) return null
const poll = status.poll const poll = status.poll
@@ -78,7 +85,7 @@ const TimelinePoll: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'poll', property: 'poll',
id: poll.id, id: poll.id,
@@ -104,7 +111,7 @@ const TimelinePoll: React.FC = () => {
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id: status.id, id: status.id,
isReblog, isReblog: !!reblogStatus,
payload: { payload: {
property: 'poll', property: 'poll',
id: poll.id, id: poll.id,

View File

@@ -20,8 +20,8 @@
} }
}, },
"at": { "at": {
"direct": "", "direct": "Missatge directe",
"public": "" "public": "Missatge públic"
}, },
"copy": { "copy": {
"action": "Copia la publicació", "action": "Copia la publicació",

View File

@@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Etiquetes en tendència"
} }
}, },
"sections": { "sections": {

View File

@@ -20,8 +20,8 @@
} }
}, },
"at": { "at": {
"direct": "", "direct": "Mensaje directo",
"public": "" "public": "Mensaje público"
}, },
"copy": { "copy": {
"action": "Copiar toot", "action": "Copiar toot",

View File

@@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Etiquetas en tendencia"
} }
}, },
"sections": { "sections": {

View File

@@ -20,8 +20,8 @@
} }
}, },
"at": { "at": {
"direct": "", "direct": "ダイレクトメッセージ",
"public": "" "public": "パブリックメッセージ"
}, },
"copy": { "copy": {
"action": "トゥートをコピー", "action": "トゥートをコピー",

View File

@@ -6,7 +6,7 @@
"action_false": "Volg gebruiker", "action_false": "Volg gebruiker",
"action_true": "Ontvolg" "action_true": "Ontvolg"
}, },
"inLists": "Gebruiker van lijsten beheren", "inLists": "Gebruiker op lijsten beheren",
"mute": { "mute": {
"action_false": "Gebruiker dempen", "action_false": "Gebruiker dempen",
"action_true": "Dempen opheffen voor gebruiker" "action_true": "Dempen opheffen voor gebruiker"
@@ -20,8 +20,8 @@
} }
}, },
"at": { "at": {
"direct": "", "direct": "Direct bericht",
"public": "" "public": "Openbaar bericht"
}, },
"copy": { "copy": {
"action": "Toot kopiëren", "action": "Toot kopiëren",

View File

@@ -1,6 +1,6 @@
{ {
"title": "Selecteer mediabron", "title": "Selecteer mediabron",
"message": "Media EXIF-gegevens zijn niet geüpload", "message": "Media EXIF gegevens worden niet geüpload",
"options": { "options": {
"image": "Foto's uploaden", "image": "Foto's uploaden",
"image_max": "Foto's uploaden (max {{max}})", "image_max": "Foto's uploaden (max {{max}})",

View File

@@ -1,7 +1,7 @@
{ {
"screenshot": { "screenshot": {
"title": "Privacy Bescherming", "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" "button": "Bevestig"
}, },
"localCorrupt": { "localCorrupt": {

View File

@@ -290,7 +290,7 @@
"heading": "Tooot beoordelen" "heading": "Tooot beoordelen"
}, },
"contact": { "contact": {
"heading": "Contacteer tooot" "heading": "Tooot contacteren"
}, },
"version": "Version v{{version}}", "version": "Version v{{version}}",
"instanceVersion": "Mastodon versie v{{version}}" "instanceVersion": "Mastodon versie v{{version}}"
@@ -341,7 +341,7 @@
"placeholder": "naar..." "placeholder": "naar..."
}, },
"empty": { "empty": {
"general": "Enter keyword to search for <bold>$t(screenTabs:shared.search.sections.accounts)</bold><bold>$t(screenTabs:shared.search.sections.hashtags)</bold> or <bold>$t(screenTabs:shared.search.sections.statuses)</bold>", "general": "Voer trefwoord in om te zoeken naar <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold> of <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
"advanced": { "advanced": {
"header": "Geavanceerd zoeken", "header": "Geavanceerd zoeken",
"example": { "example": {
@@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Trending tags"
} }
}, },
"sections": { "sections": {
@@ -363,7 +363,7 @@
"notFound": "Kan <bold>{{searchTerm}}</bold> niet vinden gerelateerd aan {{type}}" "notFound": "Kan <bold>{{searchTerm}}</bold> niet vinden gerelateerd aan {{type}}"
}, },
"toot": { "toot": {
"name": "Discussies" "name": "Gesprek"
}, },
"users": { "users": {
"accounts": { "accounts": {

View File

@@ -6,7 +6,7 @@
"discard": "Bỏ qua", "discard": "Bỏ qua",
"continue": "Tiếp tục", "continue": "Tiếp tục",
"delete": "Xóa", "delete": "Xóa",
"done": "" "done": "Xong"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "Tùy chỉnh emoji {{emoji}}" "accessibilityLabel": "Tùy chỉnh emoji {{emoji}}"

View File

@@ -6,7 +6,7 @@
"action_false": "Theo dõi người này", "action_false": "Theo dõi người này",
"action_true": "Ngưng 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": { "mute": {
"action_false": "Ẩn người này", "action_false": "Ẩn người này",
"action_true": "Bỏ ẩn người dùng" "action_true": "Bỏ ẩn người dùng"
@@ -20,8 +20,8 @@
} }
}, },
"at": { "at": {
"direct": "", "direct": "Nhắn riêng",
"public": "" "public": "Công khai"
}, },
"copy": { "copy": {
"action": "Sao chép tút", "action": "Sao chép tút",

View File

@@ -321,9 +321,9 @@
"suspended": "Người này đã bị vô hiệu hóa" "suspended": "Người này đã bị vô hiệu hóa"
}, },
"accountInLists": { "accountInLists": {
"name": "", "name": "Danh sách của @{{username}}",
"inLists": "", "inLists": "Trong danh sách",
"notInLists": "" "notInLists": "Danh sách khác"
}, },
"attachments": { "attachments": {
"name": "<0 /><1>'s media</1>" "name": "<0 /><1>'s media</1>"
@@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Hashtag xu hướng"
} }
}, },
"sections": { "sections": {

View File

@@ -1,3 +1,4 @@
import { handleError } from '@api/helpers'
import { ComponentEmojis } from '@components/Emojis' import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext' import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
@@ -6,7 +7,6 @@ import haptics from '@root/components/haptics'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import ComposeRoot from '@screens/Compose/Root' import ComposeRoot from '@screens/Compose/Root'
import formatText from '@screens/Compose/utils/formatText' import formatText from '@screens/Compose/utils/formatText'
import * as Sentry from '@sentry/react-native'
import { RootStackScreenProps } from '@utils/navigation/navigators' import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useTimelineMutation } from '@utils/queryHooks/timeline' import { useTimelineMutation } from '@utils/queryHooks/timeline'
import { updateStoreReview } from '@utils/slices/contextsSlice' import { updateStoreReview } from '@utils/slices/contextsSlice'
@@ -319,9 +319,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
] ]
) )
} else { } else {
Sentry.setContext('Error object', { error })
Sentry.captureMessage('Posting error')
haptics('Error') haptics('Error')
handleError({ message: 'Posting error', captureResponse: true })
composeDispatch({ type: 'posting', payload: false }) composeDispatch({ type: 'posting', payload: false })
Alert.alert(t('heading.right.alert.default.title'), undefined, [ Alert.alert(t('heading.right.alert.default.title'), undefined, [
{ {

View File

@@ -23,7 +23,13 @@ const AccountInformationNote = React.memo(
return ( return (
<View style={styles.note}> <View style={styles.note}>
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable /> <ParseHTML
content={account.note!}
size={'M'}
emojis={account.emojis}
selectable
numberOfLines={999}
/>
</View> </View>
) )
}, },

View File

@@ -4,8 +4,8 @@ import log from './log'
const audio = () => { const audio = () => {
log('log', 'audio', 'setting audio playback default options') log('log', 'audio', 'setting audio playback default options')
Audio.setAudioModeAsync({ Audio.setAudioModeAsync({
interruptionModeIOS: InterruptionModeIOS.DuckOthers, interruptionModeIOS: InterruptionModeIOS.DoNotMix,
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers, interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
playsInSilentModeIOS: true, playsInSilentModeIOS: true,
staysActiveInBackground: false staysActiveInBackground: false
}) })

View File

@@ -1,4 +1,5 @@
import apiGeneral from '@api/general' import apiGeneral from '@api/general'
import { handleError } from '@api/helpers'
import apiTooot from '@api/tooot' import apiTooot from '@api/tooot'
import { displayMessage } from '@components/Message' import { displayMessage } from '@components/Message'
import navigationRef from '@helpers/navigationRef' import navigationRef from '@helpers/navigationRef'
@@ -33,11 +34,8 @@ const pushUseConnect = () => {
}) })
.then(() => Notifications.setBadgeCountAsync(0)) .then(() => Notifications.setBadgeCountAsync(0))
.catch(error => { .catch(error => {
Sentry.setContext('Error response', { handleError({ message: 'Push connect error', captureResponse: true })
...(error?.response && { response: error.response?._response })
})
Sentry.setContext('Error object', { error })
Sentry.captureMessage('Push connect error')
Notifications.setBadgeCountAsync(0) Notifications.setBadgeCountAsync(0)
if (error?.status == 404) { if (error?.status == 404) {
displayMessage({ displayMessage({