Merge branch 'main' into release

This commit is contained in:
xmflsct 2022-12-05 23:21:25 +01:00
commit 884419ef76
43 changed files with 300 additions and 787 deletions

View File

@ -39,7 +39,7 @@ export interface Props {
}
const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { i18n, t } = useTranslation('screens')
const { t } = useTranslation('screens')
const dispatch = useAppDispatch()
const instanceActive = useSelector(getInstanceActive)
const { colors, theme } = useTheme()
@ -70,8 +70,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
displayMessage({
message: t('localCorrupt.message'),
description: localCorrupt.length ? localCorrupt : undefined,
type: 'error',
theme
type: 'danger'
})
// @ts-ignore
navigationRef.navigate('Screen-Tabs', {
@ -183,8 +182,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
message: t('shareError.imageNotSupported', {
type: mime.split('/')[1]
}),
type: 'error',
theme
type: 'danger'
})
return
}
@ -196,8 +194,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
message: t('shareError.videoNotSupported', {
type: mime.split('/')[1]
}),
type: 'error',
theme
type: 'danger'
})
return
}

View File

@ -1,10 +1,9 @@
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { getColors, Theme } from '@utils/styles/themes'
import React, { RefObject } from 'react'
import { AccessibilityInfo } from 'react-native'
import FlashMessage, { hideMessage, showMessage } from 'react-native-flash-message'
import FlashMessage, { MessageType, showMessage } from 'react-native-flash-message'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import haptics from './haptics'
@ -15,107 +14,80 @@ const displayMessage = ({
message,
description,
onPress,
theme,
type
}:
| {
ref?: RefObject<FlashMessage>
duration?: 'short' | 'long'
autoHide?: boolean
message: string
description?: string
onPress?: () => void
theme?: undefined
type?: undefined
}
| {
ref?: RefObject<FlashMessage>
duration?: 'short' | 'long'
autoHide?: boolean
message: string
description?: string
onPress?: () => void
theme: Theme
type: 'success' | 'error' | 'warning'
}) => {
}: {
ref?: RefObject<FlashMessage>
duration?: 'short' | 'long'
autoHide?: boolean
message: string
description?: string
onPress?: () => void
type?: MessageType
}) => {
AccessibilityInfo.announceForAccessibility(message + '.' + description)
enum iconMapping {
success = 'CheckCircle',
error = 'XCircle',
warning = 'AlertCircle'
}
enum colorMapping {
success = 'blue',
error = 'red',
warning = 'secondary'
}
if (type && type === 'error') {
if (type && type === 'danger') {
haptics('Error')
}
if (ref) {
ref.current?.showMessage({
duration: type === 'error' ? 8000 : duration === 'short' ? 3000 : 5000,
duration: type === 'danger' ? 8000 : duration === 'short' ? 3000 : 5000,
autoHide,
message,
description,
onPress,
...(theme &&
type && {
renderFlashMessageIcon: () => {
return (
<Icon
name={iconMapping[type]}
size={StyleConstants.Font.LineHeight.M}
color={getColors(theme)[colorMapping[type]]}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
)
}
})
type
})
} else {
showMessage({
duration: type === 'error' ? 8000 : duration === 'short' ? 3000 : 5000,
duration: type === 'danger' ? 8000 : duration === 'short' ? 3000 : 5000,
autoHide,
message,
description,
onPress,
...(theme &&
type && {
renderFlashMessageIcon: () => {
return (
<Icon
name={iconMapping[type]}
size={StyleConstants.Font.LineHeight.M}
color={getColors(theme)[colorMapping[type]]}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
)
}
})
type
})
}
}
const removeMessage = () => {
// if (ref) {
// ref.current?.hideMessage()
// } else {
hideMessage()
// }
}
const Message = React.forwardRef<FlashMessage>((_, ref) => {
const { colors, theme } = useTheme()
const insets = useSafeAreaInsets()
enum iconMapping {
success = 'CheckCircle',
danger = 'XCircle',
warning = 'AlertCircle',
none = '',
default = '',
info = '',
auto = ''
}
enum colorMapping {
success = 'blue',
danger = 'red',
warning = 'secondary',
none = 'secondary',
default = 'secondary',
info = 'secondary',
auto = 'secondary'
}
return (
<FlashMessage
ref={ref}
icon='auto'
renderFlashMessageIcon={type => {
return typeof type === 'string' && ['success', 'danger', 'warning'].includes(type) ? (
<Icon
name={iconMapping[type]}
size={StyleConstants.Font.LineHeight.M}
color={colors[colorMapping[type]]}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
) : null
}}
position='top'
floating
style={{
@ -142,4 +114,4 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
)
})
export { Message, displayMessage, removeMessage }
export { Message, displayMessage }

View File

@ -14,7 +14,6 @@ import {
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
@ -38,7 +37,6 @@ const menuAccount = ({
const navigation =
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
const menus: ContextMenu[][] = [[]]
@ -60,7 +58,6 @@ const menuAccount = ({
queryClient.refetchQueries(['Relationship', { id: account.id }])
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`account.${theParams.payload.property}.action`, {
@ -74,8 +71,7 @@ const menuAccount = ({
onError: (err: any, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'error',
type: 'danger',
message: t('common:message.error.message', {
function: t(`account.${theParams.payload.property}.action`, {
...(theParams.payload.property !== 'reports' && {
@ -109,8 +105,7 @@ const menuAccount = ({
},
onError: (err: any, { payload: { action } }) => {
displayMessage({
theme,
type: 'error',
type: 'danger',
message: t('common:message.error.message', {
function: t(`${action}.function`)
}),

View File

@ -1,7 +1,6 @@
import { displayMessage } from '@components/Message'
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
@ -18,14 +17,12 @@ const menuInstance = ({
}): ContextMenu[][] => {
if (!status || !queryKey) return []
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
onSettled: () => {
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`instance.block.action`, { instance })

View File

@ -1,6 +1,5 @@
import { displayMessage } from '@components/Message'
import Clipboard from '@react-native-clipboard/clipboard'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Platform, Share } from 'react-native'
@ -22,7 +21,6 @@ const menuShare = (
): ContextMenu[][] => {
if (params.type === 'status' && params.visibility === 'direct') return []
const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu')
const menus: ContextMenu[][] = [[]]
@ -56,11 +54,7 @@ const menuShare = (
item: {
onSelect: () => {
Clipboard.setString(params.copiableContent?.current.content || '')
displayMessage({
theme,
type: 'success',
message: t(`copy.succeed`)
})
displayMessage({ type: 'success', message: t(`copy.succeed`) })
},
disabled: false,
destructive: false,

View File

@ -178,6 +178,10 @@
"direct": "Habilita les notificacions push",
"settings": "Activa'ls a la configuració"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Activa per {{acct}}",
"description": "Els missatges s'envien a través del servidor del tooot"

View File

@ -178,6 +178,10 @@
"direct": "",
"settings": ""
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "",
"description": ""

View File

@ -178,6 +178,10 @@
"direct": "Push-Benachrichtigungen aktivieren",
"settings": "In den Einstellungen aktivieren"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Aktivieren für {{acct}}",
"description": "Nachrichten werden über den Tooot-Server geleitet"

View File

@ -178,6 +178,10 @@
"direct": "Enable push notification",
"settings": "Enable in settings"
},
"missingServerKey": {
"message": "Server misconfigured for push",
"description": "Please contact your server admin to configure push support"
},
"global": {
"heading": "Enable for {{acct}}",
"description": "Messages are routed through tooot's server"

View File

@ -178,6 +178,10 @@
"direct": "Habilitar notificaciones push",
"settings": "Activar en Ajustes"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Habilitar para {{acct}}",
"description": "Los mensajes se envían a través del servidor de tooot"

View File

@ -178,6 +178,10 @@
"direct": "Activer les notifications push",
"settings": "Activer dans les paramètres"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Activer pour {{acct}}",
"description": "Les messages sont acheminés via le serveur de tooot"

View File

@ -178,6 +178,10 @@
"direct": "Abilita notifiche push",
"settings": "Abilita nelle impostazioni"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Abilita per {{acct}}",
"description": "I messaggi dovranno attraversare i server di tooot"

View File

@ -178,6 +178,10 @@
"direct": "プッシュ通知を有効にする",
"settings": "設定で有効にする"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "{{acct}} の通知を有効にする",
"description": "メッセージは tooot のサーバー経由で到達します"

View File

@ -178,6 +178,10 @@
"direct": "푸시 알림 활성화",
"settings": "설정에서 활성화"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "{{acct}} 활성화",
"description": "메시지는 tooot의 서버를 거쳐 전달돼요"

View File

@ -178,6 +178,10 @@
"direct": "Push meldingen inschakelen",
"settings": "Inschakelen in instellingen"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Inschakelen voor {{acct}}",
"description": "Berichten worden doorgestuurd via tooot's server"

View File

@ -178,6 +178,10 @@
"direct": "",
"settings": ""
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "",
"description": ""

View File

@ -178,6 +178,10 @@
"direct": "Habilitar notificações via push",
"settings": "Ativar em configurações"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Habilitar para {{acct}}",
"description": "Mensagens são encaminhadas pelo servidor do tooot"

View File

@ -178,6 +178,10 @@
"direct": "Aktivera pushnotiser",
"settings": "Aktivera i inställningar"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Aktivera för {{acct}}",
"description": "Meddelanden dirigeras via tooots server"

View File

@ -178,6 +178,10 @@
"direct": "Bật thông báo đẩy",
"settings": "Bật trong cài đặt"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "Bật cho {{acct}}",
"description": "Thông báo được truyền qua máy chủ tooot"

View File

@ -178,6 +178,10 @@
"direct": "启用推送通知",
"settings": "在系统设置中启用"
},
"missingServerKey": {
"message": "服务器推送配置不正确",
"description": "请联系您的服务器管理员设置推送支持"
},
"global": {
"heading": "启用 {{acct}}",
"description": "通知消息将经由tooot服务器转发"

View File

@ -178,6 +178,10 @@
"direct": "啟用推播通知",
"settings": "在設定中啟用"
},
"missingServerKey": {
"message": "",
"description": ""
},
"global": {
"heading": "啟用 {{acct}}",
"description": "通知訊息將經由 tooot 伺服器轉發"

View File

@ -1,123 +0,0 @@
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import {
MutationVarsTimelineUpdateAccountProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from 'react-query'
export interface Props {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
account: Mastodon.Account
dismiss: () => void
}
const ActionsAccount: React.FC<Props> = ({
queryKey,
rootQueryKey,
account,
dismiss
}) => {
const { theme } = useTheme()
const { t } = useTranslation('componentTimeline')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
onSuccess: (_, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(
`shared.header.actions.account.${theParams.payload.property}.function`,
{
acct: account.acct
}
)
})
})
},
onError: (err: any, params) => {
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(
`shared.header.actions.account.${theParams.payload.property}.function`
)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
},
onSettled: () => {
queryKey && queryClient.invalidateQueries(queryKey)
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
}
})
return (
<MenuContainer>
<MenuHeader heading={t('shared.header.actions.account.heading')} />
<MenuRow
onPress={() => {
dismiss()
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'mute' }
})
}}
iconFront='EyeOff'
title={t('shared.header.actions.account.mute.button', {
acct: account.acct
})}
/>
<MenuRow
onPress={() => {
dismiss()
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block' }
})
}}
iconFront='XCircle'
title={t('shared.header.actions.account.block.button', {
acct: account.acct
})}
/>
<MenuRow
onPress={() => {
dismiss()
mutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'reports' }
})
}}
iconFront='Flag'
title={t('shared.header.actions.account.reports.button', {
acct: account.acct
})}
/>
</MenuContainer>
)
}
export default ActionsAccount

View File

@ -1,74 +0,0 @@
import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import { displayMessage } from '@components/Message'
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
export interface Props {
queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
domain: string
dismiss: () => void
}
const ActionsDomain: React.FC<Props> = ({ queryKey, rootQueryKey, domain, dismiss }) => {
const { theme } = useTheme()
const { t } = useTranslation('componentTimeline')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
onSettled: () => {
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`shared.header.actions.domain.block.function`)
})
})
queryClient.invalidateQueries(queryKey)
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
}
})
return (
<MenuContainer>
<MenuHeader heading={t(`shared.header.actions.domain.heading`)} />
<MenuRow
onPress={() =>
Alert.alert(
t('shared.header.actions.domain.alert.title', { domain }),
t('shared.header.actions.domain.alert.message'),
[
{
text: t('shared.header.actions.domain.alert.buttons.confirm'),
style: 'destructive',
onPress: () => {
dismiss()
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: domain
})
}
},
{
text: t('shared.header.actions.domain.alert.buttons.cancel'),
style: 'default'
}
]
)
}
iconFront='CloudOff'
title={t(`shared.header.actions.domain.block.button`, {
domain
})}
/>
</MenuContainer>
)
}
export default ActionsDomain

View File

@ -1,43 +0,0 @@
import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, Share } from 'react-native'
export interface Props {
type: 'status' | 'account'
url: string
dismiss: () => void
}
const ActionsShare: React.FC<Props> = ({ type, url, dismiss }) => {
const { t } = useTranslation('componentTimeline')
return (
<MenuContainer>
<MenuHeader heading={t(`shared.header.actions.share.${type}.heading`)} />
<MenuRow
iconFront='Share2'
title={t(`shared.header.actions.share.${type}.button`)}
onPress={async () => {
switch (Platform.OS) {
case 'ios':
await Share.share({
url
})
break
case 'android':
await Share.share({
message: url
})
break
}
dismiss()
}}
/>
</MenuContainer>
)
}
export default ActionsShare

View File

@ -1,231 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { displayMessage } from '@components/Message'
import { useTheme } from '@utils/styles/ThemeManager'
import apiInstance from '@api/instance'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { useSelector } from 'react-redux'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
export interface Props {
navigation: NativeStackNavigationProp<RootStackParamList, 'Screen-Actions'>
queryKey: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
status: Mastodon.Status
dismiss: () => void
}
const ActionsStatus: React.FC<Props> = ({
navigation,
queryKey,
rootQueryKey,
status,
dismiss
}) => {
const { theme } = useTheme()
const { t } = useTranslation('componentTimeline')
const queryClient = useQueryClient()
const mutation = useTimelineMutation({
onMutate: true,
onError: (err: any, params, oldData) => {
const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload
? (params as MutationVarsTimelineUpdateStatusProperty).payload.property
: 'delete'
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(`shared.header.actions.status.${theFunction}.function`)
}),
...(err?.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
queryClient.setQueryData(queryKey, oldData)
}
})
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
return (
<MenuContainer>
<MenuHeader heading={t('shared.header.actions.status.heading')} />
{canEditPost ? (
<MenuRow
onPress={async () => {
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
spoiler_text: Mastodon.Status['spoiler_text']
}>({
method: 'get',
url: `statuses/${status.id}/source`
}).then(res => {
dismiss()
navigation.navigate('Screen-Compose', {
type: 'edit',
incomingStatus: {
...status,
text: res.body.text,
spoiler_text: res.body.spoiler_text
},
...(replyToStatus && { replyToStatus }),
queryKey,
rootQueryKey
})
})
}}
iconFront='Edit3'
title={t('shared.header.actions.status.edit.button')}
/>
) : null}
<MenuRow
onPress={() => {
Alert.alert(
t('shared.header.actions.status.delete.alert.title'),
t('shared.header.actions.status.delete.alert.message'),
[
{
text: t('shared.header.actions.status.delete.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
dismiss()
mutation.mutate({
type: 'deleteItem',
source: 'statuses',
queryKey,
rootQueryKey,
id: status.id
})
}
},
{
text: t('shared.header.actions.status.delete.alert.buttons.cancel'),
style: 'default'
}
]
)
}}
iconFront='Trash'
title={t('shared.header.actions.status.delete.button')}
/>
<MenuRow
onPress={() => {
Alert.alert(
t('shared.header.actions.status.deleteEdit.alert.title'),
t('shared.header.actions.status.deleteEdit.alert.message'),
[
{
text: t('shared.header.actions.status.deleteEdit.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
mutation
.mutateAsync({
type: 'deleteItem',
source: 'statuses',
queryKey,
id: status.id
})
.then(res => {
dismiss()
navigation.navigate('Screen-Compose', {
type: 'deleteEdit',
incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }),
queryKey
})
})
}
},
{
text: t('shared.header.actions.status.deleteEdit.alert.buttons.cancel'),
style: 'default'
}
]
)
}}
iconFront='Edit'
title={t('shared.header.actions.status.deleteEdit.button')}
/>
<MenuRow
onPress={() => {
dismiss()
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
}
})
}}
iconFront='VolumeX'
title={
status.muted
? t('shared.header.actions.status.mute.button.negative')
: t('shared.header.actions.status.mute.button.positive')
}
/>
{/* Also note that reblogs cannot be pinned. */}
{(status.visibility === 'public' || status.visibility === 'unlisted') && (
<MenuRow
onPress={() => {
dismiss()
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
})
}}
iconFront='Anchor'
title={
status.pinned
? t('shared.header.actions.status.pin.button.negative')
: t('shared.header.actions.status.pin.button.positive')
}
/>
)}
</MenuContainer>
)
}
export default ActionsStatus

View File

@ -257,13 +257,17 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
() => (
<HeaderRight
type='text'
content={
params?.type
? params.type === 'conversation' && params.visibility === 'direct'
? t(`heading.right.button.${params.type}`)
: t('heading.right.button.default')
: t('heading.right.button.default')
}
content={t(
`heading.right.button.${
(params?.type &&
(params.type === 'conversation'
? params.visibility === 'direct'
? params.type
: 'default'
: params.type)) ||
'default'
}`
)}
onPress={() => {
composeDispatch({ type: 'posting', payload: true })

View File

@ -13,13 +13,13 @@ const ComposeRootFooter: React.FC<Props> = ({ accessibleRefAttachments }) => {
const { composeState } = useContext(ComposeContext)
return (
<>
<View>
{composeState.attachments.uploads.length ? (
<ComposeAttachments accessibleRefAttachments={accessibleRefAttachments} />
) : null}
{composeState.poll.active ? <ComposePoll /> : null}
{composeState.replyToStatus ? <ComposeReply /> : null}
</>
</View>
)
}

View File

@ -11,13 +11,10 @@ import ComposeTextInput from './Header/TextInput'
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
const instanceActive = useSelector(getInstanceActive)
const localInstances = useSelector(
getInstances,
(prev, next) => prev.length === next.length
)
const localInstances = useSelector(getInstances, (prev, next) => prev.length === next.length)
return (
<>
<View>
{instanceActive !== -1 && localInstances.length > 1 ? (
<View style={styles.postingAs}>
<ComposePostingAs />
@ -25,7 +22,7 @@ const ComposeRootHeader: React.FC = () => {
) : null}
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput />
</>
</View>
)
}

View File

@ -28,11 +28,9 @@ const ComposeTextInput: React.FC = () => {
<PasteInput
keyboardAppearance={mode}
style={{
...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.M,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
color: colors.primaryDefault,
borderBottomColor: colors.border,
fontSize: adaptedFontsize,

View File

@ -2,22 +2,19 @@ import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { CameraRoll } from '@react-native-camera-roll/camera-roll'
import { RootStackParamList } from '@utils/navigation/navigators'
import { Theme } from '@utils/styles/themes'
import * as FileSystem from 'expo-file-system'
import i18next from 'i18next'
import { PermissionsAndroid, Platform } from 'react-native'
type CommonProps = {
theme: Theme
image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
}
const saveIos = async ({ theme, image }: CommonProps) => {
const saveIos = async ({ image }: CommonProps) => {
CameraRoll.save(image.url)
.then(() => {
haptics('Success')
displayMessage({
theme,
type: 'success',
message: i18next.t('screenImageViewer:content.save.succeed')
})
@ -28,7 +25,6 @@ const saveIos = async ({ theme, image }: CommonProps) => {
.then(() => {
haptics('Success')
displayMessage({
theme,
type: 'success',
message: i18next.t('screenImageViewer:content.save.succeed')
})
@ -36,32 +32,31 @@ const saveIos = async ({ theme, image }: CommonProps) => {
.catch(() => {
haptics('Error')
displayMessage({
theme,
type: 'error',
type: 'danger',
message: i18next.t('screenImageViewer:content.save.failed')
})
})
} else {
haptics('Error')
displayMessage({
theme,
type: 'error',
type: 'danger',
message: i18next.t('screenImageViewer:content.save.failed')
})
}
})
}
const saveAndroid = async ({ theme, image }: CommonProps) => {
const saveAndroid = async ({ image }: CommonProps) => {
const fileUri: string = `${FileSystem.documentDirectory}${image.id}.jpg`
const downloadedFile: FileSystem.FileSystemDownloadResult =
await FileSystem.downloadAsync(image.url, fileUri)
const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync(
image.url,
fileUri
)
if (downloadedFile.status != 200) {
haptics('Error')
displayMessage({
theme,
type: 'error',
type: 'danger',
message: i18next.t('screenImageViewer:content.save.failed')
})
return
@ -75,8 +70,7 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
if (status !== 'granted') {
haptics('Error')
displayMessage({
theme,
type: 'error',
type: 'danger',
message: i18next.t('screenImageViewer:content.save.failed')
})
return
@ -87,7 +81,6 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
.then(() => {
haptics('Success')
displayMessage({
theme,
type: 'success',
message: i18next.t('screenImageViewer:content.save.succeed')
})
@ -95,8 +88,7 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
.catch(() => {
haptics('Error')
displayMessage({
theme,
type: 'error',
type: 'danger',
message: i18next.t('screenImageViewer:content.save.failed')
})
})

View File

@ -40,7 +40,7 @@ const ScreenImagesViewer = ({
const insets = useSafeAreaInsets()
const { mode, theme } = useTheme()
const { mode } = useTheme()
const { t } = useTranslation('screenImageViewer')
const initialIndex = imageUrls.findIndex(image => image.id === id)
@ -61,7 +61,7 @@ const ScreenImagesViewer = ({
async buttonIndex => {
switch (buttonIndex) {
case 0:
saveImage({ theme, image: imageUrls[currentIndex] })
saveImage({ image: imageUrls[currentIndex] })
break
case 1:
switch (Platform.OS) {
@ -188,7 +188,7 @@ const ScreenImagesViewer = ({
async buttonIndex => {
switch (buttonIndex) {
case 0:
saveImage({ theme, image: imageUrls[currentIndex] })
saveImage({ image: imageUrls[currentIndex] })
break
case 1:
switch (Platform.OS) {

View File

@ -129,15 +129,11 @@ const TabMe = React.memo(
name='Tab-Me-Push'
component={TabMePush}
options={({ navigation }) => ({
presentation: 'modal',
headerShown: true,
title: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.push.name')} />
}),
headerLeft: () => (
<HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
)
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>
<Stack.Screen

View File

@ -41,8 +41,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
},
onError: () => {
displayMessage({
theme,
type: 'error',
type: 'danger',
message: t('common:message.error.message', {
function: t('me.listAccounts.error')
})

View File

@ -41,8 +41,7 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
onError: () => {
displayMessage({
ref: messageRef,
theme,
type: 'error',
type: 'danger',
message: t('common:message.error.message', {
function:
params.type === 'add' ? t('me.stacks.listAdd.name') : t('me.stacks.listEdit.name')

View File

@ -17,7 +17,7 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
navigation,
route: { key, params }
}) => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list: params.id }]
@ -30,8 +30,7 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
},
onError: () => {
displayMessage({
theme,
type: 'error',
type: 'danger',
message: t('common:message.error.message', {
function: t('me.listDelete.heading')
})

View File

@ -121,7 +121,6 @@ const TabMeProfileFields: React.FC<
content='Save'
onPress={async () => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.note.title',

View File

@ -75,7 +75,6 @@ const TabMeProfileName: React.FC<
content='Save'
onPress={async () => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.name.title',

View File

@ -75,7 +75,6 @@ const TabMeProfileNote: React.FC<
content='Save'
onPress={async () => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.note.title',

View File

@ -5,7 +5,7 @@ import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback } from 'react'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
import { ScrollView } from 'react-native-gesture-handler'
@ -16,7 +16,7 @@ const TabMeProfileRoot: React.FC<
messageRef: RefObject<FlashMessage>
}
> = ({ messageRef, navigation }) => {
const { mode, theme } = useTheme()
const { mode } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
@ -25,90 +25,6 @@ const TabMeProfileRoot: React.FC<
const { mutateAsync } = useProfileMutation()
const dispatch = useAppDispatch()
const onPressVisibility = useCallback(() => {
showActionSheetWithOptions(
{
title: t('me.profile.root.visibility.title'),
options: [
t('me.profile.root.visibility.options.public'),
t('me.profile.root.visibility.options.unlisted'),
t('me.profile.root.visibility.options.private'),
t('common:buttons.cancel')
],
cancelButtonIndex: 3,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
case 1:
case 2:
const indexVisibilityMapping = ['public', 'unlisted', 'private'] as [
'public',
'unlisted',
'private'
]
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.visibility.title',
succeed: false,
failed: true
},
type: 'source[privacy]',
data: indexVisibilityMapping[buttonIndex]
}).then(() => dispatch(updateAccountPreferences()))
}
break
}
}
)
}, [theme, data?.source?.privacy])
const onPressSensitive = useCallback(() => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.sensitive.title',
succeed: false,
failed: true
},
type: 'source[sensitive]',
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences()))
}, [data?.source.sensitive])
const onPressLock = useCallback(() => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.lock.title',
succeed: false,
failed: true
},
type: 'locked',
data: data?.locked === undefined ? true : !data.locked
})
}, [theme, data?.locked])
const onPressBot = useCallback(() => {
mutateAsync({
theme,
messageRef,
message: {
text: 'me.profile.root.bot.title',
succeed: false,
failed: true
},
type: 'bot',
data: data?.bot === undefined ? true : !data.bot
})
}, [theme, data?.bot])
return (
<ScrollView>
<MenuContainer>
@ -166,12 +82,62 @@ const TabMeProfileRoot: React.FC<
}
loading={isLoading}
iconBack='ChevronRight'
onPress={onPressVisibility}
onPress={() =>
showActionSheetWithOptions(
{
title: t('me.profile.root.visibility.title'),
options: [
t('me.profile.root.visibility.options.public'),
t('me.profile.root.visibility.options.unlisted'),
t('me.profile.root.visibility.options.private'),
t('common:buttons.cancel')
],
cancelButtonIndex: 3,
userInterfaceStyle: mode
},
async buttonIndex => {
switch (buttonIndex) {
case 0:
case 1:
case 2:
const indexVisibilityMapping = ['public', 'unlisted', 'private'] as [
'public',
'unlisted',
'private'
]
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
mutateAsync({
messageRef,
message: {
text: 'me.profile.root.visibility.title',
succeed: false,
failed: true
},
type: 'source[privacy]',
data: indexVisibilityMapping[buttonIndex]
}).then(() => dispatch(updateAccountPreferences()))
}
break
}
}
)
}
/>
<MenuRow
title={t('me.profile.root.sensitive.title')}
switchValue={data?.source.sensitive}
switchOnValueChange={onPressSensitive}
switchOnValueChange={() =>
mutateAsync({
messageRef,
message: {
text: 'me.profile.root.sensitive.title',
succeed: false,
failed: true
},
type: 'source[sensitive]',
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
}).then(() => dispatch(updateAccountPreferences()))
}
loading={isLoading}
/>
</MenuContainer>
@ -180,14 +146,36 @@ const TabMeProfileRoot: React.FC<
title={t('me.profile.root.lock.title')}
description={t('me.profile.root.lock.description')}
switchValue={data?.locked}
switchOnValueChange={onPressLock}
switchOnValueChange={() =>
mutateAsync({
messageRef,
message: {
text: 'me.profile.root.lock.title',
succeed: false,
failed: true
},
type: 'locked',
data: data?.locked === undefined ? true : !data.locked
})
}
loading={isLoading}
/>
<MenuRow
title={t('me.profile.root.bot.title')}
description={t('me.profile.root.bot.description')}
switchValue={data?.bot}
switchOnValueChange={onPressBot}
switchOnValueChange={() =>
mutateAsync({
messageRef,
message: {
text: 'me.profile.root.bot.title',
succeed: false,
failed: true
},
type: 'bot',
data: data?.bot === undefined ? true : !data.bot
})
}
loading={isLoading}
/>
</MenuContainer>

View File

@ -3,7 +3,6 @@ import { MenuRow } from '@components/Menu'
import { displayMessage } from '@components/Message'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject } from 'react'
import { useTranslation } from 'react-i18next'
import FlashMessage from 'react-native-flash-message'
@ -14,7 +13,6 @@ export interface Props {
}
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
const { theme } = useTheme()
const { t } = useTranslation('screenTabs')
const { showActionSheetWithOptions } = useActionSheet()
@ -40,7 +38,6 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
})
if (image[0].uri) {
mutation.mutate({
theme,
messageRef,
message: {
text: `me.profile.root.${type}.title`,
@ -54,8 +51,7 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
displayMessage({
ref: messageRef,
message: t('screenTabs:me.profile.mediaSelectionFailed'),
theme: theme,
type: 'error'
type: 'danger'
})
}
}}

View File

@ -7,7 +7,6 @@ import { useAppDispatch } from '@root/store'
import * as Sentry from '@sentry/react-native'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { disableAllPushes, getInstances } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -16,7 +15,6 @@ import { useSelector } from 'react-redux'
const pushUseConnect = () => {
const { t } = useTranslation('screens')
const { theme } = useTheme()
const dispatch = useAppDispatch()
useEffect(() => {
@ -39,8 +37,7 @@ const pushUseConnect = () => {
Notifications.setBadgeCountAsync(0)
if (error?.status == 404) {
displayMessage({
theme,
type: 'error',
type: 'danger',
duration: 'long',
message: t('pushError.message'),
description: t('pushError.description'),

View File

@ -2,15 +2,13 @@ import apiInstance from '@api/instance'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import queryClient from '@helpers/queryClient'
import { Theme } from '@utils/styles/themes'
import { AxiosError } from 'axios'
import i18next from 'i18next'
import { RefObject } from 'react'
import FlashMessage from 'react-native-flash-message'
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
type AccountWithSource = Mastodon.Account &
Required<Pick<Mastodon.Account, 'source'>>
type AccountWithSource = Mastodon.Account & Required<Pick<Mastodon.Account, 'source'>>
type QueryKeyProfile = ['Profile']
const queryKey: QueryKeyProfile = ['Profile']
@ -52,7 +50,6 @@ type MutationVarsProfileBase =
}
type MutationVarsProfile = MutationVarsProfileBase & {
theme: Theme
messageRef: RefObject<FlashMessage>
message: {
text: string
@ -92,73 +89,70 @@ const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
}
const useProfileMutation = () => {
return useMutation<
{ body: AccountWithSource },
AxiosError,
MutationVarsProfile
>(mutationFunction, {
onMutate: async variables => {
await queryClient.cancelQueries(queryKey)
return useMutation<{ body: AccountWithSource }, AxiosError, MutationVarsProfile>(
mutationFunction,
{
onMutate: async variables => {
await queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData<AccountWithSource>(queryKey)
const oldData = queryClient.getQueryData<AccountWithSource>(queryKey)
queryClient.setQueryData<AccountWithSource | undefined>(queryKey, old => {
if (old) {
switch (variables.type) {
case 'source[privacy]':
return {
...old,
source: { ...old.source, privacy: variables.data }
}
case 'source[sensitive]':
return {
...old,
source: { ...old.source, sensitive: variables.data }
}
case 'locked':
return { ...old, locked: variables.data }
case 'bot':
return { ...old, bot: variables.data }
default:
return old
queryClient.setQueryData<AccountWithSource | undefined>(queryKey, old => {
if (old) {
switch (variables.type) {
case 'source[privacy]':
return {
...old,
source: { ...old.source, privacy: variables.data }
}
case 'source[sensitive]':
return {
...old,
source: { ...old.source, sensitive: variables.data }
}
case 'locked':
return { ...old, locked: variables.data }
case 'bot':
return { ...old, bot: variables.data }
default:
return old
}
}
}
})
})
return oldData
},
onError: (err, variables, context) => {
queryClient.setQueryData(queryKey, context)
haptics('Error')
if (variables.message.failed) {
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.failed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
...(err && { description: err.message }),
theme: variables.theme,
type: 'error'
})
return oldData
},
onError: (err, variables, context) => {
queryClient.setQueryData(queryKey, context)
haptics('Error')
if (variables.message.failed) {
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.failed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
...(err && { description: err.message }),
type: 'danger'
})
}
},
onSuccess: (_, variables) => {
if (variables.message.succeed) {
haptics('Success')
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
type: 'success'
})
}
},
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
},
onSuccess: (_, variables) => {
if (variables.message.succeed) {
haptics('Success')
displayMessage({
ref: variables.messageRef,
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
type: i18next.t(`screenTabs:${variables.message.text}`)
}),
theme: variables.theme,
type: 'success'
})
}
},
onSettled: () => {
queryClient.invalidateQueries(queryKey)
}
})
)
}
export { useProfileQuery, useProfileMutation }

View File

@ -1,12 +1,15 @@
import apiInstance from '@api/instance'
import apiTooot, { TOOOT_API_DOMAIN } from '@api/tooot'
import { displayMessage } from '@components/Message'
import i18n from '@root/i18n/i18n'
import { RootState } from '@root/store'
import * as Sentry from '@sentry/react-native'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { getInstance } from '@utils/slices/instancesSlice'
import { Theme } from '@utils/styles/themes'
import * as Notifications from 'expo-notifications'
import * as Random from 'expo-random'
import i18next from 'i18next'
import { Platform } from 'react-native'
import base64 from 'react-native-base64'
import androidDefaults from './androidDefaults'
@ -74,6 +77,12 @@ const pushRegister = async (
})
if (!res.body.server_key?.length) {
displayMessage({
type: 'danger',
duration: 'long',
message: i18next.t('screenTabs:me.push.missingServerKey.message'),
description: i18next.t('screenTabs:me.push.missingServerKey.description')
})
Sentry.setContext('Push server key', {
instance: instanceUri,
resBody: res.body