Lots of updates

This commit is contained in:
Zhiyuan Zheng 2021-01-10 02:12:14 +01:00
parent 4a6229514f
commit 541e2a5601
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
28 changed files with 1001 additions and 530 deletions

View File

@ -43,9 +43,12 @@ const App: React.FC = () => {
}, []) }, [])
const onBeforeLift = useCallback(async () => { 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) setLocalCorrupt(netInfoRes.corrupted)
} }

View File

@ -30,7 +30,7 @@ describe('Testing component button', () => {
it('with icon only', () => { it('with icon only', () => {
const onPress = jest.fn() const onPress = jest.fn()
const { getByTestId, toJSON } = render( const { getByTestId, toJSON } = render(
<Button type='icon' content='x' onPress={onPress} /> <Button type='icon' content='X' onPress={onPress} />
) )
fireEvent.press(getByTestId('base')) fireEvent.press(getByTestId('base'))

View File

@ -16,6 +16,7 @@ exports[`Testing component menu header with text only 1`] = `
Object { Object {
"fontSize": 14, "fontSize": 14,
"fontWeight": "600", "fontWeight": "600",
"lineHeight": 20,
}, },
Object { Object {
"color": "rgb(135, 135, 135)", "color": "rgb(135, 135, 135)",

View File

@ -15,7 +15,7 @@ exports[`Testing component menu row loading state 1`] = `
onStartShouldSetResponder={[Function]} onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 50, "minHeight": 50,
} }
} }
testID="base" testID="base"
@ -34,28 +34,35 @@ exports[`Testing component menu row loading state 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"flex": 1, "flex": 2,
"flexBasis": "70%",
"flexDirection": "row", "flexDirection": "row",
} }
} }
> >
<Text <View
numberOfLines={1}
style={ style={
Array [ Object {
Object { "flex": 1,
"flex": 1, }
"fontSize": 16,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
} }
> >
test <Text
</Text> numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test
</Text>
</View>
</View> </View>
</View> </View>
</View> </View>
@ -76,7 +83,7 @@ exports[`Testing component menu row on press event 1`] = `
onStartShouldSetResponder={[Function]} onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 50, "minHeight": 50,
} }
} }
testID="base" testID="base"
@ -95,28 +102,35 @@ exports[`Testing component menu row on press event 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"flex": 1, "flex": 2,
"flexBasis": "70%",
"flexDirection": "row", "flexDirection": "row",
} }
} }
> >
<Text <View
numberOfLines={1}
style={ style={
Array [ Object {
Object { "flex": 1,
"flex": 1, }
"fontSize": 16,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
} }
> >
test <Text
</Text> numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test
</Text>
</View>
</View> </View>
</View> </View>
</View> </View>
@ -137,7 +151,7 @@ exports[`Testing component menu row with title and content 1`] = `
onStartShouldSetResponder={[Function]} onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 50, "minHeight": 50,
} }
} }
testID="base" testID="base"
@ -156,37 +170,44 @@ exports[`Testing component menu row with title and content 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"flex": 1, "flex": 2,
"flexBasis": "70%",
"flexDirection": "row", "flexDirection": "row",
} }
} }
> >
<Text <View
numberOfLines={1}
style={ style={
Array [ Object {
Object { "flex": 1,
"flex": 1, }
"fontSize": 16,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
} }
> >
test title <Text
</Text> numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test title
</Text>
</View>
</View> </View>
<View <View
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"flex": 1, "flex": 1,
"flexBasis": "30%",
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"marginLeft": 16,
} }
} }
> >
@ -196,6 +217,7 @@ exports[`Testing component menu row with title and content 1`] = `
Array [ Array [
Object { Object {
"fontSize": 16, "fontSize": 16,
"lineHeight": 22,
}, },
Object { Object {
"color": "rgb(135, 135, 135)", "color": "rgb(135, 135, 135)",
@ -226,7 +248,7 @@ exports[`Testing component menu row with title only 1`] = `
onStartShouldSetResponder={[Function]} onStartShouldSetResponder={[Function]}
style={ style={
Object { Object {
"height": 50, "minHeight": 50,
} }
} }
testID="base" testID="base"
@ -245,28 +267,35 @@ exports[`Testing component menu row with title only 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"flex": 1, "flex": 2,
"flexBasis": "70%",
"flexDirection": "row", "flexDirection": "row",
} }
} }
> >
<Text <View
numberOfLines={1}
style={ style={
Array [ Object {
Object { "flex": 1,
"flex": 1, }
"fontSize": 16,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
} }
> >
test title <Text
</Text> numberOfLines={1}
style={
Array [
Object {
"fontSize": 16,
"lineHeight": 22,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
>
test title
</Text>
</View>
</View> </View>
</View> </View>
</View> </View>

View File

@ -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(
<Card
card={{
url: 'http://example.com',
title: 'Title'
}}
/>
)
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(
<Card
card={{
url: 'http://example.com',
title: 'Title',
description: 'Description'
}}
/>
)
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()
})
})

View File

@ -0,0 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component timeline card with text and description 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"borderRadius": 6,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
"height": 104,
"marginTop": 16,
},
Object {
"borderColor": "rgba(18, 18, 18, 0.3)",
},
]
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"padding": 8,
}
}
>
<Text
numberOfLines={2}
style={
Array [
Object {
"fontWeight": "600",
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="title"
>
Title
</Text>
<Text
numberOfLines={1}
style={
Array [
Object {
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="description"
>
Description
</Text>
<Text
numberOfLines={1}
style={
Object {
"color": "rgb(135, 135, 135)",
}
}
>
http://example.com
</Text>
</View>
</View>
`;
exports[`Testing component timeline card with text only 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"borderRadius": 6,
"borderWidth": 0.5,
"flex": 1,
"flexDirection": "row",
"height": 104,
"marginTop": 16,
},
Object {
"borderColor": "rgba(18, 18, 18, 0.3)",
},
]
}
testID="base"
>
<View
style={
Object {
"flex": 1,
"padding": 8,
}
}
>
<Text
numberOfLines={2}
style={
Array [
Object {
"fontWeight": "600",
"marginBottom": 4,
},
Object {
"color": "rgb(18, 18, 18)",
},
]
}
testID="title"
>
Title
</Text>
<Text
numberOfLines={1}
style={
Object {
"color": "rgb(135, 135, 135)",
}
}
>
http://example.com
</Text>
</View>
</View>
`;

View File

@ -1,393 +1,474 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing component button static button apply custom styling 1`] = ` exports[`Testing component button static button apply custom styling 1`] = `
<View <View>
accessible={true} <View
focusable={true} accessible={true}
onBlur={[Function]} focusable={true}
onClick={[Function]} onBlur={[Function]}
onFocus={[Function]} onClick={[Function]}
onResponderGrant={[Function]} onFocus={[Function]}
onResponderMove={[Function]} onResponderGrant={[Function]}
onResponderRelease={[Function]} onResponderMove={[Function]}
onResponderTerminate={[Function]} onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]} onResponderTerminate={[Function]}
onStartShouldSetResponder={[Function]} onResponderTerminationRequest={[Function]}
style={ onStartShouldSetResponder={[Function]}
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
Object {
"backgroundColor": "black",
},
]
}
testID="base"
>
<Text
style={ style={
Object { Array [
"color": "rgb(18, 18, 18)", Object {
"fontSize": 16, "alignItems": "center",
"fontWeight": undefined, "borderRadius": 100,
"opacity": 1, "flexDirection": "row",
} "justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
Object {
"backgroundColor": "black",
},
]
} }
testID="text" testID="base"
> >
test <Text
</Text> style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
test
</Text>
</View>
</View> </View>
`; `;
exports[`Testing component button static button disabled state 1`] = ` exports[`Testing component button static button disabled state 1`] = `
<View <View>
accessible={true} <View
focusable={true} accessible={true}
onBlur={[Function]} focusable={true}
onClick={[Function]} onBlur={[Function]}
onFocus={[Function]} onClick={[Function]}
onResponderGrant={[Function]} onFocus={[Function]}
onResponderMove={[Function]} onResponderGrant={[Function]}
onResponderRelease={[Function]} onResponderMove={[Function]}
onResponderTerminate={[Function]} onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]} onResponderTerminate={[Function]}
onStartShouldSetResponder={[Function]} onResponderTerminationRequest={[Function]}
style={ onStartShouldSetResponder={[Function]}
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={ style={
Object { Array [
"color": "rgb(135, 135, 135)", Object {
"fontSize": 16, "alignItems": "center",
"fontWeight": undefined, "borderRadius": 100,
"opacity": 1, "flexDirection": "row",
} "justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
} }
testID="text" testID="base"
> >
test <Text
</Text> style={
Object {
"color": "rgb(135, 135, 135)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
test
</Text>
</View>
</View> </View>
`; `;
exports[`Testing component button static button loading state 1`] = ` exports[`Testing component button static button loading state 1`] = `
<View <View>
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 0,
}
}
testID="text"
>
test
</Text>
<View <View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={ style={
Object { Array [
"position": "absolute", Object {
} "alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(135, 135, 135)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
} }
testID="base"
> >
<Text
style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 0,
}
}
testID="text"
>
test
</Text>
<View <View
style={ style={
Object { Object {
"alignItems": "center", "position": "absolute",
"height": 16,
"justifyContent": "center",
"opacity": 1,
"transform": Array [
Object {
"rotate": "0deg",
},
],
"width": 16,
} }
} }
> >
<View <View
style={ style={
Object { Object {
"backgroundColor": "rgb(135, 135, 135)", "alignItems": "center",
"borderRadius": 2, "height": 16,
"height": 4, "justifyContent": "center",
"position": "absolute", "opacity": 1,
"transform": Array [
Object {
"rotate": "73.27536734311887deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.7,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "46.49829517703514deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8008696779414123,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "25.743213498935145deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8875624559768125,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "11.201058030774364deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9510040862404615,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "2.731234791722257deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9881665278710133,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [ "transform": Array [
Object { Object {
"rotate": "0deg", "rotate": "0deg",
}, },
Object {
"translateY": -6,
},
Object {
"scale": 1,
},
], ],
"width": 4, "width": 16,
} }
} }
/> >
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "73.27536734311887deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.7,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "46.49829517703514deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8008696779414123,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "25.743213498935145deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.8875624559768125,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "11.201058030774364deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9510040862404615,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "2.731234791722257deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 0.9881665278710133,
},
],
"width": 4,
}
}
/>
<View
style={
Object {
"backgroundColor": "rgb(135, 135, 135)",
"borderRadius": 2,
"height": 4,
"position": "absolute",
"transform": Array [
Object {
"rotate": "0deg",
},
Object {
"translateY": -6,
},
Object {
"scale": 1,
},
],
"width": 4,
}
}
/>
</View>
</View> </View>
</View> </View>
</View> </View>
`; `;
exports[`Testing component button static button with icon only 1`] = ` exports[`Testing component button static button with icon only 1`] = `
<View <View>
accessible={true} <View
focusable={true} accessible={true}
onBlur={[Function]} focusable={true}
onClick={[Function]} onBlur={[Function]}
onFocus={[Function]} onClick={[Function]}
onResponderGrant={[Function]} onFocus={[Function]}
onResponderMove={[Function]} onResponderGrant={[Function]}
onResponderRelease={[Function]} onResponderMove={[Function]}
onResponderTerminate={[Function]} onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]} onResponderTerminate={[Function]}
onStartShouldSetResponder={[Function]} onResponderTerminationRequest={[Function]}
style={ onStartShouldSetResponder={[Function]}
Array [ style={
Object { Array [
"alignItems": "center", Object {
"borderRadius": 100, "alignItems": "center",
"flexDirection": "row", "borderRadius": 100,
"justifyContent": "center", "flexDirection": "row",
}, "justifyContent": "center",
Object { },
"backgroundColor": "rgb(250, 250, 250)", Object {
"borderColor": "rgb(18, 18, 18)", "backgroundColor": "rgb(250, 250, 250)",
"borderWidth": 1, "borderColor": "rgb(18, 18, 18)",
"paddingHorizontal": 16, "borderWidth": 1,
"paddingVertical": 8, "paddingHorizontal": 16,
}, "paddingVertical": 8,
undefined, },
] undefined,
} ]
testID="base" }
> testID="base"
<Text /> >
<View
style={
Array [
Object {
"opacity": 1,
},
Object {
"alignItems": "center",
"height": 16,
"justifyContent": "center",
"width": 16,
},
]
}
>
<RNSVGSvgView
align="xMidYMid"
bbHeight={16}
bbWidth={16}
className=""
color={4279374354}
focusable={false}
height={16}
meetOrSlice={0}
minX={0}
minY={0}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"borderWidth": 0,
},
Object {
"flex": 0,
"height": 16,
"width": 16,
},
]
}
tintColor={4279374354}
vbHeight={24}
vbWidth={24}
width={16}
>
<RNSVGGroup
propList={
Array [
"stroke",
"strokeWidth",
"strokeLinecap",
"strokeLinejoin",
]
}
stroke={
Array [
2,
]
}
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth={2}
>
<RNSVGPath
d="M18 6L6 18M6 6l12 12"
/>
</RNSVGGroup>
</RNSVGSvgView>
</View>
</View>
</View> </View>
`; `;
exports[`Testing component button static button with text only 1`] = ` exports[`Testing component button static button with text only 1`] = `
<View <View>
accessible={true} <View
focusable={true} accessible={true}
onBlur={[Function]} focusable={true}
onClick={[Function]} onBlur={[Function]}
onFocus={[Function]} onClick={[Function]}
onResponderGrant={[Function]} onFocus={[Function]}
onResponderMove={[Function]} onResponderGrant={[Function]}
onResponderRelease={[Function]} onResponderMove={[Function]}
onResponderTerminate={[Function]} onResponderRelease={[Function]}
onResponderTerminationRequest={[Function]} onResponderTerminate={[Function]}
onStartShouldSetResponder={[Function]} onResponderTerminationRequest={[Function]}
style={ onStartShouldSetResponder={[Function]}
Array [
Object {
"alignItems": "center",
"borderRadius": 100,
"flexDirection": "row",
"justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
}
testID="base"
>
<Text
style={ style={
Object { Array [
"color": "rgb(18, 18, 18)", Object {
"fontSize": 16, "alignItems": "center",
"fontWeight": undefined, "borderRadius": 100,
"opacity": 1, "flexDirection": "row",
} "justifyContent": "center",
},
Object {
"backgroundColor": "rgb(250, 250, 250)",
"borderColor": "rgb(18, 18, 18)",
"borderWidth": 1,
"paddingHorizontal": 16,
"paddingVertical": 8,
},
undefined,
]
} }
testID="text" testID="base"
> >
Test Button <Text
</Text> style={
Object {
"color": "rgb(18, 18, 18)",
"fontSize": 16,
"fontWeight": undefined,
"opacity": 1,
}
}
testID="text"
>
Test Button
</Text>
</View>
</View> </View>
`; `;

31
jest.config.js Normal file
View File

@ -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: [
'<rootDir>/jest/async-storage.js',
'<rootDir>/jest/react-native.js',
'<rootDir>/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' +
')'
]
}

3
jest/async-storage.js Normal file
View File

@ -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)

View File

@ -6,7 +6,7 @@
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"eject": "expo eject", "eject": "expo eject",
"test": "jest" "test": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^1.13.2", "@react-native-async-storage/async-storage": "^1.13.2",
@ -95,27 +95,9 @@
"chalk": "^4.1.0", "chalk": "^4.1.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"jest-expo": "^40.0.1", "jest-expo": "^40.0.1",
"nock": "^13.0.5",
"react-test-renderer": "^16.13.1", "react-test-renderer": "^16.13.1",
"typescript": "~4.1.3" "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": [
"<rootDir>/jest/react-native.js",
"<rootDir>/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 "private": true
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ const ScreenLocal: React.FC = () => {
return ( return (
<Timelines <Timelines
name='Screen-Local-Root' name='Local'
content={[ content={[
{ title: t('local:heading.segments.left'), page: 'Following' }, { title: t('local:heading.segments.left'), page: 'Following' },
{ title: t('local:heading.segments.right'), page: 'Local' } { 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 ScreenMeRoot from '@screens/Me/Root'
import ScreenMeListsList from '@screens/Me/Root/Lists/List' import ScreenMeListsList from '@screens/Me/Root/Lists/List'
import ScreenMeSettings from '@screens/Me/Settings' import ScreenMeSettings from '@screens/Me/Settings'
import UpdateRemote from '@screens/Me/Settings/UpdateRemote'
import ScreenMeSwitch from '@screens/Me/Switch' import ScreenMeSwitch from '@screens/Me/Switch'
import UpdateRemote from '@screens/Me/UpdateRemote'
import sharedScreens from '@screens/Shared/sharedScreens' import sharedScreens from '@screens/Shared/sharedScreens'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

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

View File

@ -1,4 +1,5 @@
import Timelines from '@components/Timelines' import Timelines from '@components/Timelines'
import { getRemoteUrl } from '@utils/slices/instancesSlice'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -7,10 +8,14 @@ const ScreenPublic: React.FC = () => {
return ( return (
<Timelines <Timelines
name='Screen-Public-Root' name='Public'
content={[ content={[
{ title: t('public:heading.segments.left'), page: 'LocalPublic' }, { 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, Text,
View View
} from 'react-native' } from 'react-native'
import { Chase } from 'react-native-animated-spinkit'
import { FlatList, ScrollView } from 'react-native-gesture-handler' import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { useMutation } from 'react-query' 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 ( return (
<SafeAreaView style={[styles.base, { backgroundColor: theme.background }]}> <SafeAreaView style={[styles.base, { backgroundColor: theme.background }]}>
<View style={[styles.header, { height: bottomTabBarHeight }]}> <View style={[styles.header, { height: bottomTabBarHeight }]}>
@ -211,6 +226,7 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
renderItem={renderItem} renderItem={renderItem}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={onMomentumScrollEnd} onMomentumScrollEnd={onMomentumScrollEnd}
ListEmptyComponent={ListEmptyComponent}
/> />
<View style={[styles.indicators, { height: bottomTabBarHeight }]}> <View style={[styles.indicators, { height: bottomTabBarHeight }]}>
{data && data.length > 1 ? ( {data && data.length > 1 ? (

View File

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

View File

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

View File

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

View File

@ -6513,7 +6513,7 @@ json-stable-stringify@^1.0.1:
dependencies: dependencies:
jsonify "~0.0.0" jsonify "~0.0.0"
json-stringify-safe@~5.0.1: json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
@ -6715,6 +6715,11 @@ lodash.pick@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
lodash.sortby@^4.7.0: lodash.sortby@^4.7.0:
version "4.7.0" version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -7384,6 +7389,16 @@ nocache@^2.1.0:
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
nock@^13.0.5:
version "13.0.5"
resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414"
integrity sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==
dependencies:
debug "^4.1.0"
json-stringify-safe "^5.0.1"
lodash.set "^4.3.2"
propagate "^2.0.0"
node-fetch@2.6.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0: node-fetch@2.6.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0:
version "2.6.1" version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@ -8032,6 +8047,11 @@ prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
propagate@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
proxy-from-env@^1.1.0: proxy-from-env@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"