This commit is contained in:
Zhiyuan Zheng 2021-02-17 21:39:38 +01:00
parent 13efb56324
commit 796b0544c4
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
12 changed files with 274 additions and 73 deletions

View File

@ -86,6 +86,7 @@ GEM
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-json (1.0.0) fastlane-plugin-json (1.0.0)
fastlane-plugin-sentry (1.8.0)
fastlane-plugin-versioning_android (0.1.0) fastlane-plugin-versioning_android (0.1.0)
fastlane-plugin-yarn (1.2) fastlane-plugin-yarn (1.2)
gh_inspector (1.1.3) gh_inspector (1.1.3)
@ -200,6 +201,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
fastlane fastlane
fastlane-plugin-json fastlane-plugin-json
fastlane-plugin-sentry
fastlane-plugin-versioning_android fastlane-plugin-versioning_android
fastlane-plugin-yarn fastlane-plugin-yarn

View File

@ -2,7 +2,7 @@ fastlane_version "2.173.0"
skip_docs skip_docs
ensure_env_vars( ensure_env_vars(
env_vars: ["TOOOT_ENVIRONMENT"] env_vars: ["TOOOT_ENVIRONMENT", "SENTRY_ORGANIZATION", "SENTRY_PROJECT", "SENTRY_AUTH_TOKEN"]
) )
VERSIONS = read_json( json_path: "./package.json" )[:versions] VERSIONS = read_json( json_path: "./package.json" )[:versions]
@ -15,7 +15,7 @@ case ENVIRONMENT
when "development" when "development"
GITHUB_RELEASE= "" GITHUB_RELEASE= ""
when "staging" when "staging"
GITHUB_RELEASE = "v#{VERSION}-rc#{VERSIONS[:patch]}" GITHUB_RELEASE = "v#{VERSION}-#{VERSIONS[:patch]}"
when "production" when "production"
GITHUB_RELEASE = "v#{VERSION}" GITHUB_RELEASE = "v#{VERSION}"
end end
@ -37,6 +37,15 @@ private_lane :update_expo_ios do
set_info_plist_value( path: EXPO_PLIST, key: "EXUpdatesReleaseChannel", value: RELEASE_CHANNEL ) set_info_plist_value( path: EXPO_PLIST, key: "EXUpdatesReleaseChannel", value: RELEASE_CHANNEL )
end end
desc 'IOS: Upload dSYM'
lane :upload_symbols_ios do
download_dsyms
sentry_upload_dsym(
org_slug: ENV["SENTRY_ORGANIZATION"],
project_slug: ENV["SENTRY_PROJECT"]
)
end
desc "ANDROID: Prepare play store" desc "ANDROID: Prepare play store"
private_lane :prepare_playstore_android do private_lane :prepare_playstore_android do
android_set_version_name( version_name: VERSION, gradle_file: "./android/app/build.gradle" ) android_set_version_name( version_name: VERSION, gradle_file: "./android/app/build.gradle" )
@ -77,13 +86,14 @@ private_lane :build_ios do
when "staging" when "staging"
prepare_appstore_ios prepare_appstore_ios
match( type: "appstore", readonly: true ) match( type: "appstore", readonly: true )
build_ios_app( export_method: "app-store" ) build_ios_app( export_method: "app-store", include_symbols: true, include_bitcode: true )
upload_to_testflight( upload_to_testflight(
demo_account_required: true, demo_account_required: true,
distribute_external: true, distribute_external: true,
groups: "内测用户", groups: "内测用户",
changelog: "Ready for testing" changelog: "Ready for testing"
) )
upload_symbols_ios
when "production" when "production"
prepare_appstore_ios prepare_appstore_ios
match( type: "appstore", readonly: true ) match( type: "appstore", readonly: true )

View File

@ -5,3 +5,4 @@
gem 'fastlane-plugin-yarn' gem 'fastlane-plugin-yarn'
gem 'fastlane-plugin-json' gem 'fastlane-plugin-json'
gem 'fastlane-plugin-versioning_android' gem 'fastlane-plugin-versioning_android'
gem 'fastlane-plugin-sentry'

View File

@ -64,7 +64,7 @@
"react-native-tab-view-viewpager-adapter": "^1.1.0", "react-native-tab-view-viewpager-adapter": "^1.1.0",
"react-native-toast-message": "^1.4.3", "react-native-toast-message": "^1.4.3",
"react-native-unimodules": "~0.12.0", "react-native-unimodules": "~0.12.0",
"react-query": "^3.8.2", "react-query": "^3.9.7",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-timeago": "^5.2.0", "react-timeago": "^5.2.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",

View File

@ -162,12 +162,12 @@ const ComponentInstance: React.FC<Props> = ({
type='text' type='text'
content={t('server.button.local')} content={t('server.button.local')}
onPress={processUpdate} onPress={processUpdate}
disabled={!instanceQuery.data?.uri || !agreed} disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching} loading={instanceQuery.isFetching || appsQuery.isFetching}
/> />
</View> </View>
<EULA agreed={agreed} setAgreed={setAgreed} /> {/* <EULA agreed={agreed} setAgreed={setAgreed} /> */}
<View> <View>
<Placeholder <Placeholder

View File

@ -82,19 +82,26 @@ const Timeline: React.FC<Props> = ({
...queryKeyParams, ...queryKeyParams,
options: { options: {
getPreviousPageParam: firstPage => getPreviousPageParam: firstPage =>
firstPage.links?.prev && { firstPage?.links?.prev && {
min_id: firstPage.links.prev, min_id: firstPage.links.prev,
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
limit: '6' limit: '5'
}, },
getNextPageParam: lastPage => getNextPageParam: lastPage =>
lastPage.links?.next && { max_id: lastPage.links.next } lastPage?.links?.next && { max_id: lastPage.links.next }
} }
}) })
const flattenData = data?.pages ? data.pages.flatMap(d => [...d.body]) : [] const flattenData = data?.pages ? data.pages.flatMap(d => [...d.body]) : []
// Auto go back when toot page is empty
const navigation = useNavigation()
useEffect(() => {
if (toot && isSuccess && flattenData.length === 0) {
navigation.goBack()
}
}, [isSuccess, flattenData.length])
// Toot page auto scroll to selected toot // Toot page auto scroll to selected toot
const flRef = useRef<FlatList<any>>(null) const flRef = useRef<FlatList<any>>(null)
const scrolled = useRef(false) const scrolled = useRef(false)
@ -119,13 +126,6 @@ const Timeline: React.FC<Props> = ({
350 350
) )
}, []) }, [])
// Auto go back when toot page is empty
const navigation = useNavigation()
useEffect(() => {
if (toot && isSuccess && flattenData.length === 0) {
navigation.goBack()
}
}, [isSuccess, flattenData.length])
const keyExtractor = useCallback(({ id }) => id, []) const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback( const renderItem = useCallback(
@ -233,26 +233,57 @@ const Timeline: React.FC<Props> = ({
scrollY.value = nativeEvent.contentOffset.y scrollY.value = nativeEvent.contentOffset.y
}, []) }, [])
const onResponderRelease = useCallback(() => { const onResponderRelease = useCallback(() => {
if ( if (!disableRefresh) {
scrollY.value <= -StyleConstants.Spacing.XL && const separation01 = -(
isFetchingLatest === 0 && (StyleConstants.Spacing.M * 2.5) / 2 +
!disableRefresh StyleConstants.Font.Size.S / 2
) { )
haptics('Light') const separation02 = -(
setIsFetchingLatest(1) StyleConstants.Spacing.M * 2.5 * 1.5 +
flRef.current?.scrollToOffset({ StyleConstants.Font.Size.S / 2
animated: true, )
offset: 1 if (
}) scrollY.value <= separation02 &&
!isFetching &&
isFetchingLatest === 0
) {
haptics('Light')
queryClient.setQueryData<InfiniteData<any> | undefined>(
queryKey,
data => {
if (data?.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
}
}
)
refetch()
} else if (
scrollY.value <= separation01 &&
!isFetching &&
isFetchingLatest === 0
) {
haptics('Light')
setIsFetchingLatest(1)
flRef.current?.scrollToOffset({
animated: true,
offset: 1
})
}
} }
}, [scrollY.value, isFetchingLatest, disableRefresh]) }, [scrollY.value, isFetching, isFetchingLatest, disableRefresh])
const headerPadding = useAnimatedStyle(() => { const headerPadding = useAnimatedStyle(() => {
if (isFetchingLatest !== 0) { return {
return { paddingTop: withTiming(StyleConstants.Spacing.XL) } paddingTop:
} else { isFetchingLatest !== 0 || (isFetching && !isLoading)
return { paddingTop: withTiming(0) } ? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0)
} }
}, [isFetchingLatest]) }, [isFetchingLatest, isFetching, isLoading])
const ListHeaderComponent = useMemo( const ListHeaderComponent = useMemo(
() => <Animated.View style={headerPadding} />, () => <Animated.View style={headerPadding} />,
[] []
@ -267,7 +298,7 @@ const Timeline: React.FC<Props> = ({
colors={[theme.primary]} colors={[theme.primary]}
progressBackgroundColor={theme.background} progressBackgroundColor={theme.background}
refreshing={isFetching || isLoading} refreshing={isFetching || isLoading}
// onRefresh={() => refetch()} onRefresh={() => refetch()}
/> />
) )
}, },
@ -276,8 +307,14 @@ const Timeline: React.FC<Props> = ({
return ( return (
<> <>
<TimelineRefresh isLoading={isLoading} disable={disableRefresh} /> <TimelineRefresh
scrollY={scrollY}
isLoading={isLoading}
isFetching={isFetching}
disable={disableRefresh}
/>
<FlatList <FlatList
scrollEventThrottle={16}
onScroll={onScroll} onScroll={onScroll}
onResponderRelease={onResponderRelease} onResponderRelease={onResponderRelease}
ref={flRef} ref={flRef}

View File

@ -13,6 +13,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } 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 { uniqBy } from 'lodash'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -75,7 +76,6 @@ const TimelineDefault: React.FC<Props> = ({
} }
]} ]}
onPress={onPress} onPress={onPress}
disabled={queryKey && queryKey[1].toot !== undefined}
> >
{item.reblog ? ( {item.reblog ? (
<TimelineActioned action='reblog' account={item.account} /> <TimelineActioned action='reblog' account={item.account} />
@ -143,11 +143,13 @@ const TimelineDefault: React.FC<Props> = ({
queryKey={queryKey} queryKey={queryKey}
rootQueryKey={rootQueryKey} rootQueryKey={rootQueryKey}
status={actualStatus} status={actualStatus}
accts={([actualStatus.account] as Mastodon.Account[] & accts={uniqBy(
Mastodon.Mention[]) ([actualStatus.account] as Mastodon.Account[] &
.concat(actualStatus.mentions) Mastodon.Mention[])
.filter(d => d.id !== localAccount?.id) .concat(actualStatus.mentions)
.map(d => d.acct)} .filter(d => d.id !== localAccount?.id),
d => d.id
).map(d => d.acct)}
reblog={item.reblog ? true : false} reblog={item.reblog ? true : false}
/> />
</View> </View>

View File

@ -13,6 +13,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } 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 { uniqBy } from 'lodash'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -126,11 +127,13 @@ const TimelineNotifications: React.FC<Props> = ({
<TimelineActions <TimelineActions
queryKey={queryKey} queryKey={queryKey}
status={notification.status} status={notification.status}
accts={([notification.status.account] as Mastodon.Account[] & accts={uniqBy(
Mastodon.Mention[]) ([notification.status.account] as Mastodon.Account[] &
.concat(notification.status.mentions) Mastodon.Mention[])
.filter(d => d.id !== localAccount?.id) .concat(notification.status.mentions)
.map(d => d.acct)} .filter(d => d.id !== localAccount?.id),
d => d.id
).map(d => d.acct)}
reblog={false} reblog={false}
/> />
</View> </View>

View File

@ -1,25 +1,164 @@
import haptics from '@components/haptics'
import Icon from '@components/Icon'
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 React from 'react' import React, { useCallback, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit' import { Circle } from 'react-native-animated-spinkit'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
export interface Props { export interface Props {
scrollY: Animated.SharedValue<number>
isLoading: boolean isLoading: boolean
isFetching: boolean
disable?: boolean disable?: boolean
} }
const TimelineRefresh: React.FC<Props> = ({ isLoading, disable = false }) => { const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
const { theme } = useTheme()
return !isLoading && !disable ? ( const TimelineRefresh = React.memo(
<View ({ scrollY, isLoading, isFetching, disable = false }: Props) => {
style={styles.base} if (disable || isLoading) {
children={ return null
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} /> }
}
/> const { t } = useTranslation('componentTimeline')
) : null const { theme } = useTheme()
}
const separation01 = -(
CONTAINER_HEIGHT / 2 +
StyleConstants.Font.Size.S / 2
)
const separation02 = -(
CONTAINER_HEIGHT * 1.5 +
StyleConstants.Font.Size.S / 2
)
const [textRight, setTextRight] = useState(0)
const arrowY = useAnimatedStyle(() => ({
transform: [
{
translateY: interpolate(
scrollY.value,
[0, separation01],
[
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
],
Extrapolate.CLAMP
)
}
]
}))
const arrowTop = useAnimatedStyle(() => ({
marginTop:
scrollY.value < separation02
? withTiming(CONTAINER_HEIGHT)
: withTiming(0)
}))
const arrowStage = useSharedValue(0)
const onLayout = useCallback(
({ nativeEvent }) => {
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
}
},
[textRight]
)
useAnimatedReaction(
() => {
if (isFetching) {
return false
}
switch (arrowStage.value) {
case 0:
if (scrollY.value < separation01) {
arrowStage.value = 1
return true
}
return false
case 1:
if (scrollY.value < separation02) {
arrowStage.value = 2
return true
}
if (scrollY.value > separation01) {
arrowStage.value = 0
return false
}
return false
case 2:
if (scrollY.value > separation02) {
arrowStage.value = 1
return false
}
return false
}
},
data => {
if (data) {
runOnJS(haptics)('Light')
}
},
[isFetching]
)
return (
<View style={styles.base}>
{isFetching ? (
<View style={styles.container2}>
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
</View>
) : (
<>
<View style={styles.container1}>
<Text
style={[styles.explanation, { color: theme.primary }]}
onLayout={onLayout}
children={t('refresh.fetchPreviousPage')}
/>
<Animated.View
style={[
{
position: 'absolute',
left: textRight + StyleConstants.Spacing.S
},
arrowY,
arrowTop
]}
children={
<Icon
name='ArrowLeft'
size={StyleConstants.Font.Size.M}
color={theme.primary}
/>
}
/>
</View>
<View style={styles.container2}>
<Text
style={[styles.explanation, { color: theme.primary }]}
onLayout={onLayout}
children={t('refresh.refetch')}
/>
</View>
</>
)}
</View>
)
},
(prev, next) =>
prev.isLoading === next.isLoading && prev.isFetching === next.isFetching
)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
@ -27,9 +166,18 @@ const styles = StyleSheet.create({
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
height: StyleConstants.Spacing.XL, height: CONTAINER_HEIGHT * 2,
justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
},
container1: {
flex: 1,
flexDirection: 'row',
height: CONTAINER_HEIGHT
},
container2: { height: CONTAINER_HEIGHT, justifyContent: 'center' },
explanation: {
fontSize: StyleConstants.Font.Size.S,
lineHeight: CONTAINER_HEIGHT
} }
}) })

View File

@ -11,10 +11,9 @@ export default {
end: { end: {
message: 'The end, what about a cup of <0 />' message: 'The end, what about a cup of <0 />'
}, },
header: { refresh: {
explanation: fetchPreviousPage: 'Refresh upwards',
'External instance might not be known to logged in instance, thus actions are not allowed but only for reading. You can switch to any instance in the settings.', refetch: 'Refresh all'
button: 'Go to settings'
}, },
shared: { shared: {
actioned: { actioned: {

View File

@ -11,10 +11,9 @@ export default {
end: { end: {
message: '居然刷到底了,喝杯 <0 /> 吧' message: '居然刷到底了,喝杯 <0 /> 吧'
}, },
header: { refresh: {
explanation: fetchPreviousPage: '向上刷新',
'围观的社区可能不属于已经登录的社区的已知连结,因此只可围观嘟文,不能进行操作。设置里可以切换想要围观的社区。', refetch: '完全刷新'
button: '前往设置'
}, },
shared: { shared: {
actioned: { actioned: {

View File

@ -8736,10 +8736,10 @@ react-navigation@*, react-navigation@^4.4.3:
"@react-navigation/core" "^3.7.9" "@react-navigation/core" "^3.7.9"
"@react-navigation/native" "^3.8.3" "@react-navigation/native" "^3.8.3"
react-query@^3.8.2: react-query@^3.9.7:
version "3.8.2" version "3.9.7"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.8.2.tgz#e2dac76b5d9b3465d854f4ca040a35ba4bcae1ae" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e"
integrity sha512-Ha8+WZLIHOUkKhqE4+2RZZB03WvEWmNhN4WIIw3zWVCtR7blo2n2TXtu+d3YhaY1o6dt+sHT+J+zNV8IzR583w== integrity sha512-vpQgRFOljd7Lr1wL8hOwxWzb7awLIjaeqaq6DJ1fzA8N9mK1fAkK+UVrt8WaXJGBfz7JEnfCiXuENQspk0N7Sw==
dependencies: dependencies:
"@babel/runtime" "^7.5.5" "@babel/runtime" "^7.5.5"
match-sorter "^6.0.2" match-sorter "^6.0.2"