tooot/src/components/Instance/index.tsx

411 lines
13 KiB
TypeScript
Raw Normal View History

2021-01-07 19:13:09 +01:00
import Button from '@components/Button'
import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native'
import apiGeneral from '@utils/api/general'
import browserPackage from '@utils/helpers/browserPackage'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
2023-01-03 23:57:23 +01:00
import { queryClient } from '@utils/queryHooks'
2022-12-10 01:59:26 +01:00
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
2021-01-11 21:36:57 +01:00
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { StorageAccount } from '@utils/storage/account'
import {
generateAccountKey,
getGlobalStorage,
2023-01-08 17:07:47 +01:00
setAccount,
setAccountStorage,
setGlobalStorage
} from '@utils/storage/actions'
2021-01-07 19:13:09 +01:00
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
2022-12-10 01:59:26 +01:00
import * as AuthSession from 'expo-auth-session'
2021-01-26 12:17:25 +01:00
import * as WebBrowser from 'expo-web-browser'
2021-01-07 19:13:09 +01:00
import { debounce } from 'lodash'
2022-12-10 01:59:26 +01:00
import React, { RefObject, useCallback, useState } from 'react'
2022-06-10 19:41:51 +02:00
import { Trans, useTranslation } from 'react-i18next'
2022-11-29 23:44:11 +01:00
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
2021-04-09 21:43:12 +02:00
import { ScrollView } from 'react-native-gesture-handler'
2023-01-03 23:57:23 +01:00
import parse from 'url-parse'
2022-12-10 01:59:26 +01:00
import CustomText from '../Text'
2021-01-07 19:13:09 +01:00
export interface Props {
2021-04-09 21:43:12 +02:00
scrollViewRef?: RefObject<ScrollView>
2021-01-07 19:13:09 +01:00
disableHeaderImage?: boolean
goBack?: boolean
}
const ComponentInstance: React.FC<Props> = ({
2021-04-09 21:43:12 +02:00
scrollViewRef,
2021-01-07 19:13:09 +01:00
disableHeaderImage,
goBack = false
}) => {
2022-12-23 15:53:40 +01:00
const { t } = useTranslation(['common', 'componentInstance'])
2022-02-12 14:51:01 +01:00
const { colors, mode } = useTheme()
2022-12-10 01:59:26 +01:00
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
2021-01-20 12:35:24 +01:00
2022-12-10 01:59:26 +01:00
const [domain, setDomain] = useState<string>('')
2022-12-18 00:00:58 +01:00
const [errorCode, setErrorCode] = useState<number | null>(null)
const whitelisted: boolean =
!!domain.length &&
!!errorCode &&
2023-01-03 23:57:23 +01:00
!!(parse(`https://${domain}/`).hostname === domain) &&
2022-12-18 00:00:58 +01:00
errorCode === 401
2021-01-07 19:13:09 +01:00
2021-01-11 21:36:57 +01:00
const instanceQuery = useInstanceQuery({
2021-02-20 19:12:44 +01:00
domain,
2022-12-18 00:00:58 +01:00
options: {
enabled: !!domain,
retry: false,
onError: err => {
if (err.status) {
setErrorCode(err.status)
}
}
}
2021-01-07 19:13:09 +01:00
})
2022-12-10 01:59:26 +01:00
const appsMutation = useAppsMutation({
retry: false,
onSuccess: async (data, variables) => {
const scopes = featureCheck('deprecate_auth_follow')
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push']
2022-12-10 01:59:26 +01:00
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,
2022-12-10 01:59:26 +01:00
redirectUri
})
await request.makeAuthUrlAsync(discovery)
2022-12-30 12:56:13 +01:00
const promptResult = await request.promptAsync(discovery, await browserPackage())
2022-12-10 01:59:26 +01:00
if (promptResult?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId,
clientSecret,
scopes,
2022-12-10 01:59:26 +01:00
redirectUri,
code: promptResult.params.code,
extraParams: {
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
...(request.codeVerifier && { code_verifier: request.codeVerifier })
}
2022-12-10 01:59:26 +01:00
},
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
)
queryClient.clear()
const {
body: { id, acct, avatar_static }
} = await apiGeneral<Mastodon.Account>({
method: 'get',
domain,
url: `api/v1/accounts/verify_credentials`,
headers: { Authorization: `Bearer ${accessToken}` }
})
const accounts = getGlobalStorage.object('accounts')
const accountKey = generateAccountKey({ domain, id })
const account = accounts?.find(account => account === accountKey)
const accountDetails: StorageAccount = {
'auth.clientId': clientId,
'auth.clientSecret': clientSecret,
'auth.token': accessToken,
'auth.domain': domain,
'auth.account.id': id,
'auth.account.acct': acct,
'auth.account.domain':
(instanceQuery.data as Mastodon.Instance_V2)?.domain ||
instanceQuery.data?.account_domain ||
((instanceQuery.data as Mastodon.Instance_V1)?.uri
? parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname
: undefined) ||
(instanceQuery.data as Mastodon.Instance_V1)?.uri,
'auth.account.avatar_static': avatar_static,
version: instanceQuery.data?.version || '0',
preferences: undefined,
notifications: {
follow: true,
follow_request: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
status: true,
update: true,
'admin.sign_up': true,
'admin.report': true
},
push: {
global: false,
decode: false,
alerts: {
follow: true,
follow_request: true,
favourite: true,
reblog: true,
mention: true,
poll: true,
status: true,
update: true,
'admin.sign_up': false,
'admin.report': false
},
2022-12-30 15:14:16 +01:00
key: Math.random().toString(36).slice(2, 12)
},
page_local: {
showBoosts: true,
showReplies: true
},
page_me: {
followedTags: { shown: false },
lists: { shown: false },
announcements: { shown: false, unread: 0 }
},
drafts: [],
emojis_frequent: []
}
setAccountStorage(
Object.keys(accountDetails).map((key: keyof StorageAccount) => ({
key,
value: accountDetails[key]
})),
accountKey
2022-12-10 01:59:26 +01:00
)
if (!account) {
setGlobalStorage('accounts', accounts?.concat([accountKey]))
}
2023-01-08 17:07:47 +01:00
setAccount(accountKey)
2022-12-10 01:59:26 +01:00
goBack && navigation.goBack()
}
}
})
2021-01-07 19:13:09 +01:00
const processUpdate = useCallback(() => {
2021-02-20 19:12:44 +01:00
if (domain) {
const accounts = getGlobalStorage.object('accounts')
2022-12-29 23:13:22 +01:00
if (accounts?.filter(account => account.startsWith(`${domain}/`)).length) {
2022-12-23 15:53:40 +01:00
Alert.alert(
t('componentInstance:update.alert.title'),
t('componentInstance:update.alert.message'),
[
{
text: t('common:buttons.cancel'),
style: 'cancel'
},
{
text: t('common:buttons.continue'),
onPress: () => appsMutation.mutate({ domain })
}
]
)
2021-02-09 01:16:12 +01:00
} else {
2022-12-10 01:59:26 +01:00
appsMutation.mutate({ domain })
2021-01-07 19:13:09 +01:00
}
}
2021-02-20 19:12:44 +01:00
}, [domain])
2021-01-07 19:13:09 +01:00
return (
2021-03-06 21:01:38 +01:00
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
2021-01-07 19:13:09 +01:00
{!disableHeaderImage ? (
<View style={{ flexDirection: 'row' }}>
2021-01-07 19:13:09 +01:00
<Image
2021-03-28 23:31:10 +02:00
source={require('assets/images/welcome.png')}
style={{ resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 }}
2021-01-07 19:13:09 +01:00
/>
</View>
) : null}
<View
style={{
marginTop: StyleConstants.Spacing.L,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<View
style={{
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
>
<TextInput
2021-04-09 21:43:12 +02:00
accessible={false}
accessibilityRole='none'
style={{
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
color: colors.primaryDefault,
2022-12-18 00:00:58 +01:00
borderBottomColor: instanceQuery.isError
? whitelisted
? colors.yellow
: colors.red
: colors.border,
2022-12-11 14:08:27 +01:00
...(Platform.OS === 'android' && { paddingRight: 0 })
}}
editable={false}
2021-05-10 23:45:54 +02:00
defaultValue='https://'
/>
2021-01-07 19:13:09 +01:00
<TextInput
style={{
flex: 1,
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M,
color: colors.primaryDefault,
2022-12-18 00:00:58 +01:00
borderBottomColor: instanceQuery.isError
? whitelisted
? colors.yellow
: colors.red
: colors.border,
2022-12-11 14:08:27 +01:00
...(Platform.OS === 'android' && { paddingLeft: 0 })
}}
2022-12-18 00:00:58 +01:00
onChangeText={debounce(
text => {
setDomain(text.replace(/^http(s)?\:\/\//i, ''))
setErrorCode(null)
},
1000,
{
trailing: true
}
)}
2021-01-07 19:13:09 +01:00
autoCapitalize='none'
clearButtonMode='never'
keyboardType='url'
textContentType='URL'
2022-08-07 01:18:10 +02:00
onSubmitEditing={({ nativeEvent: { text } }) => {
if (
text === domain &&
instanceQuery.isSuccess &&
instanceQuery.data &&
// @ts-ignore
(instanceQuery.data.domain || instanceQuery.data.uri)
2022-08-07 01:18:10 +02:00
) {
processUpdate()
}
}}
2022-12-23 15:53:40 +01:00
placeholder={' ' + t('componentInstance:server.textInput.placeholder')}
2022-02-12 14:51:01 +01:00
placeholderTextColor={colors.secondary}
2021-01-07 19:13:09 +01:00
returnKeyType='go'
keyboardAppearance={mode}
2021-04-09 21:43:12 +02:00
{...(scrollViewRef && {
onFocus: () =>
2022-11-29 23:44:11 +01:00
setTimeout(() => scrollViewRef.current?.scrollTo({ y: 0, animated: true }), 150)
2021-04-09 21:43:12 +02:00
})}
autoCorrect={false}
spellCheck={false}
2021-01-07 19:13:09 +01:00
/>
<Button
type='text'
2022-12-23 15:53:40 +01:00
content={t('componentInstance:server.button')}
2021-01-07 19:13:09 +01:00
onPress={processUpdate}
// @ts-ignore
disabled={!(instanceQuery.data?.domain || instanceQuery.data?.uri) && !whitelisted}
2022-12-10 01:59:26 +01:00
loading={instanceQuery.isFetching || appsMutation.isLoading}
2021-01-07 19:13:09 +01:00
/>
</View>
2021-01-26 12:17:25 +01:00
2021-01-07 19:13:09 +01:00
<View>
2022-12-18 00:00:58 +01:00
{whitelisted ? (
<CustomText
fontStyle='S'
style={{
color: colors.yellow,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingTop: StyleConstants.Spacing.XS
}}
>
2022-12-23 15:53:40 +01:00
{t('componentInstance:server.whitelisted')}
2022-12-18 00:00:58 +01:00
</CustomText>
) : null}
<View
style={{
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
2022-06-10 19:41:51 +02:00
marginTop: StyleConstants.Spacing.M
}}
>
2021-02-09 01:16:12 +01:00
<Icon
name='Lock'
size={StyleConstants.Font.Size.S}
2022-02-12 14:51:01 +01:00
color={colors.secondary}
style={{
2022-11-29 23:44:11 +01:00
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
marginRight: StyleConstants.Spacing.XS
}}
2021-02-09 01:16:12 +01:00
/>
2022-11-29 23:44:11 +01:00
<CustomText fontStyle='S' style={{ flex: 1, color: colors.secondary }}>
2022-12-23 15:53:40 +01:00
{t('componentInstance:server.disclaimer.base')}
2022-06-10 19:41:51 +02:00
</CustomText>
</View>
<View
style={{
flexDirection: 'row',
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.M
}}
>
<Icon
name='CheckSquare'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
style={{
2022-11-29 23:44:11 +01:00
marginTop: (StyleConstants.Font.LineHeight.S - StyleConstants.Font.Size.S) / 2,
2022-06-10 19:41:51 +02:00
marginRight: StyleConstants.Spacing.XS
}}
/>
<CustomText
fontStyle='S'
style={{ flex: 1, color: colors.secondary }}
accessibilityRole='link'
>
<Trans
2022-12-23 15:53:40 +01:00
ns='componentInstance'
i18nKey='server.terms.base'
2022-06-10 19:41:51 +02:00
components={[
<CustomText
accessible
style={{ color: colors.blue }}
2022-12-04 00:35:13 +01:00
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/privacy-policy', {
2022-12-16 22:00:22 +01:00
...(await browserPackage())
2022-12-04 00:35:13 +01:00
})
}
2022-06-10 19:41:51 +02:00
/>,
<CustomText
accessible
style={{ color: colors.blue }}
2022-12-04 00:35:13 +01:00
onPress={async () =>
WebBrowser.openBrowserAsync('https://tooot.app/terms-of-service', {
2022-12-16 22:00:22 +01:00
...(await browserPackage())
2022-12-04 00:35:13 +01:00
})
2022-11-29 23:44:11 +01:00
}
2022-06-10 19:41:51 +02:00
/>
]}
/>
</CustomText>
2021-02-09 01:16:12 +01:00
</View>
2021-01-07 19:13:09 +01:00
</View>
</View>
2021-03-06 21:01:38 +01:00
</KeyboardAvoidingView>
2021-01-07 19:13:09 +01:00
)
}
export default ComponentInstance