diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 8f5ab42e..103e95c2 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,5 @@ Enjoy toooting! This version includes following improvements and fixes: - Automatic setting detected language when tooting -- Fix whole word filter matching \ No newline at end of file +- Added notification for admins +- Fix whole word filter matching +- Fix tablet cannot delete toot drafts \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index c4bf0028..b7098afa 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,3 +1,5 @@ toooting愉快!此版本包括以下改进和修复: - 自动识别发嘟语言 -- 修复过滤整词功能 \ No newline at end of file +- 新增管理员推送通知 +- 修复过滤整词功能 +- 修复平板不能删除草稿 \ No newline at end of file diff --git a/src/components/Instance/Auth.tsx b/src/components/Instance/Auth.tsx deleted file mode 100644 index 1a72e2b5..00000000 --- a/src/components/Instance/Auth.tsx +++ /dev/null @@ -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>() - 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 diff --git a/src/components/Instance.tsx b/src/components/Instance/index.tsx similarity index 75% rename from src/components/Instance.tsx rename to src/components/Instance/index.tsx index b0039b37..80aecd9e 100644 --- a/src/components/Instance.tsx +++ b/src/components/Instance/index.tsx @@ -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 @@ -31,30 +36,64 @@ const ComponentInstance: React.FC = ({ }) => { const { t } = useTranslation('componentInstance') const { colors, mode } = useTheme() + const navigation = useNavigation>() + const [domain, setDomain] = useState('') + + const dispatch = useAppDispatch() const instances = useSelector(getInstances, () => true) - const [domain, setDomain] = useState() - 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 = ({ }, { 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 ( - - ) - } - }, [domain, instanceQuery.data, appsQuery.data]) - return ( = ({ 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 = ({ content={t('server.button')} onPress={processUpdate} disabled={!instanceQuery.data?.uri} - loading={instanceQuery.isFetching || appsQuery.isFetching} + loading={instanceQuery.isFetching || appsMutation.isLoading} /> @@ -276,8 +293,6 @@ const ComponentInstance: React.FC = ({ - - {requestAuth} ) } diff --git a/src/screens/Tabs/Me/Push.tsx b/src/screens/Tabs/Me/Push.tsx index 8c72a32d..fd37f3ca 100644 --- a/src/screens/Tabs/Me/Push.tsx +++ b/src/screens/Tabs/Me/Push.tsx @@ -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() + 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() const [pushEnabled, setPushEnabled] = useState() const [pushCanAskAgain, setPushCanAskAgain] = useState() - 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 ( - {!!pushAvailable ? ( + {!!serverKeyAvailable ? ( <> - {pushEnabled === false ? ( - -