1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Lots of updates

This commit is contained in:
Zhiyuan Zheng
2021-01-10 02:12:14 +01:00
parent 4a6229514f
commit 541e2a5601
28 changed files with 1001 additions and 530 deletions

View File

@ -58,7 +58,7 @@ const Button: React.FC<Props> = ({
} else {
mounted.current = true
}
}, [content, loading, disabled])
}, [content, loading, disabled, active])
const loadingSpinkit = useMemo(
() => (

View File

@ -5,15 +5,19 @@ import { useNavigation } from '@react-navigation/native'
import hookApps from '@utils/queryHooks/apps'
import hookInstance from '@utils/queryHooks/instance'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { InstanceLocal, remoteUpdate } from '@utils/slices/instancesSlice'
import {
getLocalInstances,
InstanceLocal,
remoteUpdate
} from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { debounce } from 'lodash'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet, Text, TextInput, View } from 'react-native'
import { Alert, Image, StyleSheet, Text, TextInput, View } from 'react-native'
import { useQueryClient } from 'react-query'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import InstanceAuth from './Instance/Auth'
import InstanceInfo from './Instance/Info'
import { toast } from './toast'
@ -36,6 +40,7 @@ const ComponentInstance: React.FC<Props> = ({
const { theme } = useTheme()
const [instanceDomain, setInstanceDomain] = useState<string | undefined>()
const [appData, setApplicationData] = useState<InstanceLocal['appData']>()
const localInstances = useSelector(getLocalInstances)
const instanceQuery = hookInstance({
instanceDomain,
@ -79,12 +84,32 @@ const ComponentInstance: React.FC<Props> = ({
const processUpdate = useCallback(() => {
if (instanceDomain) {
haptics('Success')
switch (type) {
case 'local':
applicationQuery.refetch()
return
if (
localInstances &&
localInstances.filter(instance => instance.url === instanceDomain)
.length
) {
Alert.alert(
'域名已存在',
'可以登录同个域名的另外一个账户,现有账户🈚️用',
[
{ text: '取消', style: 'cancel' },
{
text: '继续',
onPress: () => {
applicationQuery.refetch()
}
}
]
)
} else {
applicationQuery.refetch()
}
break
case 'remote':
haptics('Success')
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'RemotePublic' }
@ -92,8 +117,8 @@ const ComponentInstance: React.FC<Props> = ({
dispatch(remoteUpdate(instanceDomain))
queryClient.resetQueries(queryKey)
toast({ type: 'success', message: '重置成功' })
navigation.navigate('Screen-Remote-Root')
return
navigation.navigate('Screen-Public', { screen: 'Screen-Public-Root' })
break
}
}
}, [instanceDomain])
@ -160,7 +185,7 @@ const ComponentInstance: React.FC<Props> = ({
content={buttonContent}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || applicationQuery.isFetching}
loading={instanceQuery.isLoading || applicationQuery.isLoading}
/>
</View>
<View>

View File

@ -1,12 +1,12 @@
import Icon from '@components/Icon'
import openLink from '@components/openLink'
import ParseEmojis from '@components/Parse/Emojis'
import { useNavigation } from '@react-navigation/native'
import { useNavigation, useRoute } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React, { useCallback, useState } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import { Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import Animated, {
useAnimatedStyle,
@ -16,6 +16,7 @@ import Animated, {
// Prevent going to the same hashtag multiple times
const renderNode = ({
routeParams,
theme,
node,
index,
@ -26,6 +27,7 @@ const renderNode = ({
showFullLink,
disableDetails
}: {
routeParams?: any
theme: any
node: any
index: number
@ -42,6 +44,10 @@ const renderNode = ({
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
const differentTag = routeParams?.hashtag
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
: true
return (
<Text
key={index}
@ -50,8 +56,8 @@ const renderNode = ({
...StyleConstants.FontStyle[size]
}}
onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
!disableDetails &&
differentTag &&
navigation.push('Screen-Shared-Hashtag', {
hashtag: tag[1] || tag[2]
})
@ -65,6 +71,9 @@ const renderNode = ({
const accountIndex = mentions.findIndex(
mention => mention.url === href
)
const differentAccount = routeParams?.account
? routeParams.account.id !== mentions[accountIndex].id
: true
return (
<Text
key={index}
@ -75,6 +84,7 @@ const renderNode = ({
onPress={() => {
accountIndex !== -1 &&
!disableDetails &&
differentAccount &&
navigation.push('Screen-Shared-Account', {
account: mentions[accountIndex]
})
@ -151,11 +161,13 @@ const ParseHTML: React.FC<Props> = ({
disableDetails = false
}) => {
const navigation = useNavigation()
const route = useRoute()
const { theme } = useTheme()
const renderNodeCallback = useCallback(
(node, index) =>
renderNode({
routeParams: route.params,
theme,
node,
index,

View File

@ -2,6 +2,7 @@ import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { toast } from '@components/toast'
import { QueryKeyRelationship } from '@utils/queryHooks/relationship'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -15,7 +16,7 @@ export interface Props {
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }]
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const queryClient = useQueryClient()
const fireMutation = useCallback(
@ -31,7 +32,7 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const mutation = useMutation(fireMutation, {
onSuccess: res => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, res)
queryClient.setQueryData(queryKeyRelationship, res)
queryClient.refetchQueries(['Notifications'])
},
onError: (err: any, { type }) => {

View File

@ -2,7 +2,9 @@ import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { toast } from '@components/toast'
import hookRelationship from '@utils/queryHooks/relationship'
import hookRelationship, {
QueryKeyRelationship
} from '@utils/queryHooks/relationship'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query'
@ -14,7 +16,7 @@ export interface Props {
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }]
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const query = hookRelationship({ id })
const queryClient = useQueryClient()
@ -31,7 +33,7 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const mutation = useMutation(fireMutation, {
onSuccess: res => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, res)
queryClient.setQueryData(queryKeyRelationship, res)
},
onError: (err: any, { type }) => {
haptics('Error')

View File

@ -16,11 +16,12 @@ const Stack = createNativeStackNavigator<
>()
export interface Props {
name: 'Screen-Local-Root' | 'Screen-Public-Root'
content: { title: string; page: App.Pages }[]
name: 'Local' | 'Public'
content: { title: string; page: App.Pages; remote?: boolean }[]
}
const Timelines: React.FC<Props> = ({ name, content }) => {
const remoteUrl = useSelector(getRemoteUrl)
const navigation = useNavigation()
const { mode } = useTheme()
const localActiveIndex = useSelector(getLocalActiveIndex)
@ -71,16 +72,19 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
<Stack.Screen
// @ts-ignore
name={name}
name={`Screen-${name}-Root`}
component={screenComponent}
options={{
headerTitle: name === 'Screen-Public-Root' ? publicDomain : '',
headerTitle: name === 'Public' ? publicDomain : '',
...(localActiveIndex !== null && {
headerCenter: () => (
<View style={styles.segmentsContainer}>
<SegmentedControl
appearance={mode}
values={[content[0].title, content[1].title]}
values={[
content[0].title,
content[1].remote ? remoteUrl : content[1].title
]}
selectedIndex={segment}
onChange={({ nativeEvent }) =>
setSegment(nativeEvent.selectedSegmentIndex)
@ -102,7 +106,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
const styles = StyleSheet.create({
segmentsContainer: {
flexBasis: '60%'
flexBasis: '65%'
}
})

View File

@ -16,11 +16,9 @@ const TimelineHeader = React.memo(
{' '}
<Text
style={{ color: theme.blue }}
onPress={() =>
navigation.navigate('Screen-Me', {
screen: 'Screen-Me-Settings-UpdateRemote'
})
}
onPress={() => {
navigation.navigate('Screen-Me')
}}
>
{' '}
<Icon

View File

@ -13,22 +13,14 @@ export interface Props {
const TimelineCard: React.FC<Props> = ({ card }) => {
const { theme } = useTheme()
let isMounted = false
useEffect(() => {
isMounted = true
return () => {
isMounted = false
}
})
const [imageLoaded, setImageLoaded] = useState(false)
useEffect(() => {
const preFetch = () =>
card.image &&
isMounted &&
Image.getSize(card.image, () => isMounted && setImageLoaded(true))
preFetch()
}, [isMounted])
const preFetch = () => Image.getSize(card.image, () => setImageLoaded(true))
if (card.image) {
preFetch()
}
}, [])
const cardVisual = useMemo(() => {
if (imageLoaded) {
return <Image source={{ uri: card.image }} style={styles.image} />
@ -45,12 +37,18 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
<Pressable
style={[styles.card, { borderColor: theme.border }]}
onPress={async () => await openLink(card.url)}
testID='base'
>
{card.image && <View style={styles.left}>{cardVisual}</View>}
{card.image && (
<View style={styles.left} testID='image'>
{cardVisual}
</View>
)}
<View style={styles.right}>
<Text
numberOfLines={2}
style={[styles.rightTitle, { color: theme.primary }]}
testID='title'
>
{card.title}
</Text>
@ -58,6 +56,7 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
<Text
numberOfLines={1}
style={[styles.rightDescription, { color: theme.primary }]}
testID='description'
>
{card.description}
</Text>

View File

@ -7,7 +7,7 @@ const ScreenLocal: React.FC = () => {
return (
<Timelines
name='Screen-Local-Root'
name='Local'
content={[
{ title: t('local:heading.segments.left'), page: 'Following' },
{ title: t('local:heading.segments.right'), page: 'Local' }

View File

@ -6,8 +6,8 @@ import ScreenMeLists from '@screens/Me/Lists'
import ScreenMeRoot from '@screens/Me/Root'
import ScreenMeListsList from '@screens/Me/Root/Lists/List'
import ScreenMeSettings from '@screens/Me/Settings'
import UpdateRemote from '@screens/Me/Settings/UpdateRemote'
import ScreenMeSwitch from '@screens/Me/Switch'
import UpdateRemote from '@screens/Me/UpdateRemote'
import sharedScreens from '@screens/Shared/sharedScreens'
import React from 'react'
import { useTranslation } from 'react-i18next'

View File

@ -20,13 +20,13 @@ import { useDispatch, useSelector } from 'react-redux'
interface Props {
index: NonNullable<InstancesState['local']['activeIndex']>
instance: InstanceLocal
active?: boolean
disabled?: boolean
}
const AccountButton: React.FC<Props> = ({
index,
instance,
active = false
disabled = false
}) => {
const queryClient = useQueryClient()
const navigation = useNavigation()
@ -40,7 +40,7 @@ const AccountButton: React.FC<Props> = ({
return (
<Button
type='text'
active={active}
disabled={disabled}
loading={isLoading}
style={styles.button}
content={`@${data?.acct || '...'}@${instance.url}`}
@ -78,7 +78,7 @@ const ScreenMeSwitchRoot = () => {
key={index}
index={index}
instance={instance}
active={localActiveIndex === index}
disabled={localActiveIndex === index}
/>
))
: null}

View File

@ -1,4 +1,5 @@
import Timelines from '@components/Timelines'
import { getRemoteUrl } from '@utils/slices/instancesSlice'
import React from 'react'
import { useTranslation } from 'react-i18next'
@ -7,10 +8,14 @@ const ScreenPublic: React.FC = () => {
return (
<Timelines
name='Screen-Public-Root'
name='Public'
content={[
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
{ title: t('public:heading.segments.right'), page: 'RemotePublic' }
{
title: t('public:heading.segments.right'),
page: 'RemotePublic',
remote: true
}
]}
/>
)

View File

@ -17,6 +17,7 @@ import {
Text,
View
} from 'react-native'
import { Chase } from 'react-native-animated-spinkit'
import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useMutation } from 'react-query'
@ -199,6 +200,20 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
[]
)
const ListEmptyComponent = useCallback(() => {
return (
<View
style={{
width: Dimensions.get('screen').width,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Chase size={StyleConstants.Font.Size.L} color={theme.secondary} />
</View>
)
}, [])
return (
<SafeAreaView style={[styles.base, { backgroundColor: theme.background }]}>
<View style={[styles.header, { height: bottomTabBarHeight }]}>
@ -211,6 +226,7 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={onMomentumScrollEnd}
ListEmptyComponent={ListEmptyComponent}
/>
<View style={[styles.indicators, { height: bottomTabBarHeight }]}>
{data && data.length > 1 ? (

View File

@ -17,7 +17,7 @@ import {
NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/types'
type BaseScreens =
export type BaseScreens =
| Nav.LocalStackParamList
| Nav.RemoteStackParamList
| Nav.NotificationsStackParamList

View File

@ -2,12 +2,15 @@ import client from '@api/client'
import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Relationship', { id: Mastodon.Account['id'] }]
export type QueryKeyRelationship = [
'Relationship',
{ id: Mastodon.Account['id'] }
]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
const queryFunction = ({ queryKey }: { queryKey: QueryKeyRelationship }) => {
const { id } = queryKey[1]
return client<Mastodon.Relationship>({
return client<Mastodon.Relationship[]>({
method: 'get',
instance: 'local',
url: `accounts/relationships`,
@ -17,14 +20,21 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
})
}
const hookRelationship = <TData = Mastodon.Relationship>({
const hookRelationship = ({
options,
...queryKeyParams
}: QueryKey[1] & {
options?: UseQueryOptions<Mastodon.Relationship, AxiosError, TData>
}: QueryKeyRelationship[1] & {
options?: UseQueryOptions<
Mastodon.Relationship[],
AxiosError,
Mastodon.Relationship
>
}) => {
const queryKey: QueryKey = ['Relationship', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
const queryKey: QueryKeyRelationship = ['Relationship', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, {
...options,
select: data => data[0]
})
}
export default hookRelationship

View File

@ -55,9 +55,10 @@ export const localAddInstance = createAsyncThunk(
url: InstanceLocal['url']
token: InstanceLocal['token']
appData: InstanceLocal['appData']
}): Promise<InstanceLocal> => {
const store = require('@root/store')
const state = store.getState().instances
}): Promise<{ type: 'add' | 'overwrite'; data: InstanceLocal }> => {
const { store } = require('@root/store')
const instanceLocal: InstancesState['local'] = store.getState().instances
.local
const { id } = await client<Mastodon.Account>({
method: 'get',
@ -67,14 +68,24 @@ export const localAddInstance = createAsyncThunk(
headers: { Authorization: `Bearer ${token}` }
})
// Overwrite existing account?
// if (
// state.local.instances.filter(
// instance => instance && instance.account && instance.account.id === id
// ).length
// ) {
// return Promise.reject()
// }
let type: 'add' | 'overwrite'
if (
instanceLocal.instances.filter(instance => {
if (instance) {
if (instance.url === url && instance.account.id === id) {
return true
} else {
return false
}
} else {
return false
}
}).length
) {
type = 'overwrite'
} else {
type = 'add'
}
const preferences = await client<Mastodon.Preferences>({
method: 'get',
@ -85,15 +96,18 @@ export const localAddInstance = createAsyncThunk(
})
return Promise.resolve({
appData,
url,
token,
account: {
id,
preferences
},
notification: {
unread: false
type,
data: {
appData,
url,
token,
account: {
id,
preferences
},
notification: {
unread: false
}
}
})
}
@ -101,31 +115,37 @@ export const localAddInstance = createAsyncThunk(
export const localRemoveInstance = createAsyncThunk(
'instances/localRemoveInstance',
async (index?: InstancesState['local']['activeIndex']): Promise<number> => {
const store = require('@root/store')
const local = store.getState().instances.local
const { store } = require('@root/store')
const instanceLocal: InstancesState['local'] = store.getState().instances
.local
if (index) {
return Promise.resolve(index)
} else {
if (local.activeIndex !== null) {
const currentInstance = local.instances[local.activeIndex]
if (instanceLocal.activeIndex !== null) {
const currentInstance =
instanceLocal.instances[instanceLocal.activeIndex]
let revoked = undefined
try {
revoked = await AuthSession.revokeAsync(
{
clientId: currentInstance.appData.clientId,
clientSecret: currentInstance.appData.clientSecret,
token: currentInstance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${currentInstance.url}/oauth/revoke`
}
)
} catch {}
const revoked = await AuthSession.revokeAsync(
{
clientId: currentInstance.appData.clientId,
clientSecret: currentInstance.appData.clientSecret,
token: currentInstance.token,
scopes: ['read', 'write', 'follow', 'push']
},
{
revocationEndpoint: `https://${currentInstance.url}/oauth/revoke`
}
)
if (!revoked) {
console.warn('Revoking error')
}
return Promise.resolve(local.activeIndex)
return Promise.resolve(instanceLocal.activeIndex)
} else {
throw new Error('Active index invalid, cannot remove instance')
}
@ -176,8 +196,23 @@ const instancesSlice = createSlice({
extraReducers: builder => {
builder
.addCase(localAddInstance.fulfilled, (state, action) => {
state.local.instances.push(action.payload)
state.local.activeIndex = state.local.instances.length - 1
switch (action.payload.type) {
case 'add':
state.local.instances.push(action.payload.data)
state.local.activeIndex = state.local.instances.length - 1
break
case 'overwrite':
state.local.instances = state.local.instances.map(instance => {
if (
instance.url === action.payload.data.url &&
instance.account.id === action.payload.data.account.id
) {
return action.payload.data
} else {
return instance
}
})
}
analytics('login')
})