diff --git a/App.tsx b/App.tsx
index b4d19691..181695fc 100644
--- a/App.tsx
+++ b/App.tsx
@@ -43,9 +43,12 @@ const App: React.FC = () => {
}, [])
const onBeforeLift = useCallback(async () => {
- const netInfoRes = await netInfo()
+ let netInfoRes = undefined
+ try {
+ netInfoRes = await netInfo()
+ } catch {}
- if (netInfoRes.corrupted && netInfoRes.corrupted.length) {
+ if (netInfoRes && netInfoRes.corrupted && netInfoRes.corrupted.length) {
setLocalCorrupt(netInfoRes.corrupted)
}
diff --git a/__tests__/components/Button.js b/__tests__/components/Button.js
index 063ebf06..9a2e0b8c 100644
--- a/__tests__/components/Button.js
+++ b/__tests__/components/Button.js
@@ -30,7 +30,7 @@ describe('Testing component button', () => {
it('with icon only', () => {
const onPress = jest.fn()
const { getByTestId, toJSON } = render(
-
+
)
fireEvent.press(getByTestId('base'))
diff --git a/__tests__/components/Menu/__snapshots__/Header.js.snap b/__tests__/components/Menu/__snapshots__/Header.js.snap
index 3e3468ab..077a9a17 100644
--- a/__tests__/components/Menu/__snapshots__/Header.js.snap
+++ b/__tests__/components/Menu/__snapshots__/Header.js.snap
@@ -16,6 +16,7 @@ exports[`Testing component menu header with text only 1`] = `
Object {
"fontSize": 14,
"fontWeight": "600",
+ "lineHeight": 20,
},
Object {
"color": "rgb(135, 135, 135)",
diff --git a/__tests__/components/Menu/__snapshots__/Row.js.snap b/__tests__/components/Menu/__snapshots__/Row.js.snap
index c90e5b66..52661a5d 100644
--- a/__tests__/components/Menu/__snapshots__/Row.js.snap
+++ b/__tests__/components/Menu/__snapshots__/Row.js.snap
@@ -15,7 +15,7 @@ exports[`Testing component menu row loading state 1`] = `
onStartShouldSetResponder={[Function]}
style={
Object {
- "height": 50,
+ "minHeight": 50,
}
}
testID="base"
@@ -34,28 +34,35 @@ exports[`Testing component menu row loading state 1`] = `
style={
Object {
"alignItems": "center",
- "flex": 1,
- "flexBasis": "70%",
+ "flex": 2,
"flexDirection": "row",
}
}
>
-
- test
-
+
+ test
+
+
@@ -76,7 +83,7 @@ exports[`Testing component menu row on press event 1`] = `
onStartShouldSetResponder={[Function]}
style={
Object {
- "height": 50,
+ "minHeight": 50,
}
}
testID="base"
@@ -95,28 +102,35 @@ exports[`Testing component menu row on press event 1`] = `
style={
Object {
"alignItems": "center",
- "flex": 1,
- "flexBasis": "70%",
+ "flex": 2,
"flexDirection": "row",
}
}
>
-
- test
-
+
+ test
+
+
@@ -137,7 +151,7 @@ exports[`Testing component menu row with title and content 1`] = `
onStartShouldSetResponder={[Function]}
style={
Object {
- "height": 50,
+ "minHeight": 50,
}
}
testID="base"
@@ -156,37 +170,44 @@ exports[`Testing component menu row with title and content 1`] = `
style={
Object {
"alignItems": "center",
- "flex": 1,
- "flexBasis": "70%",
+ "flex": 2,
"flexDirection": "row",
}
}
>
-
- test title
-
+
+ test title
+
+
@@ -196,6 +217,7 @@ exports[`Testing component menu row with title and content 1`] = `
Array [
Object {
"fontSize": 16,
+ "lineHeight": 22,
},
Object {
"color": "rgb(135, 135, 135)",
@@ -226,7 +248,7 @@ exports[`Testing component menu row with title only 1`] = `
onStartShouldSetResponder={[Function]}
style={
Object {
- "height": 50,
+ "minHeight": 50,
}
}
testID="base"
@@ -245,28 +267,35 @@ exports[`Testing component menu row with title only 1`] = `
style={
Object {
"alignItems": "center",
- "flex": 1,
- "flexBasis": "70%",
+ "flex": 2,
"flexDirection": "row",
}
}
>
-
- test title
-
+
+ test title
+
+
diff --git a/__tests__/components/Timelines/Timeline/Shared/Card.js b/__tests__/components/Timelines/Timeline/Shared/Card.js
new file mode 100644
index 00000000..e43eecd9
--- /dev/null
+++ b/__tests__/components/Timelines/Timeline/Shared/Card.js
@@ -0,0 +1,59 @@
+import React from 'react'
+import {
+ toBeDisabled,
+ toContainElement,
+ toHaveStyle,
+ toHaveTextContent
+} from '@testing-library/jest-native'
+import { cleanup, render } from '@testing-library/react-native/pure'
+
+import Card from '@components/Timelines/Timeline/Shared/Card'
+
+expect.extend({
+ toBeDisabled,
+ toContainElement,
+ toHaveStyle,
+ toHaveTextContent
+})
+
+describe('Testing component timeline card', () => {
+ afterEach(cleanup)
+
+ it('with text only', () => {
+ const { getByTestId, queryByTestId, toJSON } = render(
+
+ )
+
+ expect(queryByTestId('image')).toBeNull()
+ expect(getByTestId('base')).toContainElement(getByTestId('title'))
+ expect(queryByTestId('description')).toBeNull()
+
+ expect(getByTestId('title')).toHaveTextContent('Title')
+ expect(toJSON()).toMatchSnapshot()
+ })
+
+ it('with text and description', () => {
+ const { getByTestId, queryByTestId, toJSON } = render(
+
+ )
+
+ expect(queryByTestId('image')).toBeNull()
+ expect(getByTestId('base')).toContainElement(getByTestId('title'))
+ expect(getByTestId('base')).toContainElement(getByTestId('description'))
+
+ expect(getByTestId('title')).toHaveTextContent('Title')
+ expect(getByTestId('description')).toHaveTextContent('Description')
+ expect(toJSON()).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/components/Timelines/Timeline/Shared/__snapshots__/Card.js.snap b/__tests__/components/Timelines/Timeline/Shared/__snapshots__/Card.js.snap
new file mode 100644
index 00000000..c9fd5e61
--- /dev/null
+++ b/__tests__/components/Timelines/Timeline/Shared/__snapshots__/Card.js.snap
@@ -0,0 +1,155 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Testing component timeline card with text and description 1`] = `
+
+
+
+ Title
+
+
+ Description
+
+
+ http://example.com
+
+
+
+`;
+
+exports[`Testing component timeline card with text only 1`] = `
+
+
+
+ Title
+
+
+ http://example.com
+
+
+
+`;
diff --git a/__tests__/components/__snapshots__/Button.js.snap b/__tests__/components/__snapshots__/Button.js.snap
index f8537837..aa9be260 100644
--- a/__tests__/components/__snapshots__/Button.js.snap
+++ b/__tests__/components/__snapshots__/Button.js.snap
@@ -1,393 +1,474 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component button static button apply custom styling 1`] = `
-
-
+
- test
-
+
+ test
+
+
`;
exports[`Testing component button static button disabled state 1`] = `
-
-
+
- test
-
+
+ test
+
+
`;
exports[`Testing component button static button loading state 1`] = `
-
-
- test
-
+
+
+ test
+
-
-
-
-
-
+ >
+
+
+
+
+
+
+
`;
exports[`Testing component button static button with icon only 1`] = `
-
-
+
+
+
+
+
+
+
+
+
+
`;
exports[`Testing component button static button with text only 1`] = `
-
-
+
- Test Button
-
+
+ Test Button
+
+
`;
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 00000000..281d0829
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,31 @@
+module.exports = {
+ preset: 'jest-expo',
+ collectCoverage: true,
+ collectCoverageFrom: [
+ 'src /**/*.{ts,tsx}',
+ '!**/coverage /**',
+ '!**/node_modules /**',
+ '!**/app.config.ts',
+ '!**/babel.config.js',
+ '!**/jest.setup.ts'
+ ],
+ setupFiles: [
+ '/jest/async-storage.js',
+ '/jest/react-native.js',
+ '/jest/react-navigation.js'
+ ],
+ transformIgnorePatterns: [
+ 'node_modules/(?!(jest-)?react-native' +
+ '|react-clone-referenced-element' +
+ '|@react-native-community' +
+ '|expo(nent)?' +
+ '|@expo(nent)?/.*' +
+ '|react-navigation' +
+ '|@react-navigation/.*|@unimodules/.*|unimodules' +
+ '|sentry-expo' +
+ '|native-base' +
+ '|@sentry/.*' +
+ '|redux-persist-expo-securestore' +
+ ')'
+ ]
+}
diff --git a/jest/async-storage.js b/jest/async-storage.js
new file mode 100644
index 00000000..2b54cedb
--- /dev/null
+++ b/jest/async-storage.js
@@ -0,0 +1,3 @@
+import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'
+
+jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage)
diff --git a/package.json b/package.json
index 37ff4a21..90f86a6e 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
- "test": "jest"
+ "test": "jest --watchAll"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.2",
@@ -95,27 +95,9 @@
"chalk": "^4.1.0",
"jest": "^26.6.3",
"jest-expo": "^40.0.1",
+ "nock": "^13.0.5",
"react-test-renderer": "^16.13.1",
"typescript": "~4.1.3"
},
- "jest": {
- "preset": "jest-expo",
- "collectCoverage": true,
- "collectCoverageFrom": [
- "src /**/*.{ts,tsx}",
- "!**/coverage /**",
- "!**/node_modules /**",
- "!**/app.config.ts",
- "!**/babel.config.js",
- "!**/jest.setup.ts"
- ],
- "setupFiles": [
- "/jest/react-native.js",
- "/jest/react-navigation.js"
- ],
- "transformIgnorePatterns": [
- "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
- ]
- },
"private": true
-}
+}
\ No newline at end of file
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 9340b2ec..200cd00a 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -58,7 +58,7 @@ const Button: React.FC = ({
} else {
mounted.current = true
}
- }, [content, loading, disabled])
+ }, [content, loading, disabled, active])
const loadingSpinkit = useMemo(
() => (
diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx
index 9e45ec4a..bac20289 100644
--- a/src/components/Instance.tsx
+++ b/src/components/Instance.tsx
@@ -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 = ({
const { theme } = useTheme()
const [instanceDomain, setInstanceDomain] = useState()
const [appData, setApplicationData] = useState()
+ const localInstances = useSelector(getLocalInstances)
const instanceQuery = hookInstance({
instanceDomain,
@@ -79,12 +84,32 @@ const ComponentInstance: React.FC = ({
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 = ({
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 = ({
content={buttonContent}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
- loading={instanceQuery.isFetching || applicationQuery.isFetching}
+ loading={instanceQuery.isLoading || applicationQuery.isLoading}
/>
diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx
index 63ac405e..3dcbc963 100644
--- a/src/components/Parse/HTML.tsx
+++ b/src/components/Parse/HTML.tsx
@@ -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 (
{
- 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 (
{
accountIndex !== -1 &&
!disableDetails &&
+ differentAccount &&
navigation.push('Screen-Shared-Account', {
account: mentions[accountIndex]
})
@@ -151,11 +161,13 @@ const ParseHTML: React.FC = ({
disableDetails = false
}) => {
const navigation = useNavigation()
+ const route = useRoute()
const { theme } = useTheme()
const renderNodeCallback = useCallback(
(node, index) =>
renderNode({
+ routeParams: route.params,
theme,
node,
index,
diff --git a/src/components/Relationship/Incoming.tsx b/src/components/Relationship/Incoming.tsx
index 31eee365..a2f627a8 100644
--- a/src/components/Relationship/Incoming.tsx
+++ b/src/components/Relationship/Incoming.tsx
@@ -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 = ({ 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 = ({ id }) => {
const mutation = useMutation(fireMutation, {
onSuccess: res => {
haptics('Success')
- queryClient.setQueryData(relationshipQueryKey, res)
+ queryClient.setQueryData(queryKeyRelationship, res)
queryClient.refetchQueries(['Notifications'])
},
onError: (err: any, { type }) => {
diff --git a/src/components/Relationship/Outgoing.tsx b/src/components/Relationship/Outgoing.tsx
index c630ad7e..25ddc77c 100644
--- a/src/components/Relationship/Outgoing.tsx
+++ b/src/components/Relationship/Outgoing.tsx
@@ -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 = ({ 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 = ({ id }) => {
const mutation = useMutation(fireMutation, {
onSuccess: res => {
haptics('Success')
- queryClient.setQueryData(relationshipQueryKey, res)
+ queryClient.setQueryData(queryKeyRelationship, res)
},
onError: (err: any, { type }) => {
haptics('Error')
diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx
index 945cdb7e..6a2e60a0 100644
--- a/src/components/Timelines.tsx
+++ b/src/components/Timelines.tsx
@@ -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 = ({ name, content }) => {
+ const remoteUrl = useSelector(getRemoteUrl)
const navigation = useNavigation()
const { mode } = useTheme()
const localActiveIndex = useSelector(getLocalActiveIndex)
@@ -71,16 +72,19 @@ const Timelines: React.FC = ({ name, content }) => {
(
setSegment(nativeEvent.selectedSegmentIndex)
@@ -102,7 +106,7 @@ const Timelines: React.FC = ({ name, content }) => {
const styles = StyleSheet.create({
segmentsContainer: {
- flexBasis: '60%'
+ flexBasis: '65%'
}
})
diff --git a/src/components/Timelines/Timeline/Header.tsx b/src/components/Timelines/Timeline/Header.tsx
index 35ccaee2..4bbde688 100644
--- a/src/components/Timelines/Timeline/Header.tsx
+++ b/src/components/Timelines/Timeline/Header.tsx
@@ -16,11 +16,9 @@ const TimelineHeader = React.memo(
一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字一大堆文字{' '}
- navigation.navigate('Screen-Me', {
- screen: 'Screen-Me-Settings-UpdateRemote'
- })
- }
+ onPress={() => {
+ navigation.navigate('Screen-Me')
+ }}
>
前往设置{' '}
= ({ 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
@@ -45,12 +37,18 @@ const TimelineCard: React.FC = ({ card }) => {
await openLink(card.url)}
+ testID='base'
>
- {card.image && {cardVisual}}
+ {card.image && (
+
+ {cardVisual}
+
+ )}
{card.title}
@@ -58,6 +56,7 @@ const TimelineCard: React.FC = ({ card }) => {
{card.description}
diff --git a/src/screens/Local.tsx b/src/screens/Local.tsx
index 525cf656..6f673f81 100644
--- a/src/screens/Local.tsx
+++ b/src/screens/Local.tsx
@@ -7,7 +7,7 @@ const ScreenLocal: React.FC = () => {
return (
instance: InstanceLocal
- active?: boolean
+ disabled?: boolean
}
const AccountButton: React.FC = ({
index,
instance,
- active = false
+ disabled = false
}) => {
const queryClient = useQueryClient()
const navigation = useNavigation()
@@ -40,7 +40,7 @@ const AccountButton: React.FC = ({
return (