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: Enjoy toooting! This version includes following improvements and fixes:
- Automatic setting detected language when tooting - 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愉快此版本包括以下改进和修复 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 Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import browserPackage from '@helpers/browserPackage' 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 { 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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash' 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 { Trans, useTranslation } from 'react-i18next'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder' import { Placeholder } from 'rn-placeholder'
import InstanceAuth from './Instance/Auth' import InstanceInfo from './Info'
import InstanceInfo from './Instance/Info' import CustomText from '../Text'
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 { export interface Props {
scrollViewRef?: RefObject<ScrollView> scrollViewRef?: RefObject<ScrollView>
@ -31,30 +36,64 @@ const ComponentInstance: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation('componentInstance') const { t } = useTranslation('componentInstance')
const { colors, mode } = useTheme() 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 instances = useSelector(getInstances, () => true)
const [domain, setDomain] = useState<string>()
const instanceQuery = useInstanceQuery({ const instanceQuery = useInstanceQuery({
domain, domain,
options: { enabled: !!domain, retry: false } options: { enabled: !!domain, retry: false }
}) })
const appsQuery = useAppsQuery({
domain,
options: { enabled: false, retry: false }
})
const onChangeText = useCallback( const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
debounce(
text => { const appsMutation = useAppsMutation({
setDomain(text.replace(/^http(s)?\:\/\//i, '')) retry: false,
appsQuery.remove() onSuccess: async (data, variables) => {
}, const clientId = data.client_id
1000, const clientSecret = data.client_secret
{ trailing: true }
), 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(() => { const processUpdate = useCallback(() => {
if (domain) { if (domain) {
@ -66,39 +105,15 @@ const ComponentInstance: React.FC<Props> = ({
}, },
{ {
text: t('common:buttons.continue'), text: t('common:buttons.continue'),
onPress: () => { onPress: () => appsMutation.mutate({ domain })
appsQuery.refetch()
}
} }
]) ])
} else { } else {
appsQuery.refetch() appsMutation.mutate({ domain })
} }
} }
}, [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 ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
@ -145,7 +160,9 @@ const ComponentInstance: React.FC<Props> = ({
color: colors.primaryDefault, color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError ? colors.red : colors.border borderBottomColor: instanceQuery.isError ? colors.red : colors.border
}} }}
onChangeText={onChangeText} onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, {
trailing: true
})}
autoCapitalize='none' autoCapitalize='none'
clearButtonMode='never' clearButtonMode='never'
keyboardType='url' keyboardType='url'
@ -176,7 +193,7 @@ const ComponentInstance: React.FC<Props> = ({
content={t('server.button')} content={t('server.button')}
onPress={processUpdate} onPress={processUpdate}
disabled={!instanceQuery.data?.uri} disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching} loading={instanceQuery.isFetching || appsMutation.isLoading}
/> />
</View> </View>
@ -276,8 +293,6 @@ const ComponentInstance: React.FC<Props> = ({
</View> </View>
</View> </View>
</View> </View>
{requestAuth}
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }

View File

@ -5,6 +5,7 @@ import CustomText from '@components/Text'
import browserPackage from '@helpers/browserPackage' import browserPackage from '@helpers/browserPackage'
import { useAppDispatch } from '@root/store' import { useAppDispatch } from '@root/store'
import { isDevelopment } from '@utils/checkEnvironment' import { isDevelopment } from '@utils/checkEnvironment'
import { useAppsQuery } from '@utils/queryHooks/apps'
import { useProfileQuery } from '@utils/queryHooks/profile' import { useProfileQuery } from '@utils/queryHooks/profile'
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice' import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
import { import {
@ -30,7 +31,16 @@ import { useSelector } from 'react-redux'
const TabMePush: React.FC = () => { const TabMePush: React.FC = () => {
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const instance = useSelector(getInstance) 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 dispatch = useAppDispatch()
const instancePush = useSelector(getInstancePush) const instancePush = useSelector(getInstancePush)
@ -38,36 +48,39 @@ const TabMePush: React.FC = () => {
const [pushAvailable, setPushAvailable] = useState<boolean>() const [pushAvailable, setPushAvailable] = useState<boolean>()
const [pushEnabled, setPushEnabled] = useState<boolean>() const [pushEnabled, setPushEnabled] = useState<boolean>()
const [pushCanAskAgain, setPushCanAskAgain] = 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) { useEffect(() => {
setPushAvailable(true) const checkPush = async () => {
} else { switch (Platform.OS) {
setPushAvailable(!!expoToken) 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) const subscription = AppState.addEventListener('change', checkPush)
return () => { return () => {
subscription.remove() subscription.remove()
} }
}, []) }, [serverKeyAvailable])
const alerts = () => const alerts = () =>
instancePush?.alerts instancePush?.alerts
@ -120,63 +133,91 @@ const TabMePush: React.FC = () => {
return ( return (
<ScrollView> <ScrollView>
{!!pushAvailable ? ( {!!serverKeyAvailable ? (
<> <>
{pushEnabled === false ? ( {!!pushAvailable ? (
<MenuContainer> <>
<Button {pushEnabled === false ? (
type='text' <MenuContainer>
content={ <Button
pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings') type='text'
} content={
style={{ pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings')
marginTop: StyleConstants.Spacing.Global.PagePadding, }
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2 style={{
}} marginTop: StyleConstants.Spacing.Global.PagePadding,
onPress={async () => { marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
if (pushCanAskAgain) { }}
const result = await Notifications.requestPermissionsAsync() onPress={async () => {
setPushEnabled(result.granted) if (pushCanAskAgain) {
setPushCanAskAgain(result.canAskAgain) const result = await Notifications.requestPermissionsAsync()
} else { setPushEnabled(result.granted)
Linking.openSettings() 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> {t('me.push.notAvailable')}
) : null} </CustomText>
<MenuContainer> </View>
<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 <View
@ -197,7 +238,16 @@ const TabMePush: React.FC = () => {
marginTop: StyleConstants.Spacing.S 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> </CustomText>
</View> </View>
)} )}

View File

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