Improve push error messaging

This commit is contained in:
xmflsct 2022-12-10 01:59:26 +01:00
parent 1a069d5acc
commit 748351026f
6 changed files with 238 additions and 240 deletions

View File

@ -1,3 +1,5 @@
Enjoy toooting! This version includes following improvements and fixes:
- Automatic setting detected language when tooting
- Fix whole word filter matching
- Added notification for admins
- Fix whole word filter matching
- Fix tablet cannot delete toot drafts

View File

@ -1,3 +1,5 @@
toooting愉快此版本包括以下改进和修复
- 自动识别发嘟语言
- 修复过滤整词功能
- 新增管理员推送通知
- 修复过滤整词功能
- 修复平板不能删除草稿

View File

@ -1,90 +0,0 @@
import browserPackage from '@helpers/browserPackage'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
import addInstance from '@utils/slices/instances/add'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import * as AuthSession from 'expo-auth-session'
import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
instanceDomain: string
// Domain can be different than uri
instance: Mastodon.Instance
appData: InstanceLatest['appData']
goBack?: boolean
}
const InstanceAuth = React.memo(
({ instanceDomain, instance, appData, goBack }: Props) => {
const redirectUri = AuthSession.makeRedirectUri({
native: 'tooot://instance-auth',
useProxy: false
})
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const queryClient = useQueryClient()
const dispatch = useAppDispatch()
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: deprecateAuthFollow
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push'],
redirectUri
},
{
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
}
)
useEffect(() => {
;(async () => {
if (request?.clientId) {
await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e))
}
})()
}, [request])
useEffect(() => {
;(async () => {
if (response?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri,
code: response.params.code,
extraParams: {
grant_type: 'authorization_code'
}
},
{
tokenEndpoint: `https://${instanceDomain}/oauth/token`
}
)
queryClient.clear()
dispatch(
addInstance({
domain: instanceDomain,
token: accessToken,
instance,
appData
})
)
goBack && navigation.goBack()
}
})()
}, [response])
return <></>
},
() => true
)
export default InstanceAuth

View File

@ -1,22 +1,27 @@
import Button from '@components/Button'
import Icon from '@components/Icon'
import browserPackage from '@helpers/browserPackage'
import { useAppsQuery } from '@utils/queryHooks/apps'
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { getInstances } from '@utils/slices/instancesSlice'
import { checkInstanceFeature, getInstances } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash'
import React, { RefObject, useCallback, useMemo, useState } from 'react'
import React, { RefObject, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder'
import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info'
import CustomText from './Text'
import InstanceInfo from './Info'
import CustomText from '../Text'
import { useNavigation } from '@react-navigation/native'
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
import queryClient from '@helpers/queryClient'
import { useAppDispatch } from '@root/store'
import addInstance from '@utils/slices/instances/add'
export interface Props {
scrollViewRef?: RefObject<ScrollView>
@ -31,30 +36,64 @@ const ComponentInstance: React.FC<Props> = ({
}) => {
const { t } = useTranslation('componentInstance')
const { colors, mode } = useTheme()
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const [domain, setDomain] = useState<string>('')
const dispatch = useAppDispatch()
const instances = useSelector(getInstances, () => true)
const [domain, setDomain] = useState<string>()
const instanceQuery = useInstanceQuery({
domain,
options: { enabled: !!domain, retry: false }
})
const appsQuery = useAppsQuery({
domain,
options: { enabled: false, retry: false }
})
const onChangeText = useCallback(
debounce(
text => {
setDomain(text.replace(/^http(s)?\:\/\//i, ''))
appsQuery.remove()
},
1000,
{ trailing: true }
),
[]
)
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
const appsMutation = useAppsMutation({
retry: false,
onSuccess: async (data, variables) => {
const clientId = data.client_id
const clientSecret = data.client_secret
const discovery = { authorizationEndpoint: `https://${domain}/oauth/authorize` }
const request = new AuthSession.AuthRequest({
clientId,
clientSecret,
scopes: deprecateAuthFollow
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push'],
redirectUri
})
await request.makeAuthUrlAsync(discovery)
const promptResult = await request.promptAsync(discovery)
if (promptResult?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId,
clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri,
code: promptResult.params.code,
extraParams: { grant_type: 'authorization_code' }
},
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
)
queryClient.clear()
dispatch(
addInstance({
domain,
token: accessToken,
instance: instanceQuery.data!,
appData: { clientId, clientSecret }
})
)
goBack && navigation.goBack()
}
}
})
const processUpdate = useCallback(() => {
if (domain) {
@ -66,39 +105,15 @@ const ComponentInstance: React.FC<Props> = ({
},
{
text: t('common:buttons.continue'),
onPress: () => {
appsQuery.refetch()
}
onPress: () => appsMutation.mutate({ domain })
}
])
} else {
appsQuery.refetch()
appsMutation.mutate({ domain })
}
}
}, [domain])
const requestAuth = useMemo(() => {
if (
domain &&
instanceQuery.data?.uri &&
appsQuery.data?.client_id &&
appsQuery.data.client_secret
) {
return (
<InstanceAuth
key={Math.random()}
instanceDomain={domain}
instance={instanceQuery.data}
appData={{
clientId: appsQuery.data.client_id,
clientSecret: appsQuery.data.client_secret
}}
goBack={goBack}
/>
)
}
}, [domain, instanceQuery.data, appsQuery.data])
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
@ -145,7 +160,9 @@ const ComponentInstance: React.FC<Props> = ({
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
}}
onChangeText={onChangeText}
onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, {
trailing: true
})}
autoCapitalize='none'
clearButtonMode='never'
keyboardType='url'
@ -176,7 +193,7 @@ const ComponentInstance: React.FC<Props> = ({
content={t('server.button')}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching}
loading={instanceQuery.isFetching || appsMutation.isLoading}
/>
</View>
@ -276,8 +293,6 @@ const ComponentInstance: React.FC<Props> = ({
</View>
</View>
</View>
{requestAuth}
</KeyboardAvoidingView>
)
}

View File

@ -5,6 +5,7 @@ import CustomText from '@components/Text'
import browserPackage from '@helpers/browserPackage'
import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import {
@ -30,7 +31,16 @@ import { useSelector } from 'react-redux'
const TabMePush: React.FC = () => {
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const instance = useSelector(getInstance)
const expoToken = useSelector(getExpoToken)
const [serverKeyAvailable, setServerKeyAvailable] = useState<boolean>()
useAppsQuery({
options: {
onSuccess: data => setServerKeyAvailable(!!data.vapid_key)
}
})
const dispatch = useAppDispatch()
const instancePush = useSelector(getInstancePush)
@ -38,36 +48,39 @@ const TabMePush: React.FC = () => {
const [pushAvailable, setPushAvailable] = useState<boolean>()
const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
const expoToken = useSelector(getExpoToken)
const checkPush = async () => {
switch (Platform.OS) {
case 'ios':
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
break
case 'android':
await setChannels(instance)
layoutAnimation()
dispatch(retrieveExpoToken())
break
}
}
useEffect(() => {
checkPush()
if (isDevelopment) {
setPushAvailable(true)
} else {
setPushAvailable(!!expoToken)
useEffect(() => {
const checkPush = async () => {
switch (Platform.OS) {
case 'ios':
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
break
case 'android':
await setChannels(instance)
layoutAnimation()
dispatch(retrieveExpoToken())
break
}
}
if (serverKeyAvailable) {
checkPush()
if (isDevelopment) {
setPushAvailable(true)
} else {
setPushAvailable(!!expoToken)
}
}
const subscription = AppState.addEventListener('change', checkPush)
return () => {
subscription.remove()
}
}, [])
}, [serverKeyAvailable])
const alerts = () =>
instancePush?.alerts
@ -120,63 +133,91 @@ const TabMePush: React.FC = () => {
return (
<ScrollView>
{!!pushAvailable ? (
{!!serverKeyAvailable ? (
<>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
Linking.openSettings()
{!!pushAvailable ? (
<>
{pushEnabled === false ? (
<MenuContainer>
<Button
type='text'
content={
pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings')
}
style={{
marginTop: StyleConstants.Spacing.Global.PagePadding,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
}}
onPress={async () => {
if (pushCanAskAgain) {
const result = await Notifications.requestPermissionsAsync()
setPushEnabled(result.granted)
setPushCanAskAgain(result.canAskAgain)
} else {
Linking.openSettings()
}
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instance.account.acct}@${instance.uri}`
})}
description={t('me.push.global.description')}
switchDisabled={!pushEnabled}
switchValue={pushEnabled === false ? false : instancePush?.global}
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global))}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode}
switchDisabled={!pushEnabled || !instancePush?.global}
switchValue={instancePush?.decode}
switchOnValueChange={() =>
dispatch(updateInstancePushDecode(!instancePush?.decode))
}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works', {
browserPackage: await browserPackage()
})
}
/>
</MenuContainer>
<MenuContainer children={alerts()} />
<MenuContainer children={adminAlerts()} />
</>
) : (
<View
style={{
flex: 1,
minHeight: '100%',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
<CustomText
fontStyle='M'
style={{
color: colors.primaryDefault,
textAlign: 'center',
marginTop: StyleConstants.Spacing.S
}}
/>
</MenuContainer>
) : null}
<MenuContainer>
<MenuRow
title={t('me.push.global.heading', {
acct: `@${instance.account.acct}@${instance.uri}`
})}
description={t('me.push.global.description')}
switchDisabled={!pushEnabled}
switchValue={pushEnabled === false ? false : instancePush?.global}
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global))}
/>
</MenuContainer>
<MenuContainer>
<MenuRow
title={t('me.push.decode.heading')}
description={t('me.push.decode.description')}
loading={instancePush?.decode}
switchDisabled={!pushEnabled || !instancePush?.global}
switchValue={instancePush?.decode}
switchOnValueChange={() => dispatch(updateInstancePushDecode(!instancePush?.decode))}
/>
<MenuRow
title={t('me.push.howitworks')}
iconBack='ExternalLink'
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works', {
browserPackage: await browserPackage()
})
}
/>
</MenuContainer>
<MenuContainer children={alerts()} />
<MenuContainer children={adminAlerts()} />
>
{t('me.push.notAvailable')}
</CustomText>
</View>
)}
</>
) : (
<View
@ -197,7 +238,16 @@ const TabMePush: React.FC = () => {
marginTop: StyleConstants.Spacing.S
}}
>
{t('me.push.notAvailable')}
{t('me.push.missingServerKey.message')}
</CustomText>
<CustomText
fontStyle='S'
style={{
color: colors.primaryDefault,
textAlign: 'center'
}}
>
{t('me.push.missingServerKey.description')}
</CustomText>
</View>
)}

View File

@ -1,18 +1,41 @@
import apiGeneral from '@api/general'
import apiInstance from '@api/instance'
import { AxiosError } from 'axios'
import * as AuthSession from 'expo-auth-session'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query'
import {
QueryFunctionContext,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from 'react-query'
export type QueryKeyApps = ['Apps', { domain?: string }]
export type QueryKeyApps = ['Apps']
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
const redirectUri = AuthSession.makeRedirectUri({
native: 'tooot://instance-auth',
useProxy: false
const queryFunctionApps = async ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
const res = await apiInstance<Mastodon.Apps>({
method: 'get',
url: 'apps/verify_credentials'
})
return res.body
}
const { domain } = queryKey[1]
const useAppsQuery = (
params: {
options?: UseQueryOptions<Mastodon.Apps, AxiosError>
} | void
) => {
const queryKey: QueryKeyApps = ['Apps']
return useQuery(queryKey, queryFunctionApps, params?.options)
}
type MutationVarsApps = { domain: string }
export const redirectUri = AuthSession.makeRedirectUri({
native: 'tooot://instance-auth',
useProxy: false
})
const mutationFunctionApps = async ({ domain }: MutationVarsApps) => {
const formData = new FormData()
formData.append('client_name', 'tooot')
formData.append('website', 'https://tooot.app')
@ -21,20 +44,16 @@ const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
return apiGeneral<Mastodon.Apps>({
method: 'post',
domain: domain || '',
domain: domain,
url: `api/v1/apps`,
body: formData
}).then(res => res.body)
}
const useAppsQuery = ({
options,
...queryKeyParams
}: QueryKeyApps[1] & {
options?: UseQueryOptions<Mastodon.Apps, AxiosError>
}) => {
const queryKey: QueryKeyApps = ['Apps', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
const useAppsMutation = (
options: UseMutationOptions<Mastodon.Apps, AxiosError, MutationVarsApps>
) => {
return useMutation(mutationFunctionApps, options)
}
export { useAppsQuery }
export { useAppsQuery, useAppsMutation }