diff --git a/App.tsx b/App.tsx index bf57ef38..67550953 100644 --- a/App.tsx +++ b/App.tsx @@ -4,8 +4,8 @@ import { QueryCache, ReactQueryCacheProvider, setConsole } from 'react-query' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' -import { Index } from './src/Index' -import { persistor, store } from './src/store' +import { Index } from '@root/Index' +import { persistor, store } from '@root/store' import ThemeManager from '@utils/styles/ThemeManager' const queryCache = new QueryCache() diff --git a/assets/screens/meRoot/welcome.png b/assets/screens/meRoot/welcome.png new file mode 100644 index 00000000..2e75881d Binary files /dev/null and b/assets/screens/meRoot/welcome.png differ diff --git a/babel.config.js b/babel.config.js index d260ea40..a7820c61 100644 --- a/babel.config.js +++ b/babel.config.js @@ -9,6 +9,7 @@ module.exports = function (api) { { root: ['./'], alias: { + '@assets': './assets', '@root': './src', '@api': './src/api', '@components': './src/components', diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index f81d038e..5fe5c511 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -226,6 +226,32 @@ declare namespace Mastodon { title: string } + type Instance = { + // Base + uri: string + title: string + description: string + short_description: string + email: string + version: string + languages: string[] + registrations: boolean + approval_required: boolean + invites_enabled: boolean + urls: { + streaming_api: string + } + stats: { + user_count: number + status_count: number + domain_count: number + } + + // Others + thumbnail?: string + contact_account?: Account + } + type Mention = { // Base id: string diff --git a/src/Index.tsx b/src/Index.tsx index 313930c1..cf0ff5ea 100644 --- a/src/Index.tsx +++ b/src/Index.tsx @@ -18,6 +18,8 @@ import { useTheme } from '@utils/styles/ThemeManager' import getCurrentTab from '@utils/getCurrentTab' import { toastConfig } from '@components/toast' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { getLocalUrl } from './utils/slices/instancesSlice' enableScreens() const Tab = createBottomTabNavigator() @@ -31,6 +33,7 @@ export type RootStackParamList = { } export const Index: React.FC = () => { + const localInstance = useSelector(getLocalUrl) const { i18n } = useTranslation() const { mode, theme } = useTheme() enum barStyle { @@ -46,12 +49,14 @@ export const Index: React.FC = () => { screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { let name: any + let updateColor: string = color switch (route.name) { case 'Screen-Local': name = 'home' break case 'Screen-Public': name = 'globe' + !focused && (updateColor = theme.secondary) break case 'Screen-Post': name = 'plus' @@ -61,30 +66,42 @@ export const Index: React.FC = () => { break case 'Screen-Me': name = focused ? 'meh' : 'smile' + !focused && (updateColor = theme.secondary) break default: name = 'alert-octagon' break } - return + return } })} tabBarOptions={{ activeTintColor: theme.primary, - inactiveTintColor: theme.secondary, + inactiveTintColor: localInstance ? theme.secondary : theme.disabled, showLabel: false }} > - + { + if (!localInstance) { + e.preventDefault() + } + } + }} + /> ({ tabPress: e => { e.preventDefault() - navigation.navigate(getCurrentTab(navigation), { - screen: 'Screen-Shared-Compose' - }) + localInstance && + navigation.navigate(getCurrentTab(navigation), { + screen: 'Screen-Shared-Compose' + }) } })} > @@ -93,6 +110,13 @@ export const Index: React.FC = () => { { + if (!localInstance) { + e.preventDefault() + } + } + }} /> diff --git a/src/api/client.ts b/src/api/client.ts index 4231e991..5e6385cb 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { store, RootState } from '@root/store' +import { store, RootState } from 'src/store' const client = async ({ method, diff --git a/src/components/Button/ButtonRow.tsx b/src/components/Button/ButtonRow.tsx index 3036feb7..2b08ca29 100644 --- a/src/components/Button/ButtonRow.tsx +++ b/src/components/Button/ButtonRow.tsx @@ -8,18 +8,17 @@ type PropsBase = { onPress: () => void disabled?: boolean buttonSize?: 'S' | 'M' + size?: 'S' | 'M' | 'L' } export interface PropsText extends PropsBase { text: string icon?: any - size?: 'S' | 'M' | 'L' } export interface PropsIcon extends PropsBase { text?: string icon: any - size?: 'S' | 'M' | 'L' } const ButtonRow: React.FC = ({ @@ -71,16 +70,13 @@ const styles = StyleSheet.create({ button: { paddingLeft: StyleConstants.Spacing.M, paddingRight: StyleConstants.Spacing.M, - borderWidth: 1, - borderRadius: 100 + borderWidth: 1.25, + borderRadius: 100, + alignItems: 'center' }, text: { textAlign: 'center' } }) -export default React.memo(ButtonRow, (prev, next) => { - let skipUpdate = true - skipUpdate = prev.disabled === next.disabled - return skipUpdate -}) +export default ButtonRow diff --git a/src/components/PleaseLogin.tsx b/src/components/PleaseLogin.tsx deleted file mode 100644 index a40986cc..00000000 --- a/src/components/PleaseLogin.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import { Text } from 'react-native' - -const PleaseLogin = () => { - return 请先登录 -} - -export default PleaseLogin diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx index 9392171f..583b78c6 100644 --- a/src/components/Timelines.tsx +++ b/src/components/Timelines.tsx @@ -15,7 +15,6 @@ import { import { useTheme } from '@utils/styles/ThemeManager' import { useNavigation } from '@react-navigation/native' import getCurrentTab from '@utils/getCurrentTab' -import PleaseLogin from '@components/PleaseLogin' const Stack = createNativeStackNavigator() @@ -30,9 +29,7 @@ const Page = ({ {localRegistered || page === 'RemotePublic' ? ( - ) : ( - - )} + ) : null} ) } diff --git a/src/screens/Me.tsx b/src/screens/Me.tsx index ce04e281..031f58c8 100644 --- a/src/screens/Me.tsx +++ b/src/screens/Me.tsx @@ -28,13 +28,13 @@ const ScreenMe: React.FC = () => { name='Screen-Me-Root' component={ScreenMeRoot} options={ - localRegistered - ? { - headerTranslucent: true, - headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, - headerCenter: () => <> - } - : { headerTitle: t('headers.me.root') } + // localRegistered ? + { + headerTranslucent: true, + headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, + headerCenter: () => <> + } + // : { headerTitle: t('meRoot:heading') } } /> { const localRegistered = useSelector(getLocalUrl) return ( - + {localRegistered ? : } {localRegistered && } diff --git a/src/screens/Me/Root/Login.tsx b/src/screens/Me/Root/Login.tsx index f53f74eb..bec7a7fd 100644 --- a/src/screens/Me/Root/Login.tsx +++ b/src/screens/Me/Root/Login.tsx @@ -1,5 +1,19 @@ -import React, { useCallback, useEffect, useState } from 'react' -import { StyleSheet, Text, TextInput, View } from 'react-native' +import React, { + createRef, + useCallback, + useEffect, + useRef, + useState +} from 'react' +import { + Animated, + Dimensions, + Image, + StyleSheet, + Text, + TextInput, + View +} from 'react-native' import { useQuery } from 'react-query' import { debounce } from 'lodash' @@ -14,6 +28,9 @@ import { useTheme } from '@utils/styles/ThemeManager' import { useTranslation } from 'react-i18next' import { StyleConstants } from '@utils/styles/constants' import { ButtonRow } from '@components/Button' +import ParseContent from '@root/components/ParseContent' +import ShimmerPlaceholder from 'react-native-shimmer-placeholder' +import { Feather } from '@expo/vector-icons' const Login: React.FC = () => { const { t } = useTranslation('meRoot') @@ -26,7 +43,7 @@ const Login: React.FC = () => { clientSecret: string }>() - const { isSuccess, refetch, data } = useQuery( + const { isSuccess, isFetching, refetch, data } = useQuery( ['Instance', { instance }], instanceFetch, { @@ -40,11 +57,13 @@ const Login: React.FC = () => { text => { setInstance(text) setApplicationData(undefined) - refetch() + if (text) { + refetch() + } }, 1000, { - leading: true + trailing: true } ), [] @@ -121,47 +140,217 @@ const Login: React.FC = () => { })() }, [response]) - return ( - - - isSuccess && data && data.uri && (await createApplication()) - } - placeholder={t('content.login.server.placeholder')} - placeholderTextColor={theme.secondary} - returnKeyType='go' - /> - await createApplication()} - text={t('content.login.button')} - disabled={!data?.uri} - /> - {isSuccess && data && data.uri && ( - - {data.title} + const infoRef = createRef() + + const instanceInfo = useCallback( + ({ + header, + content, + parse + }: { + header: string + content: string + parse?: boolean + }) => { + if (isFetching) { + Animated.loop(infoRef.current?.getAnimated()!).start() + } + + return ( + + + {header} + + + + {parse ? ( + + ) : ( + content + )} + + - )} - + ) + }, + [data?.uri, isFetching] + ) + + return ( + <> + + + + + + + isSuccess && data && data.uri && (await createApplication()) + } + placeholder={t('content.login.server.placeholder')} + placeholderTextColor={theme.secondary} + returnKeyType='go' + /> + await createApplication()} + {...(isFetching + ? { icon: 'loader' } + : { text: t('content.login.button') as string })} + disabled={!data?.uri} + /> + + + {instanceInfo({ header: '实例名称', content: data?.title })} + {instanceInfo({ + header: '实例介绍', + content: data?.short_description, + parse: true + })} + + + + 用户总数 + + + + {data?.stats?.user_count} + + + + + + 嘟嘟总数 + + + + {data?.stats?.status_count} + + + + + + 连结总数 + + + + {data?.stats?.domain_count} + + + + + + {' '} + 本站不留存任何信息 + + + + ) } const styles = StyleSheet.create({ base: { padding: StyleConstants.Spacing.Global.PagePadding + }, + inputRow: { + flex: 1, + flexDirection: 'row' + }, + textInput: { + flex: 1, + borderBottomWidth: 1.25, + paddingTop: StyleConstants.Spacing.S - 1.5, + paddingBottom: StyleConstants.Spacing.S - 1.5, + paddingLeft: StyleConstants.Spacing.Global.PagePadding, + paddingRight: StyleConstants.Spacing.Global.PagePadding, + fontSize: StyleConstants.Font.Size.M, + marginRight: StyleConstants.Spacing.M + }, + instanceInfo: { + marginTop: StyleConstants.Spacing.M, + paddingLeft: StyleConstants.Spacing.Global.PagePadding, + paddingRight: StyleConstants.Spacing.Global.PagePadding + }, + instanceInfoHeader: { + fontSize: StyleConstants.Font.Size.S, + fontWeight: StyleConstants.Font.Weight.Bold, + marginBottom: StyleConstants.Spacing.XS + }, + instanceInfoContent: { fontSize: StyleConstants.Font.Size.M }, + instanceStats: { + flex: 1, + flexDirection: 'row', + marginTop: StyleConstants.Spacing.M, + paddingLeft: StyleConstants.Spacing.Global.PagePadding, + paddingRight: StyleConstants.Spacing.Global.PagePadding, + marginBottom: StyleConstants.Spacing.M + }, + instanceStat: { + flex: 1 + }, + disclaimer: { + fontSize: StyleConstants.Font.Size.S, + paddingLeft: StyleConstants.Spacing.Global.PagePadding, + paddingRight: StyleConstants.Spacing.Global.PagePadding, + marginBottom: StyleConstants.Spacing.M } }) diff --git a/src/screens/Me/Root/Logout.tsx b/src/screens/Me/Root/Logout.tsx index 840e7785..f81be008 100644 --- a/src/screens/Me/Root/Logout.tsx +++ b/src/screens/Me/Root/Logout.tsx @@ -6,11 +6,13 @@ import MenuButton from '@components/Menu/Button' import { MenuContainer } from '@components/Menu' import { useNavigation } from '@react-navigation/native' import { useTranslation } from 'react-i18next' +import { useQueryCache } from 'react-query' const Logout: React.FC = () => { const { t } = useTranslation('meRoot') const dispatch = useDispatch() const navigation = useNavigation() + const queryCache = useQueryCache() const alertOption = { title: t('content.logout.alert.title'), @@ -20,6 +22,7 @@ const Logout: React.FC = () => { text: t('content.logout.alert.buttons.logout'), style: 'destructive' as const, onPress: () => { + queryCache.clear() dispatch(updateLocal({})) navigation.navigate('Screen-Public', { screen: 'Screen-Public-Root', diff --git a/src/screens/Notifications.tsx b/src/screens/Notifications.tsx index d87d4202..06b86bee 100644 --- a/src/screens/Notifications.tsx +++ b/src/screens/Notifications.tsx @@ -5,7 +5,6 @@ import Timeline from '@components/Timelines/Timeline' import sharedScreens from '@screens/Shared/sharedScreens' import { useSelector } from 'react-redux' import { RootState } from '@root/store' -import PleaseLogin from '@components/PleaseLogin' import { useTranslation } from 'react-i18next' const Stack = createNativeStackNavigator() @@ -21,9 +20,7 @@ const ScreenNotifications: React.FC = () => { screenOptions={{ headerTitle: t('notifications:heading') }} > - {() => - localRegistered ? : - } + {() => (localRegistered ? : null)} {sharedScreens(Stack)} diff --git a/src/screens/Shared/Compose/Root.tsx b/src/screens/Shared/Compose/Root.tsx index ab44ee19..e8a03282 100644 --- a/src/screens/Shared/Compose/Root.tsx +++ b/src/screens/Shared/Compose/Root.tsx @@ -25,7 +25,11 @@ import { emojisFetch } from '@utils/fetches/emojisFetch' import { searchFetch } from '@utils/fetches/searchFetch' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import { PostAction, ComposeState, ComposeContext } from '@screens/Shared/Compose' +import { + PostAction, + ComposeState, + ComposeContext +} from '@screens/Shared/Compose' import ComposeActions from '@screens/Shared/Compose/Actions' import updateText from './updateText' import * as Permissions from 'expo-permissions' @@ -189,7 +193,7 @@ const ComposeRoot: React.FC = () => { /> } + ListHeaderComponent={} ListFooterComponent={} ListEmptyComponent={listEmpty} data={data} diff --git a/tsconfig.json b/tsconfig.json index 8aefea1e..fa5c48b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "strict": true, "baseUrl": "./", "paths": { + // "@assets/*": ["./assets/*"], "@root/*": ["./src/*"], "@api/*": ["./src/api/*"], "@components/*": ["./src/components/*"],