mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +01:00
Rewrite timeline logic
This commit is contained in:
parent
45681fc1f5
commit
f3fa6bc662
2
.github/workflows/production.yml
vendored
2
.github/workflows/production.yml
vendored
@ -2,7 +2,7 @@ name: Publish production
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
- main
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -152,7 +152,7 @@ lane :build do
|
||||
else
|
||||
puts("Release #{GITHUB_RELEASE} does not exist. Create new release as well as new native build.")
|
||||
build_ios
|
||||
build_android
|
||||
# build_android
|
||||
case ENVIRONMENT
|
||||
when "staging"
|
||||
github_release(prerelease: true)
|
||||
|
28
package.json
28
package.json
@ -1,4 +1,19 @@
|
||||
{
|
||||
"name": "tooot",
|
||||
"versions": {
|
||||
"native": "210201",
|
||||
"major": 0,
|
||||
"minor": 5,
|
||||
"patch": 0,
|
||||
"expo": "40.0.0"
|
||||
},
|
||||
"description": "tooot app for Mastodon",
|
||||
"author": "xmflsct <me@xmflsct.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tooot-app/app.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
@ -65,7 +80,7 @@
|
||||
"react-native-tab-view-viewpager-adapter": "^1.1.0",
|
||||
"react-native-toast-message": "^1.4.3",
|
||||
"react-native-unimodules": "~0.12.0",
|
||||
"react-query": "^3.9.7",
|
||||
"react-query": "^3.12.0",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-timeago": "^5.2.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
@ -104,14 +119,5 @@
|
||||
"react-navigation-stack": "^2.10.2",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"typescript": "~4.1.3"
|
||||
},
|
||||
"private": true,
|
||||
"name": "tooot",
|
||||
"versions": {
|
||||
"native": "210201",
|
||||
"major": 0,
|
||||
"minor": 5,
|
||||
"patch": 0,
|
||||
"expo": "40.0.0"
|
||||
}
|
||||
}
|
||||
}
|
4
src/@types/react-navigation.d.ts
vendored
4
src/@types/react-navigation.d.ts
vendored
@ -71,7 +71,7 @@ declare namespace Nav {
|
||||
'Tab-Local': undefined
|
||||
'Tab-Public': undefined
|
||||
'Tab-Compose': undefined
|
||||
'Tab-Notifications': undefined
|
||||
'Tab-Notifications': { id?: Mastodon.Notification['id'] }
|
||||
'Tab-Me': undefined
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ declare namespace Nav {
|
||||
title: Mastodon.List['title']
|
||||
}
|
||||
'Tab-Me-Settings': undefined
|
||||
'Tab-Me-Settings-Notification': undefined
|
||||
'Tab-Me-Settings-Push': undefined
|
||||
'Tab-Me-Switch': undefined
|
||||
} & TabSharedStackParamList
|
||||
}
|
||||
|
@ -17,14 +17,16 @@ import { enableScreens } from 'react-native-screens'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import push from './startup/push'
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||
}
|
||||
Platform.select({
|
||||
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||
})
|
||||
|
||||
dev()
|
||||
sentry()
|
||||
audio()
|
||||
push()
|
||||
onlineStatus()
|
||||
|
||||
log('log', 'react-query', 'initializing')
|
||||
|
@ -15,16 +15,47 @@ import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { themes } from '@utils/styles/themes'
|
||||
import * as Analytics from 'expo-firebase-analytics'
|
||||
// import { addScreenshotListener } from 'expo-screen-capture'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { addScreenshotListener } from 'expo-screen-capture'
|
||||
import React, { createRef, useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, StatusBar } from 'react-native'
|
||||
import { Alert, Linking, Platform, StatusBar } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
|
||||
|
||||
const linking = {
|
||||
prefixes: ['tooot://', 'https://tooot.app'],
|
||||
config: {
|
||||
screens: {
|
||||
'Screen-Tabs': {
|
||||
screens: {
|
||||
'Tab-Notifications': 'push/:id'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
subscribe (listener: (arg0: string) => any) {
|
||||
const onReceiveURL = ({ url }: { url: string }) => listener(url)
|
||||
Linking.addEventListener('url', onReceiveURL)
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
response => {
|
||||
const url = response.notification.request.content.data.url
|
||||
console.log(url)
|
||||
url && typeof url === 'string' && listener(url)
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
Linking.removeEventListener('url', onReceiveURL)
|
||||
subscription.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
localCorrupt?: string
|
||||
}
|
||||
@ -57,15 +88,15 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
// }, [isConnected, firstRender])
|
||||
|
||||
// Prevent screenshot alert
|
||||
// useEffect(() => {
|
||||
// const screenshotListener = addScreenshotListener(() =>
|
||||
// Alert.alert(t('screenshot.title'), t('screenshot.message'), [
|
||||
// { text: t('screenshot.button'), style: 'destructive' }
|
||||
// ])
|
||||
// )
|
||||
// Platform.OS === 'ios' && screenshotListener
|
||||
// return () => screenshotListener.remove()
|
||||
// }, [])
|
||||
useEffect(() => {
|
||||
const screenshotListener = addScreenshotListener(() =>
|
||||
Alert.alert(t('screenshot.title'), t('screenshot.message'), [
|
||||
{ text: t('screenshot.button'), style: 'destructive' }
|
||||
])
|
||||
)
|
||||
Platform.select({ ios: screenshotListener })
|
||||
return () => screenshotListener.remove()
|
||||
}, [])
|
||||
|
||||
// On launch display login credentials corrupt information
|
||||
useEffect(() => {
|
||||
@ -127,6 +158,10 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
|
||||
if (previousRouteName !== currentRouteName) {
|
||||
Analytics.setCurrentScreen(currentRouteName)
|
||||
Sentry.Native.setContext('page', {
|
||||
previous: previousRouteName,
|
||||
current: currentRouteName
|
||||
})
|
||||
}
|
||||
|
||||
routeNameRef.current = currentRouteName
|
||||
@ -140,6 +175,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
theme={themes[mode]}
|
||||
onReady={navigationContainerOnReady}
|
||||
onStateChange={navigationContainerOnStateChange}
|
||||
linking={linking}
|
||||
>
|
||||
<Stack.Navigator initialRouteName='Screen-Tabs'>
|
||||
<Stack.Screen
|
||||
@ -185,9 +221,9 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Toast ref={Toast.setRef} config={toastConfig} />
|
||||
) : null}
|
||||
{Platform.select({
|
||||
ios: <Toast ref={Toast.setRef} config={toastConfig} />
|
||||
})}
|
||||
</NavigationContainer>
|
||||
</>
|
||||
)
|
||||
|
@ -57,13 +57,9 @@ const MenuRow: React.FC<Props> = ({
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
if (!loading) {
|
||||
onPress && onPress()
|
||||
}
|
||||
}
|
||||
}}
|
||||
onHandlerStateChange={({ nativeEvent }) =>
|
||||
nativeEvent.state === State.ACTIVE && !loading && onPress && onPress()
|
||||
}
|
||||
>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
@ -82,11 +78,6 @@ const MenuRow: React.FC<Props> = ({
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -94,21 +85,18 @@ const MenuRow: React.FC<Props> = ({
|
||||
<View style={styles.back}>
|
||||
{content ? (
|
||||
typeof content === 'string' ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{loading && !iconBack && loadingSpinkit}
|
||||
</>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
@ -119,23 +107,27 @@ const MenuRow: React.FC<Props> = ({
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<>
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
) : null}
|
||||
{loading && loadingSpinkit}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@ -147,9 +139,7 @@ const styles = StyleSheet.create({
|
||||
core: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
front: {
|
||||
flex: 2,
|
||||
@ -174,7 +164,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
description: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
marginTop: StyleConstants.Spacing.XS
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
content: {
|
||||
...StyleConstants.FontStyle.M
|
||||
|
@ -1,93 +1,61 @@
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import { useNavigation, useScrollToTop } from '@react-navigation/native'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { RefObject, useCallback, useRef } from 'react'
|
||||
import {
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
StyleSheet
|
||||
} from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue
|
||||
} from 'react-native-reanimated'
|
||||
import { InfiniteData, useQueryClient } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
import haptics from './haptics'
|
||||
import TimelineConversation from './Timeline/Conversation'
|
||||
import TimelineDefault from './Timeline/Default'
|
||||
import TimelineEmpty from './Timeline/Empty'
|
||||
import TimelineEnd from './Timeline/End'
|
||||
import TimelineNotifications from './Timeline/Notifications'
|
||||
import TimelineRefresh from './Timeline/Refresh'
|
||||
import TimelineFooter from './Timeline/Footer'
|
||||
import TimelineRefresh, {
|
||||
SEPARATION_Y_1,
|
||||
SEPARATION_Y_2
|
||||
} from './Timeline/Refresh'
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
||||
|
||||
export interface Props {
|
||||
page: App.Pages
|
||||
hashtag?: Mastodon.Tag['name']
|
||||
list?: Mastodon.List['id']
|
||||
toot?: Mastodon.Status['id']
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
account?: Mastodon.Account['id']
|
||||
flRef?: RefObject<FlatList<any>>
|
||||
queryKey: QueryKeyTimeline
|
||||
disableRefresh?: boolean
|
||||
disableInfinity?: boolean
|
||||
customProps?: Partial<FlatListProps<any>>
|
||||
customProps: Partial<FlatListProps<any>> &
|
||||
Pick<FlatListProps<any>, 'renderItem'>
|
||||
}
|
||||
|
||||
const Timeline: React.FC<Props> = ({
|
||||
page,
|
||||
hashtag,
|
||||
list,
|
||||
toot,
|
||||
rootQueryKey,
|
||||
account,
|
||||
flRef: customFLRef,
|
||||
queryKey,
|
||||
disableRefresh = false,
|
||||
disableInfinity = false,
|
||||
customProps
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
// Update timeline when account switched
|
||||
useSelector(getInstanceActive)
|
||||
|
||||
const queryKeyParams = {
|
||||
page,
|
||||
...(hashtag && { hashtag }),
|
||||
...(list && { list }),
|
||||
...(toot && { toot }),
|
||||
...(account && { account })
|
||||
}
|
||||
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', queryKeyParams]
|
||||
const {
|
||||
status,
|
||||
data,
|
||||
refetch,
|
||||
isSuccess,
|
||||
isFetching,
|
||||
isLoading,
|
||||
hasPreviousPage,
|
||||
fetchPreviousPage,
|
||||
isFetchingPreviousPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useTimelineQuery({
|
||||
...queryKeyParams,
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage?.links?.prev && {
|
||||
min_id: firstPage.links.prev,
|
||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
||||
limit: '5'
|
||||
},
|
||||
|
||||
notifyOnChangeProps: Platform.select({
|
||||
ios: ['data', 'isFetching'],
|
||||
android: ['data', 'isFetching', 'isLoading']
|
||||
}),
|
||||
getNextPageParam: lastPage =>
|
||||
lastPage?.links?.next && { max_id: lastPage.links.next }
|
||||
}
|
||||
@ -95,243 +63,87 @@ const Timeline: React.FC<Props> = ({
|
||||
|
||||
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
|
||||
const flRef = useRef<FlatList<any>>(null)
|
||||
const scrolled = useRef(false)
|
||||
useEffect(() => {
|
||||
if (toot && isSuccess && !scrolled.current) {
|
||||
scrolled.current = true
|
||||
const pointer = findIndex(flattenData, ['id', toot])
|
||||
setTimeout(() => {
|
||||
flRef.current?.scrollToIndex({
|
||||
index: pointer,
|
||||
viewOffset: 100
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}, [isSuccess, flattenData.length, scrolled])
|
||||
const onScrollToIndexFailed = useCallback(error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
setTimeout(
|
||||
() =>
|
||||
flRef.current?.scrollToIndex({ index: error.index, viewOffset: 100 }),
|
||||
350
|
||||
)
|
||||
}, [])
|
||||
|
||||
const keyExtractor = useCallback(({ id }) => id, [])
|
||||
const renderItem = useCallback(
|
||||
({ item }) => {
|
||||
switch (page) {
|
||||
case 'Conversations':
|
||||
return (
|
||||
<TimelineConversation conversation={item} queryKey={queryKey} />
|
||||
)
|
||||
case 'Notifications':
|
||||
return (
|
||||
<TimelineNotifications notification={item} queryKey={queryKey} />
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<TimelineDefault
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
{...(toot === item.id && { highlighted: true })}
|
||||
{...(toot && { rootQueryKey })}
|
||||
// @ts-ignore
|
||||
{...(data?.pages[0].pinned && { pinned: data?.pages[0].pinned })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
[data?.pages[0]]
|
||||
)
|
||||
const ItemSeparatorComponent = useCallback(
|
||||
({ leadingItem }) => (
|
||||
<ComponentSeparator
|
||||
{...(toot === leadingItem.id
|
||||
? { extraMarginLeft: 0 }
|
||||
: {
|
||||
extraMarginLeft:
|
||||
StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
})}
|
||||
/>
|
||||
),
|
||||
({ leadingItem }) =>
|
||||
queryKey[1].page === 'Toot' && queryKey[1].toot === leadingItem.id ? (
|
||||
<ComponentSeparator extraMarginLeft={0} />
|
||||
) : (
|
||||
<ComponentSeparator
|
||||
extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const flItemEmptyComponent = useMemo(
|
||||
() => <TimelineEmpty status={status} refetch={refetch} />,
|
||||
[status]
|
||||
)
|
||||
const onEndReached = useCallback(
|
||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||
[isFetchingNextPage]
|
||||
)
|
||||
const ListFooterComponent = useMemo(
|
||||
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
||||
[hasNextPage]
|
||||
)
|
||||
|
||||
useScrollToTop(flRef)
|
||||
const queryClient = useQueryClient()
|
||||
const flRef = useRef<FlatList>(null)
|
||||
const scrollY = useSharedValue(0)
|
||||
const [isFetchingLatest, setIsFetchingLatest] = useState(0)
|
||||
useEffect(() => {
|
||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
||||
if (isFetchingLatest !== 0) {
|
||||
if (!isFetchingPreviousPage) {
|
||||
fetchPreviousPage()
|
||||
setIsFetchingLatest(isFetchingLatest + 1)
|
||||
} else {
|
||||
if (isFetchingLatest === 8) {
|
||||
setIsFetchingLatest(0)
|
||||
if (data?.pages[0].body.length === 0) {
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (data?.pages[0].body.length === 0) {
|
||||
setIsFetchingLatest(0)
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
const fetchingType = useSharedValue<0 | 1 | 2>(0)
|
||||
|
||||
const onScroll = useAnimatedScrollHandler(
|
||||
{
|
||||
onScroll: ({ contentOffset: { y } }) => {
|
||||
scrollY.value = y
|
||||
},
|
||||
onEndDrag: ({ contentOffset: { y } }) => {
|
||||
if (!disableRefresh && !isFetching) {
|
||||
if (y <= SEPARATION_Y_2) {
|
||||
fetchingType.value = 2
|
||||
} else if (y <= SEPARATION_Y_1) {
|
||||
fetchingType.value = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isFetchingPreviousPage, isFetchingLatest, data?.pages[0].body])
|
||||
const onScroll = useCallback(({ nativeEvent }) => {
|
||||
scrollY.value = nativeEvent.contentOffset.y
|
||||
}, [])
|
||||
const onResponderRelease = useCallback(() => {
|
||||
if (!disableRefresh) {
|
||||
const separation01 = -(
|
||||
(StyleConstants.Spacing.M * 2.5) / 2 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
const separation02 = -(
|
||||
StyleConstants.Spacing.M * 2.5 * 1.5 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
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, isFetching, isFetchingLatest, disableRefresh])
|
||||
const headerPadding = useAnimatedStyle(() => {
|
||||
return {
|
||||
paddingTop:
|
||||
isFetchingLatest !== 0 || (isFetching && !isLoading)
|
||||
? withTiming(StyleConstants.Spacing.M * 2.5)
|
||||
: withTiming(0)
|
||||
}
|
||||
}, [isFetchingLatest, isFetching, isLoading])
|
||||
const ListHeaderComponent = useMemo(
|
||||
() => <Animated.View style={headerPadding} />,
|
||||
[]
|
||||
},
|
||||
[isFetching]
|
||||
)
|
||||
|
||||
const androidRefreshControl = useMemo(
|
||||
() =>
|
||||
Platform.OS === 'android' && {
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
enabled
|
||||
colors={[theme.primary]}
|
||||
progressBackgroundColor={theme.background}
|
||||
refreshing={isFetching || isLoading}
|
||||
onRefresh={() => refetch()}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[isFetching, isLoading]
|
||||
)
|
||||
const androidRefreshControl = Platform.select({
|
||||
android: {
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
enabled
|
||||
colors={[theme.primary]}
|
||||
progressBackgroundColor={theme.background}
|
||||
refreshing={isFetching || isLoading}
|
||||
onRefresh={() => refetch()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
useScrollToTop(flRef)
|
||||
return (
|
||||
<>
|
||||
<TimelineRefresh
|
||||
queryKey={queryKey}
|
||||
scrollY={scrollY}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
disable={disableRefresh}
|
||||
fetchingType={fetchingType}
|
||||
disableRefresh={disableRefresh}
|
||||
/>
|
||||
<FlatList
|
||||
<AnimatedFlatList
|
||||
// @ts-ignore
|
||||
ref={customFLRef || flRef}
|
||||
scrollEventThrottle={16}
|
||||
onScroll={onScroll}
|
||||
onResponderRelease={onResponderRelease}
|
||||
ref={flRef}
|
||||
windowSize={8}
|
||||
data={flattenData}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={3}
|
||||
style={styles.flatList}
|
||||
renderItem={renderItem}
|
||||
onEndReached={onEndReached}
|
||||
keyExtractor={keyExtractor}
|
||||
onEndReachedThreshold={0.75}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListEmptyComponent={flItemEmptyComponent}
|
||||
ListFooterComponent={
|
||||
<TimelineFooter
|
||||
queryKey={queryKey}
|
||||
disableInfinity={disableInfinity}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
|
||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0
|
||||
}}
|
||||
|
@ -41,10 +41,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||
pinned
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.id === next?.id
|
||||
)
|
||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => true)
|
||||
const navigation = useNavigation<
|
||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||
>()
|
||||
|
@ -1,72 +1,79 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import Icon from '@components/Icon'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import { QueryStatus } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
status: QueryStatus
|
||||
refetch: () => void
|
||||
queryKey: QueryKeyTimeline
|
||||
}
|
||||
|
||||
const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
const { mode, theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('componentTimeline')
|
||||
const TimelineEmpty = React.memo(
|
||||
({ queryKey }: Props) => {
|
||||
const { status, refetch } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: { notifyOnChangeProps: ['status'] }
|
||||
})
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
)
|
||||
case 'error':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Frown'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
{t('empty.error.message')}
|
||||
</Text>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('empty.error.button')}
|
||||
onPress={() => {
|
||||
analytics('timeline_error_press_refetch')
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Smartphone'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
{t('empty.success.message')}
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [mode, i18n.language, status])
|
||||
return (
|
||||
<View
|
||||
style={[styles.base, { backgroundColor: theme.background }]}
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const { mode, theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('componentTimeline')
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
)
|
||||
case 'error':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Frown'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
{t('empty.error.message')}
|
||||
</Text>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('empty.error.button')}
|
||||
onPress={() => {
|
||||
analytics('timeline_error_press_refetch')
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Smartphone'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
{t('empty.success.message')}
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [mode, i18n.language, status])
|
||||
return (
|
||||
<View
|
||||
style={[styles.base, { backgroundColor: theme.background }]}
|
||||
children={children}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
|
@ -1,50 +0,0 @@
|
||||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
|
||||
export interface Props {
|
||||
hasNextPage?: boolean
|
||||
}
|
||||
|
||||
const TimelineEnd: React.FC<Props> = ({ hasNextPage }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{hasNextPage ? (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
) : (
|
||||
<Text style={[styles.text, { color: theme.secondary }]}>
|
||||
<Trans
|
||||
i18nKey='componentTimeline:end.message'
|
||||
components={[
|
||||
<Icon
|
||||
name='Coffee'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: StyleConstants.Spacing.M
|
||||
},
|
||||
text: {
|
||||
...StyleConstants.FontStyle.S
|
||||
}
|
||||
})
|
||||
|
||||
export default TimelineEnd
|
63
src/components/Timeline/Footer.tsx
Normal file
63
src/components/Timeline/Footer.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import Icon from '@components/Icon'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
disableInfinity: boolean
|
||||
}
|
||||
|
||||
const TimelineFooter = React.memo(
|
||||
({ queryKey, disableInfinity }: Props) => {
|
||||
const { hasNextPage } = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: {
|
||||
enabled: !disableInfinity,
|
||||
notifyOnChangeProps: ['hasNextPage']
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{!disableInfinity && hasNextPage ? (
|
||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
) : (
|
||||
<Text style={[styles.text, { color: theme.secondary }]}>
|
||||
<Trans
|
||||
i18nKey='componentTimeline:end.message'
|
||||
components={[
|
||||
<Icon
|
||||
name='Coffee'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: StyleConstants.Spacing.M
|
||||
},
|
||||
text: {
|
||||
...StyleConstants.FontStyle.S
|
||||
}
|
||||
})
|
||||
|
||||
export default TimelineFooter
|
@ -1,8 +1,13 @@
|
||||
import haptics from '@components/haptics'
|
||||
import Icon from '@components/Icon'
|
||||
import {
|
||||
QueryKeyTimeline,
|
||||
TimelineData,
|
||||
useTimelineQuery
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
@ -15,104 +20,201 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated'
|
||||
import { InfiniteData, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
scrollY: Animated.SharedValue<number>
|
||||
isLoading: boolean
|
||||
isFetching: boolean
|
||||
disable?: boolean
|
||||
fetchingType: Animated.SharedValue<0 | 1 | 2>
|
||||
disableRefresh?: boolean
|
||||
}
|
||||
|
||||
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
|
||||
export const SEPARATION_Y_1 = -(
|
||||
CONTAINER_HEIGHT / 2 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
export const SEPARATION_Y_2 = -(
|
||||
CONTAINER_HEIGHT * 1.5 +
|
||||
StyleConstants.Font.Size.S / 2
|
||||
)
|
||||
|
||||
const TimelineRefresh = React.memo(
|
||||
({ scrollY, isLoading, isFetching, disable = false }: Props) => {
|
||||
if (disable || isLoading) {
|
||||
return null
|
||||
const TimelineRefresh: React.FC<Props> = ({
|
||||
queryKey,
|
||||
scrollY,
|
||||
fetchingType,
|
||||
disableRefresh = false
|
||||
}) => {
|
||||
if (disableRefresh) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fetchingLatestIndex = useRef(0)
|
||||
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
isFetching,
|
||||
isLoading,
|
||||
fetchPreviousPage,
|
||||
hasPreviousPage,
|
||||
isFetchingNextPage
|
||||
} = useTimelineQuery({
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage?.links?.prev && {
|
||||
min_id: firstPage.links.prev,
|
||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
||||
limit: '5'
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (fetchingLatestIndex.current > 0) {
|
||||
if (fetchingLatestIndex.current > 8) {
|
||||
clearFirstPage()
|
||||
fetchingLatestIndex.current = 0
|
||||
} else {
|
||||
if (hasPreviousPage) {
|
||||
fetchPreviousPage()
|
||||
fetchingLatestIndex.current++
|
||||
} else {
|
||||
clearFirstPage()
|
||||
fetchingLatestIndex.current = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
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 queryClient = useQueryClient()
|
||||
const clearFirstPage = useCallback(() => {
|
||||
if (data?.pages[0].body.length === 0) {
|
||||
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
|
||||
queryKey,
|
||||
data => {
|
||||
if (data?.pages[0].body.length === 0) {
|
||||
return {
|
||||
pages: data.pages.slice(1),
|
||||
pageParams: data.pageParams.slice(1)
|
||||
}
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
]
|
||||
}))
|
||||
const arrowTop = useAnimatedStyle(() => ({
|
||||
marginTop:
|
||||
scrollY.value < separation02
|
||||
? withTiming(CONTAINER_HEIGHT)
|
||||
: withTiming(0)
|
||||
}))
|
||||
)
|
||||
}
|
||||
}, [data?.pages.length && data?.pages[0].body.length])
|
||||
|
||||
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) {
|
||||
const [textRight, setTextRight] = useState(0)
|
||||
const arrowY = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollY.value,
|
||||
[0, SEPARATION_Y_1],
|
||||
[
|
||||
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
|
||||
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
|
||||
],
|
||||
Extrapolate.CLAMP
|
||||
)
|
||||
}
|
||||
]
|
||||
}))
|
||||
const arrowTop = useAnimatedStyle(() => ({
|
||||
marginTop:
|
||||
scrollY.value < SEPARATION_Y_2
|
||||
? 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 < SEPARATION_Y_1) {
|
||||
arrowStage.value = 1
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
switch (arrowStage.value) {
|
||||
case 0:
|
||||
if (scrollY.value < separation01) {
|
||||
arrowStage.value = 1
|
||||
return true
|
||||
}
|
||||
case 1:
|
||||
if (scrollY.value < SEPARATION_Y_2) {
|
||||
arrowStage.value = 2
|
||||
return true
|
||||
}
|
||||
if (scrollY.value > SEPARATION_Y_1) {
|
||||
arrowStage.value = 0
|
||||
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 > SEPARATION_Y_2) {
|
||||
arrowStage.value = 1
|
||||
return false
|
||||
case 2:
|
||||
if (scrollY.value > separation02) {
|
||||
arrowStage.value = 1
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
data => {
|
||||
if (data) {
|
||||
runOnJS(haptics)('Light')
|
||||
}
|
||||
},
|
||||
[isFetching]
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
data => {
|
||||
if (data) {
|
||||
runOnJS(haptics)('Light')
|
||||
}
|
||||
},
|
||||
[isFetching]
|
||||
)
|
||||
const wrapper = () => {
|
||||
fetchingLatestIndex.current = 1
|
||||
}
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return fetchingType.value
|
||||
},
|
||||
data => {
|
||||
fetchingType.value = 0
|
||||
switch (data) {
|
||||
case 1:
|
||||
runOnJS(wrapper)()
|
||||
runOnJS(clearFirstPage)()
|
||||
runOnJS(fetchPreviousPage)()
|
||||
break
|
||||
case 2:
|
||||
runOnJS(clearFirstPage)()
|
||||
runOnJS(refetch)()
|
||||
break
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
const headerPadding = useAnimatedStyle(
|
||||
() => ({
|
||||
paddingTop:
|
||||
fetchingLatestIndex.current !== 0 ||
|
||||
(isFetching && !isLoading && !isFetchingNextPage)
|
||||
? withTiming(StyleConstants.Spacing.M * 2.5)
|
||||
: withTiming(0)
|
||||
}),
|
||||
[fetchingLatestIndex.current, isFetching, isFetchingNextPage, isLoading]
|
||||
)
|
||||
|
||||
return (
|
||||
<Animated.View style={headerPadding}>
|
||||
<View style={styles.base}>
|
||||
{isFetching ? (
|
||||
<View style={styles.container2}>
|
||||
@ -154,11 +256,9 @@ const TimelineRefresh = React.memo(
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.isLoading === next.isLoading && prev.isFetching === next.isFetching
|
||||
)
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
|
@ -15,140 +15,139 @@ export interface Props {
|
||||
notification?: boolean
|
||||
}
|
||||
|
||||
const TimelineActioned: React.FC<Props> = ({
|
||||
account,
|
||||
action,
|
||||
notification = false
|
||||
}) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
const navigation = useNavigation<
|
||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||
>()
|
||||
const name = account.display_name || account.username
|
||||
const iconColor = theme.primary
|
||||
const TimelineActioned = React.memo(
|
||||
({ account, action, notification = false }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { theme } = useTheme()
|
||||
const navigation = useNavigation<
|
||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||
>()
|
||||
const name = account.display_name || account.username
|
||||
const iconColor = theme.primary
|
||||
|
||||
const content = (content: string) => (
|
||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||
)
|
||||
const content = (content: string) => (
|
||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||
)
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_actioned_press', { action })
|
||||
navigation.push('Tab-Shared-Account', { account })
|
||||
}, [])
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_actioned_press', { action })
|
||||
navigation.push('Tab-Shared-Account', { account })
|
||||
}, [])
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (action) {
|
||||
case 'pinned':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Anchor'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.pinned'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'favourite':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Heart'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.favourite', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'follow':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'follow_request':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow_request', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'poll':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.poll'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'reblog':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Repeat'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(
|
||||
notification
|
||||
? t('shared.actioned.reblog.notification', { name })
|
||||
: t('shared.actioned.reblog.default', { name })
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'status':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Activity'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.status', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
const children = useMemo(() => {
|
||||
switch (action) {
|
||||
case 'pinned':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Anchor'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.pinned'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'favourite':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Heart'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.favourite', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'follow':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'follow_request':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='UserPlus'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.follow_request', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'poll':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='BarChart2'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.poll'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'reblog':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Repeat'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(
|
||||
notification
|
||||
? t('shared.actioned.reblog.notification', { name })
|
||||
: t('shared.actioned.reblog.default', { name })
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
case 'status':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Activity'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(t('shared.actioned.status', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <View style={styles.actioned} children={children} />
|
||||
}
|
||||
return <View style={styles.actioned} children={children} />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actioned: {
|
||||
@ -163,4 +162,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineActioned, () => true)
|
||||
export default TimelineActioned
|
||||
|
@ -56,7 +56,10 @@ const TimelineActions: React.FC<Props> = ({
|
||||
theParams.payload.currentValue === true)
|
||||
) {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
} else if (theParams.payload.property === 'reblogged') {
|
||||
} else if (
|
||||
theParams.payload.property === 'reblogged' &&
|
||||
queryKey[1].page !== 'Following'
|
||||
) {
|
||||
// When reblogged, update cache of following page
|
||||
const tempQueryKey: QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
|
@ -16,129 +16,132 @@ export interface Props {
|
||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
||||
}
|
||||
|
||||
const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const TimelineAttachment = React.memo(
|
||||
({ status }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
||||
const onPressBlurView = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_blurview_press_show')
|
||||
layoutAnimation()
|
||||
setSensitiveShown(false)
|
||||
haptics('Light')
|
||||
}, [])
|
||||
const onPressShow = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_blurview_press_hide')
|
||||
setSensitiveShown(true)
|
||||
haptics('Light')
|
||||
}, [])
|
||||
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
||||
const onPressBlurView = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_blurview_press_show')
|
||||
layoutAnimation()
|
||||
setSensitiveShown(false)
|
||||
haptics('Light')
|
||||
}, [])
|
||||
const onPressShow = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_blurview_press_hide')
|
||||
setSensitiveShown(true)
|
||||
haptics('Light')
|
||||
}, [])
|
||||
|
||||
let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = []
|
||||
const navigation = useNavigation()
|
||||
const navigateToImagesViewer = (imageIndex: number) =>
|
||||
navigation.navigate('Screen-ImagesViewer', {
|
||||
imageUrls,
|
||||
imageIndex
|
||||
})
|
||||
const attachments = useMemo(
|
||||
() =>
|
||||
status.media_attachments.map((attachment, index) => {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
imageUrls.push({
|
||||
url: attachment.url,
|
||||
preview_url: attachment.preview_url,
|
||||
remote_url: attachment.remote_url,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height,
|
||||
imageIndex: index
|
||||
})
|
||||
return (
|
||||
<AttachmentImage
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
image={attachment}
|
||||
navigateToImagesViewer={navigateToImagesViewer}
|
||||
/>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
/>
|
||||
)
|
||||
case 'gifv':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
gifv
|
||||
/>
|
||||
)
|
||||
case 'audio':
|
||||
return (
|
||||
<AttachmentAudio
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
audio={attachment}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<AttachmentUnsupported
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}),
|
||||
[sensitiveShown]
|
||||
)
|
||||
let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = []
|
||||
const navigation = useNavigation()
|
||||
const navigateToImagesViewer = (imageIndex: number) =>
|
||||
navigation.navigate('Screen-ImagesViewer', {
|
||||
imageUrls,
|
||||
imageIndex
|
||||
})
|
||||
const attachments = useMemo(
|
||||
() =>
|
||||
status.media_attachments.map((attachment, index) => {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
imageUrls.push({
|
||||
url: attachment.url,
|
||||
preview_url: attachment.preview_url,
|
||||
remote_url: attachment.remote_url,
|
||||
width: attachment.meta?.original?.width,
|
||||
height: attachment.meta?.original?.height,
|
||||
imageIndex: index
|
||||
})
|
||||
return (
|
||||
<AttachmentImage
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
image={attachment}
|
||||
navigateToImagesViewer={navigateToImagesViewer}
|
||||
/>
|
||||
)
|
||||
case 'video':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
/>
|
||||
)
|
||||
case 'gifv':
|
||||
return (
|
||||
<AttachmentVideo
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
video={attachment}
|
||||
gifv
|
||||
/>
|
||||
)
|
||||
case 'audio':
|
||||
return (
|
||||
<AttachmentAudio
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
audio={attachment}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<AttachmentUnsupported
|
||||
key={index}
|
||||
total={status.media_attachments.length}
|
||||
index={index}
|
||||
sensitiveShown={sensitiveShown}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}),
|
||||
[sensitiveShown]
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container} children={attachments} />
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.container} children={attachments} />
|
||||
|
||||
{status.sensitive &&
|
||||
(sensitiveShown ? (
|
||||
<Pressable style={styles.sensitiveBlur}>
|
||||
{status.sensitive &&
|
||||
(sensitiveShown ? (
|
||||
<Pressable style={styles.sensitiveBlur}>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('shared.attachment.sensitive.button')}
|
||||
overlay
|
||||
onPress={onPressBlurView}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Button
|
||||
type='text'
|
||||
content={t('shared.attachment.sensitive.button')}
|
||||
type='icon'
|
||||
content='EyeOff'
|
||||
round
|
||||
overlay
|
||||
onPress={onPressBlurView}
|
||||
onPress={onPressShow}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S * 2,
|
||||
left: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Button
|
||||
type='icon'
|
||||
content='EyeOff'
|
||||
round
|
||||
overlay
|
||||
onPress={onPressShow}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: StyleConstants.Spacing.S * 2,
|
||||
left: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
@ -166,4 +169,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineAttachment, () => true)
|
||||
export default TimelineAttachment
|
||||
|
@ -13,31 +13,28 @@ export interface Props {
|
||||
navigateToImagesViewer: (imageIndex: number) => void
|
||||
}
|
||||
|
||||
const AttachmentImage: React.FC<Props> = ({
|
||||
total,
|
||||
index,
|
||||
sensitiveShown,
|
||||
image,
|
||||
navigateToImagesViewer
|
||||
}) => {
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_image_press', { id: image.id })
|
||||
navigateToImagesViewer(index)
|
||||
}, [])
|
||||
const AttachmentImage = React.memo(
|
||||
({ total, index, sensitiveShown, image, navigateToImagesViewer }: Props) => {
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_attachment_image_press', { id: image.id })
|
||||
navigateToImagesViewer(index)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GracefullyImage
|
||||
hidden={sensitiveShown}
|
||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||
blurhash={image.blurhash}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.base,
|
||||
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<GracefullyImage
|
||||
hidden={sensitiveShown}
|
||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||
blurhash={image.blurhash}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.base,
|
||||
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.sensitiveShown === next.sensitiveShown
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
@ -47,7 +44,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(
|
||||
AttachmentImage,
|
||||
(prev, next) => prev.sensitiveShown === next.sensitiveShown
|
||||
)
|
||||
export default AttachmentImage
|
||||
|
@ -11,33 +11,36 @@ export interface Props {
|
||||
account: Mastodon.Account
|
||||
}
|
||||
|
||||
const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
|
||||
const navigation = useNavigation<
|
||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||
>()
|
||||
// Need to fix go back root
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_avatar_press', {
|
||||
page: queryKey && queryKey[1].page
|
||||
})
|
||||
queryKey && navigation.push('Tab-Shared-Account', { account })
|
||||
}, [])
|
||||
const TimelineAvatar = React.memo(
|
||||
({ queryKey, account }: Props) => {
|
||||
const navigation = useNavigation<
|
||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||
>()
|
||||
// Need to fix go back root
|
||||
const onPress = useCallback(() => {
|
||||
analytics('timeline_shared_avatar_press', {
|
||||
page: queryKey && queryKey[1].page
|
||||
})
|
||||
queryKey && navigation.push('Tab-Shared-Account', { account })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GracefullyImage
|
||||
onPress={onPress}
|
||||
uri={{ original: account.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<GracefullyImage
|
||||
onPress={onPress}
|
||||
uri={{ original: account.avatar_static }}
|
||||
dimension={{
|
||||
width: StyleConstants.Avatar.M,
|
||||
height: StyleConstants.Avatar.M
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default React.memo(TimelineAvatar, () => true)
|
||||
export default TimelineAvatar
|
||||
|
@ -10,53 +10,56 @@ export interface Props {
|
||||
card: Mastodon.Card
|
||||
}
|
||||
|
||||
const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
const { theme } = useTheme()
|
||||
const TimelineCard = React.memo(
|
||||
({ card }: Props) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.card, { borderColor: theme.border }]}
|
||||
onPress={async () => {
|
||||
analytics('timeline_shared_card_press')
|
||||
await openLink(card.url)
|
||||
}}
|
||||
testID='base'
|
||||
>
|
||||
{card.image && (
|
||||
<GracefullyImage
|
||||
uri={{ original: card.image }}
|
||||
blurhash={card.blurhash}
|
||||
style={styles.left}
|
||||
imageStyle={styles.image}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.right}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.rightTitle, { color: theme.primary }]}
|
||||
testID='title'
|
||||
>
|
||||
{card.title}
|
||||
</Text>
|
||||
{card.description ? (
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.card, { borderColor: theme.border }]}
|
||||
onPress={async () => {
|
||||
analytics('timeline_shared_card_press')
|
||||
await openLink(card.url)
|
||||
}}
|
||||
testID='base'
|
||||
>
|
||||
{card.image && (
|
||||
<GracefullyImage
|
||||
uri={{ original: card.image }}
|
||||
blurhash={card.blurhash}
|
||||
style={styles.left}
|
||||
imageStyle={styles.image}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.right}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.rightTitle, { color: theme.primary }]}
|
||||
testID='title'
|
||||
>
|
||||
{card.title}
|
||||
</Text>
|
||||
{card.description ? (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.rightDescription, { color: theme.primary }]}
|
||||
testID='description'
|
||||
>
|
||||
{card.description}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.rightDescription, { color: theme.primary }]}
|
||||
testID='description'
|
||||
style={[styles.rightLink, { color: theme.secondary }]}
|
||||
>
|
||||
{card.description}
|
||||
{card.url}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[styles.rightLink, { color: theme.secondary }]}
|
||||
>
|
||||
{card.url}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
@ -93,4 +96,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineCard, () => true)
|
||||
export default TimelineCard
|
||||
|
@ -11,53 +11,56 @@ export interface Props {
|
||||
disableDetails?: boolean
|
||||
}
|
||||
|
||||
const TimelineContent: React.FC<Props> = ({
|
||||
status,
|
||||
numberOfLines,
|
||||
highlighted = false,
|
||||
disableDetails = false
|
||||
}) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const TimelineContent = React.memo(
|
||||
({
|
||||
status,
|
||||
numberOfLines,
|
||||
highlighted = false,
|
||||
disableDetails = false
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
return (
|
||||
<>
|
||||
{status.spoiler_text ? (
|
||||
<>
|
||||
<View style={{ marginBottom: StyleConstants.Font.Size.M }}>
|
||||
return (
|
||||
<>
|
||||
{status.spoiler_text ? (
|
||||
<>
|
||||
<View style={{ marginBottom: StyleConstants.Font.Size.M }}>
|
||||
<ParseHTML
|
||||
content={status.spoiler_text}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
</View>
|
||||
<ParseHTML
|
||||
content={status.spoiler_text}
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={999}
|
||||
numberOfLines={0}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={0}
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
numberOfLines={highlighted ? 999 : numberOfLines}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ParseHTML
|
||||
content={status.content}
|
||||
size={highlighted ? 'L' : 'M'}
|
||||
emojis={status.emojis}
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={highlighted ? 999 : numberOfLines}
|
||||
disableDetails={disableDetails}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default React.memo(TimelineContent, () => true)
|
||||
export default TimelineContent
|
||||
|
@ -44,78 +44,81 @@ export interface Props {
|
||||
conversation: Mastodon.Conversation
|
||||
}
|
||||
|
||||
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const HeaderConversation = React.memo(
|
||||
({ queryKey, conversation }: Props) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
queryClient,
|
||||
onMutate: true,
|
||||
onError: (err: any, _, oldData) => {
|
||||
haptics('Error')
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.error.message', {
|
||||
function: t(`shared.header.conversation.delete.function`)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
err.data &&
|
||||
err.data.error &&
|
||||
typeof err.data.error === 'string' && {
|
||||
description: err.data.error
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
queryClient,
|
||||
onMutate: true,
|
||||
onError: (err: any, _, oldData) => {
|
||||
haptics('Error')
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.error.message', {
|
||||
function: t(`shared.header.conversation.delete.function`)
|
||||
}),
|
||||
autoHide: false
|
||||
})
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
|
||||
const actionOnPress = useCallback(() => {
|
||||
analytics('timeline_conversation_delete_press')
|
||||
mutation.mutate({
|
||||
type: 'deleteItem',
|
||||
source: 'conversations',
|
||||
queryKey,
|
||||
id: conversation.id
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
err.data &&
|
||||
err.data.error &&
|
||||
typeof err.data.error === 'string' && {
|
||||
description: err.data.error
|
||||
}),
|
||||
autoHide: false
|
||||
})
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const actionChildren = useMemo(
|
||||
() => (
|
||||
<Icon
|
||||
name='Trash'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndMeta}>
|
||||
<Names accounts={conversation.accounts} />
|
||||
<View style={styles.meta}>
|
||||
{conversation.last_status?.created_at ? (
|
||||
<HeaderSharedCreated
|
||||
created_at={conversation.last_status?.created_at}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={conversation.last_status?.muted} />
|
||||
const actionOnPress = useCallback(() => {
|
||||
analytics('timeline_conversation_delete_press')
|
||||
mutation.mutate({
|
||||
type: 'deleteItem',
|
||||
source: 'conversations',
|
||||
queryKey,
|
||||
id: conversation.id
|
||||
})
|
||||
}, [])
|
||||
|
||||
const actionChildren = useMemo(
|
||||
() => (
|
||||
<Icon
|
||||
name='Trash'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndMeta}>
|
||||
<Names accounts={conversation.accounts} />
|
||||
<View style={styles.meta}>
|
||||
{conversation.last_status?.created_at ? (
|
||||
<HeaderSharedCreated
|
||||
created_at={conversation.last_status?.created_at}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={conversation.last_status?.muted} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={actionOnPress}
|
||||
children={actionChildren}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={actionOnPress}
|
||||
children={actionChildren}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
|
@ -17,50 +17,49 @@ export interface Props {
|
||||
status: Mastodon.Status
|
||||
}
|
||||
|
||||
const TimelineHeaderDefault: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
const TimelineHeaderDefault = React.memo(
|
||||
({ queryKey, rootQueryKey, status }: Props) => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.accountAndMeta}>
|
||||
<HeaderSharedAccount account={status.account} />
|
||||
<View style={styles.meta}>
|
||||
<HeaderSharedCreated created_at={status.created_at} />
|
||||
<HeaderSharedVisibility visibility={status.visibility} />
|
||||
<HeaderSharedMuted muted={status.muted} />
|
||||
<HeaderSharedApplication application={status.application} />
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.accountAndMeta}>
|
||||
<HeaderSharedAccount account={status.account} />
|
||||
<View style={styles.meta}>
|
||||
<HeaderSharedCreated created_at={status.created_at} />
|
||||
<HeaderSharedVisibility visibility={status.visibility} />
|
||||
<HeaderSharedMuted muted={status.muted} />
|
||||
<HeaderSharedApplication application={status.application} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{queryKey ? (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Actions', {
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
url: status.url || status.uri,
|
||||
type: 'status'
|
||||
})
|
||||
}
|
||||
children={
|
||||
<Icon
|
||||
name='MoreHorizontal'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
{queryKey ? (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Actions', {
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
url: status.url || status.uri,
|
||||
type: 'status'
|
||||
})
|
||||
}
|
||||
children={
|
||||
<Icon
|
||||
name='MoreHorizontal'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
@ -87,7 +86,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(
|
||||
TimelineHeaderDefault,
|
||||
(prev, next) => prev.status.muted !== next.status.muted
|
||||
)
|
||||
export default TimelineHeaderDefault
|
||||
|
@ -20,98 +20,98 @@ export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
}
|
||||
|
||||
const TimelineHeaderNotification: React.FC<Props> = ({
|
||||
queryKey,
|
||||
notification
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
const TimelineHeaderNotification = React.memo(
|
||||
({ queryKey, notification }: Props) => {
|
||||
const navigation = useNavigation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const actions = useMemo(() => {
|
||||
switch (notification.type) {
|
||||
case 'follow':
|
||||
return <RelationshipOutgoing id={notification.account.id} />
|
||||
case 'follow_request':
|
||||
return <RelationshipIncoming id={notification.account.id} />
|
||||
default:
|
||||
if (notification.status) {
|
||||
return (
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Actions', {
|
||||
queryKey,
|
||||
status: notification.status,
|
||||
url: notification.status?.url || notification.status?.uri,
|
||||
type: 'status'
|
||||
})
|
||||
}
|
||||
children={
|
||||
<Icon
|
||||
name='MoreHorizontal'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
}
|
||||
const actions = useMemo(() => {
|
||||
switch (notification.type) {
|
||||
case 'follow':
|
||||
return <RelationshipOutgoing id={notification.account.id} />
|
||||
case 'follow_request':
|
||||
return <RelationshipIncoming id={notification.account.id} />
|
||||
default:
|
||||
if (notification.status) {
|
||||
return (
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Actions', {
|
||||
queryKey,
|
||||
status: notification.status,
|
||||
url: notification.status?.url || notification.status?.uri,
|
||||
type: 'status'
|
||||
})
|
||||
}
|
||||
children={
|
||||
<Icon
|
||||
name='MoreHorizontal'
|
||||
color={theme.secondary}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [notification.type])
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View
|
||||
style={{
|
||||
flex:
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request'
|
||||
? 1
|
||||
: 4
|
||||
}}
|
||||
>
|
||||
<HeaderSharedAccount
|
||||
account={
|
||||
notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
}
|
||||
{...((notification.type === 'follow' ||
|
||||
notification.type === 'follow_request') && { withoutName: true })}
|
||||
/>
|
||||
<View style={styles.meta}>
|
||||
<HeaderSharedCreated created_at={notification.created_at} />
|
||||
{notification.status?.visibility ? (
|
||||
<HeaderSharedVisibility
|
||||
visibility={notification.status.visibility}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={notification.status?.muted} />
|
||||
<HeaderSharedApplication
|
||||
application={notification.status?.application}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [notification.type])
|
||||
</View>
|
||||
</View>
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View
|
||||
style={{
|
||||
flex:
|
||||
<View
|
||||
style={[
|
||||
styles.relationship,
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request'
|
||||
? 1
|
||||
: 4
|
||||
}}
|
||||
>
|
||||
<HeaderSharedAccount
|
||||
account={
|
||||
notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
}
|
||||
{...((notification.type === 'follow' ||
|
||||
notification.type === 'follow_request') && { withoutName: true })}
|
||||
/>
|
||||
<View style={styles.meta}>
|
||||
<HeaderSharedCreated created_at={notification.created_at} />
|
||||
{notification.status?.visibility ? (
|
||||
<HeaderSharedVisibility
|
||||
visibility={notification.status.visibility}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderSharedMuted muted={notification.status?.muted} />
|
||||
<HeaderSharedApplication
|
||||
application={notification.status?.application}
|
||||
/>
|
||||
? { flexShrink: 1 }
|
||||
: { flex: 1 }
|
||||
]}
|
||||
>
|
||||
{actions}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.relationship,
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request'
|
||||
? { flexShrink: 1 }
|
||||
: { flex: 1 }
|
||||
]}
|
||||
>
|
||||
{actions}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
@ -129,4 +129,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(TimelineHeaderNotification, () => true)
|
||||
export default TimelineHeaderNotification
|
||||
|
@ -14,6 +14,7 @@ export default {
|
||||
meLists: require('./screens/meLists').default,
|
||||
meListsList: require('./screens/meListsList').default,
|
||||
meSettings: require('./screens/meSettings').default,
|
||||
meSettingsPush: require('./screens/meSettingsPush').default,
|
||||
meSwitch: require('./screens/meSwitch').default,
|
||||
|
||||
sharedAccount: require('./screens/sharedAccount').default,
|
||||
|
@ -1,6 +1,13 @@
|
||||
export default {
|
||||
heading: 'Settings',
|
||||
content: {
|
||||
push: {
|
||||
heading: '$t(meSettingsPush:heading)',
|
||||
content: {
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled'
|
||||
}
|
||||
},
|
||||
language: {
|
||||
heading: 'Language',
|
||||
options: {
|
||||
|
30
src/i18n/en/screens/meSettingsPush.ts
Normal file
30
src/i18n/en/screens/meSettingsPush.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export default {
|
||||
heading: 'Push Notification',
|
||||
content: {
|
||||
global: {
|
||||
heading: 'Enable push notification',
|
||||
description: "Messages are routed through tooot's server"
|
||||
},
|
||||
decode: {
|
||||
heading: 'Show message details',
|
||||
description:
|
||||
"Messages routed through tooot's server are encrypted, but you can choose to decode the message on the server. Our server source code is open source, and no log policy."
|
||||
},
|
||||
follow: {
|
||||
heading: 'New follower'
|
||||
},
|
||||
favourite: {
|
||||
heading: 'Favourited'
|
||||
},
|
||||
reblog: {
|
||||
heading: 'Boosted'
|
||||
},
|
||||
mention: {
|
||||
heading: 'Mentioned you'
|
||||
},
|
||||
poll: {
|
||||
heading: 'Poll updates'
|
||||
},
|
||||
howitworks: 'Learn how routing works'
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ export default {
|
||||
meLists: require('./screens/meLists').default,
|
||||
meListsList: require('./screens/meListsList').default,
|
||||
meSettings: require('./screens/meSettings').default,
|
||||
meSettingsNotification: require('./screens/meSettingsNotification').default,
|
||||
meSettingsPush: require('./screens/meSettingsPush').default,
|
||||
meSwitch: require('./screens/meSwitch').default,
|
||||
|
||||
sharedAccount: require('./screens/sharedAccount').default,
|
||||
|
@ -12,7 +12,7 @@ export default {
|
||||
message: '居然刷到底了,喝杯 <0 /> 吧'
|
||||
},
|
||||
refresh: {
|
||||
fetchPreviousPage: '较新于此嘟嘟',
|
||||
fetchPreviousPage: '较新于此的嘟嘟',
|
||||
refetch: '最新的嘟嘟'
|
||||
},
|
||||
shared: {
|
||||
|
@ -1,8 +1,12 @@
|
||||
export default {
|
||||
heading: '设置',
|
||||
content: {
|
||||
notification: {
|
||||
heading: '$t(meSettingsNotification:heading)'
|
||||
push: {
|
||||
heading: '$t(meSettingsPush:heading)',
|
||||
content: {
|
||||
enabled: '已开启',
|
||||
disabled: '已关闭'
|
||||
}
|
||||
},
|
||||
language: {
|
||||
heading: '切换语言',
|
||||
|
@ -1,24 +0,0 @@
|
||||
export default {
|
||||
heading: '通知',
|
||||
content: {
|
||||
global: {
|
||||
heading: '启用通知',
|
||||
description: 'blahblahblah'
|
||||
},
|
||||
follow: {
|
||||
heading: '新关注者'
|
||||
},
|
||||
favourite: {
|
||||
heading: '嘟文被喜欢'
|
||||
},
|
||||
reblog: {
|
||||
heading: '嘟文被转嘟'
|
||||
},
|
||||
mention: {
|
||||
heading: '提及你'
|
||||
},
|
||||
poll: {
|
||||
heading: '投票'
|
||||
}
|
||||
}
|
||||
}
|
29
src/i18n/zh-Hans/screens/meSettingsPush.ts
Normal file
29
src/i18n/zh-Hans/screens/meSettingsPush.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export default {
|
||||
heading: '推送通知',
|
||||
content: {
|
||||
global: {
|
||||
heading: '启用通知',
|
||||
description: '通知消息将经由tooot服务器转发'
|
||||
},
|
||||
decode: {
|
||||
heading: '显示通知内容',
|
||||
description: '经由tooot服务器中转的通知消息已被加密,但可以允许tooot服务器解密并转发消息。tooot消息服务器源码开源,且不留存任何日志。'
|
||||
},
|
||||
follow: {
|
||||
heading: '新关注者'
|
||||
},
|
||||
favourite: {
|
||||
heading: '嘟文被喜欢'
|
||||
},
|
||||
reblog: {
|
||||
heading: '嘟文被转嘟'
|
||||
},
|
||||
mention: {
|
||||
heading: '提及你'
|
||||
},
|
||||
poll: {
|
||||
heading: '投票'
|
||||
},
|
||||
howitworks: '了解通知消息转发如何工作'
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import { getLocalAccount, getLocalUrl } from '@utils/slices/instancesSlice'
|
||||
import {
|
||||
getInstanceAccount,
|
||||
getInstanceUrl
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
@ -38,7 +41,7 @@ const ScreenActionsRoot = React.memo(
|
||||
const { t } = useTranslation()
|
||||
|
||||
const localAccount = useSelector(
|
||||
getLocalAccount,
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.id === next?.id
|
||||
)
|
||||
let sameAccount = false
|
||||
@ -51,7 +54,7 @@ const ScreenActionsRoot = React.memo(
|
||||
break
|
||||
}
|
||||
|
||||
const localDomain = useSelector(getLocalUrl)
|
||||
const localDomain = useSelector(getInstanceUrl)
|
||||
let sameDomain = true
|
||||
let statusDomain: string
|
||||
switch (params.type) {
|
||||
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { forEach, groupBy, sortBy } from 'lodash'
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
import { FlatList, StyleSheet, View } from 'react-native'
|
||||
import { FlatList, Image, StyleSheet, View } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import ComposeActions from './Root/Actions'
|
||||
import ComposePosting from './Posting'
|
||||
@ -15,6 +15,21 @@ import ComposeRootSuggestion from './Root/Suggestion'
|
||||
import ComposeContext from './utils/createContext'
|
||||
import ComposeDrafts from './Root/Drafts'
|
||||
|
||||
const prefetchEmojis = (
|
||||
sortedEmojis: { title: string; data: Mastodon.Emoji[] }[]
|
||||
) => {
|
||||
let requestedIndex = 0
|
||||
sortedEmojis.map(sorted => {
|
||||
sorted.data.map(emoji => {
|
||||
if (requestedIndex > 40) {
|
||||
return
|
||||
}
|
||||
Image.prefetch(emoji.url)
|
||||
requestedIndex++
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const ComposeRoot: React.FC = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
@ -52,6 +67,7 @@ const ComposeRoot: React.FC = () => {
|
||||
type: 'emoji',
|
||||
payload: { ...composeState.emoji, emojis: sortedEmojis }
|
||||
})
|
||||
prefetchEmojis(sortedEmojis)
|
||||
}
|
||||
}, [emojisData])
|
||||
|
||||
|
@ -2,9 +2,15 @@ import analytics from '@components/analytics'
|
||||
import haptics from '@components/haptics'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
SectionList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
import updateText from '../../updateText'
|
||||
|
||||
@ -25,7 +31,12 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||
haptics('Light')
|
||||
}, [composeState])
|
||||
const children = useMemo(
|
||||
() => <FastImage source={{ uri: emoji.url }} style={styles.emoji} />,
|
||||
() => (
|
||||
<Image
|
||||
source={{ uri: emoji.url, cache: 'force-cache' }}
|
||||
style={styles.emoji}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
return (
|
||||
|
@ -1,8 +1,10 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { HeaderCenter, HeaderRight } from '@components/Header'
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'
|
||||
import { ScreenTabsParamList } from '@screens/Tabs'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -48,8 +50,17 @@ const TabLocal = React.memo(
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
const children = useCallback(
|
||||
() => (instanceActive !== -1 ? <Timeline page='Following' /> : null),
|
||||
() =>
|
||||
instanceActive !== -1 ? (
|
||||
<Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
) : null,
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -12,7 +12,7 @@ import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import ScreenMeSettingsNotification from './Me/Notification'
|
||||
import ScreenMeSettingsPush from './Me/Push'
|
||||
|
||||
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
|
||||
|
||||
@ -116,13 +116,13 @@ const TabMe = React.memo(
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tab-Me-Settings-Notification'
|
||||
component={ScreenMeSettingsNotification}
|
||||
name='Tab-Me-Settings-Push'
|
||||
component={ScreenMeSettingsPush}
|
||||
options={({ navigation }: any) => ({
|
||||
headerTitle: t('meSettingsNotification:heading'),
|
||||
headerTitle: t('meSettingsPush:heading'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('meSettingsNotification:heading')} />
|
||||
<HeaderCenter content={t('meSettingsPush:heading')} />
|
||||
)
|
||||
}),
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||
|
@ -1,9 +1,12 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React from 'react'
|
||||
|
||||
const ScreenMeBookmarks = React.memo(
|
||||
() => {
|
||||
return <Timeline page='Bookmarks' />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||
|
||||
return <Timeline queryKey={queryKey} />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -1,9 +1,19 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import React from 'react'
|
||||
import TimelineConversation from '@components/Timeline/Conversation'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
const ScreenMeConversations = React.memo(
|
||||
() => {
|
||||
return <Timeline page='Conversations' />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Conversations' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineConversation conversation={item} queryKey={queryKey} />
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -1,9 +1,12 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React from 'react'
|
||||
|
||||
const ScreenMeFavourites = React.memo(
|
||||
() => {
|
||||
return <Timeline page='Favourites' />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||
|
||||
return <Timeline queryKey={queryKey} />
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
@ -1,37 +0,0 @@
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { usePushQuery } from '@utils/queryHooks/push'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
const ScreenMeSettingsNotification: React.FC = () => {
|
||||
const { t } = useTranslation('meSettingsNotification')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { data, isLoading } = usePushQuery({})
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('content.global.heading')}
|
||||
description={t('content.global.description')}
|
||||
// switchValue={notification.enabled}
|
||||
// switchOnValueChange={() => dispatch(updateNotification(true))}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('content.follow.heading')}
|
||||
loading={isLoading}
|
||||
// switchDisabled={!notification.enabled}
|
||||
// switchValue={notification.enabled ? data?.alerts.follow : false}
|
||||
// switchOnValueChange={() => dispatch(updateNotification(true))}
|
||||
/>
|
||||
</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScreenMeSettingsNotification
|
90
src/screens/Tabs/Me/Push.tsx
Normal file
90
src/screens/Tabs/Me/Push.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import { getInstancePush } from '@utils/slices/instancesSlice'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const ScreenMeSettingsPush: React.FC = () => {
|
||||
const { t } = useTranslation('meSettingsPush')
|
||||
const dispatch = useDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (['follow', 'favourite', 'reblog', 'mention', 'poll'] as [
|
||||
'follow',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll'
|
||||
]).map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`content.${alert}.heading`)}
|
||||
switchDisabled={!instancePush.global.value || isLoading}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}, [instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('content.global.heading')}
|
||||
description={t('content.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={isLoading}
|
||||
switchValue={instancePush?.global.value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePush(!instancePush?.global.value))
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('content.decode.heading')}
|
||||
description={t('content.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={!instancePush?.global.value || isLoading}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('content.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={() =>
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works')
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScreenMeSettingsPush
|
@ -1,5 +1,6 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React from 'react'
|
||||
|
||||
const ScreenMeListsList: React.FC<StackScreenProps<
|
||||
@ -10,7 +11,9 @@ const ScreenMeListsList: React.FC<StackScreenProps<
|
||||
params: { list }
|
||||
}
|
||||
}) => {
|
||||
return <Timeline page='List' list={list} />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }]
|
||||
|
||||
return <Timeline queryKey={queryKey} />
|
||||
}
|
||||
|
||||
export default ScreenMeListsList
|
||||
|
@ -4,7 +4,10 @@ import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import {
|
||||
getInstanceActive,
|
||||
getInstancePush
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import {
|
||||
changeBrowser,
|
||||
changeLanguage,
|
||||
@ -29,15 +32,24 @@ const SettingsApp: React.FC = () => {
|
||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||
const settingsTheme = useSelector(getSettingsTheme)
|
||||
const settingsBrowser = useSelector(getSettingsBrowser)
|
||||
const instancePush = useSelector(
|
||||
getInstancePush,
|
||||
(prev, next) => prev?.global.value === next?.global.value
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
{instanceActive !== -1 ? (
|
||||
<MenuRow
|
||||
title={t('content.notification.heading')}
|
||||
title={t('content.push.heading')}
|
||||
content={
|
||||
instancePush?.global.value
|
||||
? t('content.push.content.enabled')
|
||||
: t('content.push.content.disabled')
|
||||
}
|
||||
iconBack='ChevronRight'
|
||||
onPress={() => {
|
||||
navigation.navigate('Tab-Me-Settings-Notification')
|
||||
navigation.navigate('Tab-Me-Settings-Push')
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -4,16 +4,29 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { persistor } from '@root/store'
|
||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const SettingsDev: React.FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const instanceActive = useSelector(getInstanceActive)
|
||||
const instances = useSelector(getInstances)
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<Text
|
||||
selectable
|
||||
style={{
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
...StyleConstants.FontStyle.S,
|
||||
color: theme.primary
|
||||
}}
|
||||
>
|
||||
{instances[instanceActive].token}
|
||||
</Text>
|
||||
<MenuRow
|
||||
title={'Local active index'}
|
||||
content={typeof instanceActive + ' - ' + instanceActive}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { HeaderCenter } from '@components/Header'
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineNotifications from '@components/Timeline/Notifications'
|
||||
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { updateInstanceNotification } from '@utils/slices/instancesSlice'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -28,11 +30,20 @@ const TabNotifications = React.memo(
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineNotifications notification={item} queryKey={queryKey} />
|
||||
),
|
||||
[]
|
||||
)
|
||||
const children = useCallback(
|
||||
({ navigation }) => (
|
||||
<Timeline
|
||||
page='Notifications'
|
||||
queryKey={queryKey}
|
||||
customProps={{
|
||||
renderItem,
|
||||
viewabilityConfigCallbackPairs: [
|
||||
{
|
||||
onViewableItemsChanged: ({
|
||||
|
@ -1,9 +1,11 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import SegmentedControl from '@react-native-community/segmented-control'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
@ -24,9 +26,18 @@ const TabPublic = React.memo(
|
||||
const instanceActive = useSelector(getInstanceActive)
|
||||
|
||||
const [segment, setSegment] = useState(0)
|
||||
const pages: { title: string; page: App.Pages }[] = [
|
||||
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
||||
{ title: t('public:heading.segments.right'), page: 'Local' }
|
||||
const pages: {
|
||||
title: string
|
||||
key: App.Pages
|
||||
}[] = [
|
||||
{
|
||||
title: t('public:heading.segments.left'),
|
||||
key: 'LocalPublic'
|
||||
},
|
||||
{
|
||||
title: t('public:heading.segments.right'),
|
||||
key: 'Local'
|
||||
}
|
||||
]
|
||||
const screenOptions = useMemo(
|
||||
() => ({
|
||||
@ -52,7 +63,7 @@ const TabPublic = React.memo(
|
||||
<HeaderRight
|
||||
content='Search'
|
||||
onPress={() => {
|
||||
analytics('search_tap', { page: pages[segment].page })
|
||||
analytics('search_tap', { page: pages[segment].key })
|
||||
navigation.navigate('Tab-Public', { screen: 'Tab-Shared-Search' })
|
||||
}}
|
||||
/>
|
||||
@ -61,37 +72,42 @@ const TabPublic = React.memo(
|
||||
[mode, segment, i18n.language]
|
||||
)
|
||||
|
||||
const routes = pages.map(p => ({ key: p.page }))
|
||||
const routes = pages.map(p => ({ key: p.key }))
|
||||
const renderPager = useCallback(
|
||||
props => <ViewPagerAdapter {...props} />,
|
||||
[]
|
||||
)
|
||||
const renderScene = useCallback(
|
||||
({
|
||||
route
|
||||
route: { key: page }
|
||||
}: {
|
||||
route: {
|
||||
key: App.Pages
|
||||
}
|
||||
}) => {
|
||||
return instanceActive !== -1 && <Timeline page={route.key} />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page }]
|
||||
const renderItem = ({ item }) => (
|
||||
<TimelineDefault item={item} queryKey={queryKey} />
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
},
|
||||
[instanceActive]
|
||||
[]
|
||||
)
|
||||
const children = useCallback(
|
||||
() => (
|
||||
<TabView
|
||||
lazy
|
||||
swipeEnabled
|
||||
renderPager={renderPager}
|
||||
renderScene={renderScene}
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index => setSegment(index)}
|
||||
navigationState={{ index: segment, routes }}
|
||||
initialLayout={{ width: Dimensions.get('screen').width }}
|
||||
/>
|
||||
),
|
||||
[segment]
|
||||
() =>
|
||||
instanceActive !== -1 ? (
|
||||
<TabView
|
||||
lazy
|
||||
swipeEnabled
|
||||
renderPager={renderPager}
|
||||
renderScene={renderScene}
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index => setSegment(index)}
|
||||
navigationState={{ index: segment, routes }}
|
||||
initialLayout={{ width: Dimensions.get('screen').width }}
|
||||
/>
|
||||
) : null,
|
||||
[segment, instanceActive]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,9 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import Timeline from '@components/Timeline'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
@ -67,15 +69,24 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
||||
)
|
||||
}, [data])
|
||||
|
||||
const queryKey: QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
{ page: 'Account_Default', account: account.id }
|
||||
]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||
<AccountNav scrollY={scrollY} account={data} />
|
||||
|
||||
<Timeline
|
||||
page='Account_Default'
|
||||
account={account.id}
|
||||
queryKey={queryKey}
|
||||
disableRefresh
|
||||
customProps={{
|
||||
renderItem,
|
||||
onScroll,
|
||||
scrollEventThrottle: 16,
|
||||
ListHeaderComponent
|
||||
|
@ -63,7 +63,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
||||
]}
|
||||
selectable
|
||||
>
|
||||
@{myInfo ? instanceAccount.acct : account?.acct}
|
||||
@{myInfo ? instanceAccount?.acct : account?.acct}
|
||||
{myInfo ? `@${instanceUri}` : null}
|
||||
</Text>
|
||||
{movedContent}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import React from 'react'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import { SharedAttachmentsProp } from './sharedScreens'
|
||||
|
||||
const TabSharedAttachments: React.FC<SharedAttachmentsProp> = ({
|
||||
@ -7,7 +9,15 @@ const TabSharedAttachments: React.FC<SharedAttachmentsProp> = ({
|
||||
params: { account }
|
||||
}
|
||||
}) => {
|
||||
return <Timeline page='Account_Attachments' account={account.id} />
|
||||
const queryKey: QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
{ page: 'Account_Attachments', account: account.id }
|
||||
]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
}
|
||||
|
||||
export default TabSharedAttachments
|
||||
|
@ -1,5 +1,7 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import React from 'react'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React, { useCallback } from 'react'
|
||||
import { SharedHashtagProp } from './sharedScreens'
|
||||
|
||||
const TabSharedHashtag: React.FC<SharedHashtagProp> = ({
|
||||
@ -7,7 +9,12 @@ const TabSharedHashtag: React.FC<SharedHashtagProp> = ({
|
||||
params: { hashtag }
|
||||
}
|
||||
}) => {
|
||||
return <Timeline page='Hashtag' hashtag={hashtag} />
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
|
||||
const renderItem = useCallback(
|
||||
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||
[]
|
||||
)
|
||||
return <Timeline queryKey={queryKey} customProps={{ renderItem }} />
|
||||
}
|
||||
|
||||
export default TabSharedHashtag
|
||||
|
@ -1,9 +1,10 @@
|
||||
import ComponentAccount from '@components/Account'
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import TimelineEmpty from '@components/Timeline/Empty'
|
||||
import TimelineEnd from '@components/Timeline/End'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { useRelationshipsQuery } from '@utils/queryHooks/relationships'
|
||||
import {
|
||||
QueryKeyRelationships,
|
||||
useRelationshipsQuery
|
||||
} from '@utils/queryHooks/relationships'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { RefreshControl, StyleSheet } from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
@ -14,17 +15,15 @@ export interface Props {
|
||||
}
|
||||
|
||||
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
const queryKey: QueryKeyRelationships = ['Relationships', { type, id }]
|
||||
const {
|
||||
status,
|
||||
data,
|
||||
isFetching,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useRelationshipsQuery({
|
||||
type,
|
||||
id,
|
||||
...queryKey[1],
|
||||
options: {
|
||||
getPreviousPageParam: firstPage =>
|
||||
firstPage.links?.prev && { since_id: firstPage.links.next },
|
||||
@ -41,18 +40,10 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
({ item }) => <ComponentAccount account={item} origin='relationship' />,
|
||||
[]
|
||||
)
|
||||
const flItemEmptyComponent = useMemo(
|
||||
() => <TimelineEmpty status={status} refetch={refetch} />,
|
||||
[status]
|
||||
)
|
||||
const onEndReached = useCallback(
|
||||
() => !isFetchingNextPage && fetchNextPage(),
|
||||
[isFetchingNextPage]
|
||||
)
|
||||
const ListFooterComponent = useCallback(
|
||||
() => <TimelineEnd hasNextPage={hasNextPage} />,
|
||||
[hasNextPage]
|
||||
)
|
||||
const refreshControl = useMemo(
|
||||
() => (
|
||||
<RefreshControl refreshing={isFetching} onRefresh={() => refetch()} />
|
||||
@ -74,8 +65,6 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||
onEndReached={onEndReached}
|
||||
keyExtractor={keyExtractor}
|
||||
onEndReachedThreshold={0.75}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListEmptyComponent={flItemEmptyComponent}
|
||||
refreshControl={refreshControl}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
maintainVisibleContentPosition={{
|
||||
|
@ -1,5 +1,11 @@
|
||||
import Timeline from '@components/Timeline'
|
||||
import React from 'react'
|
||||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { InfiniteQueryObserver, useQueryClient } from 'react-query'
|
||||
import { SharedTootProp } from './sharedScreens'
|
||||
|
||||
const TabSharedToot: React.FC<SharedTootProp> = ({
|
||||
@ -7,11 +13,73 @@ const TabSharedToot: React.FC<SharedTootProp> = ({
|
||||
params: { toot, rootQueryKey }
|
||||
}
|
||||
}) => {
|
||||
const queryKey: QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
{ page: 'Toot', toot: toot.id }
|
||||
]
|
||||
|
||||
const flRef = useRef<FlatList>(null)
|
||||
|
||||
const [testState, setTestState] = useState(false)
|
||||
const scrolled = useRef(false)
|
||||
const navigation = useNavigation()
|
||||
const queryClient = useQueryClient()
|
||||
const observer = new InfiniteQueryObserver(queryClient, { queryKey })
|
||||
useEffect(() => {
|
||||
const unsubscribe = observer.subscribe(result => {
|
||||
if (result.isSuccess) {
|
||||
setTestState(true)
|
||||
const flattenData = result.data?.pages
|
||||
? // @ts-ignore
|
||||
result.data.pages.flatMap(d => [...d.body])
|
||||
: []
|
||||
// Auto go back when toot page is empty
|
||||
if (flattenData.length === 0) {
|
||||
navigation.goBack()
|
||||
}
|
||||
if (!scrolled.current) {
|
||||
scrolled.current = true
|
||||
const pointer = findIndex(flattenData, ['id', toot.id])
|
||||
setTimeout(() => {
|
||||
flRef.current?.scrollToIndex({
|
||||
index: pointer,
|
||||
viewOffset: 100
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
// Toot page auto scroll to selected toot
|
||||
const onScrollToIndexFailed = useCallback(error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
setTimeout(
|
||||
() =>
|
||||
flRef.current?.scrollToIndex({ index: error.index, viewOffset: 100 }),
|
||||
350
|
||||
)
|
||||
}, [])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<TimelineDefault
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
rootQueryKey={rootQueryKey}
|
||||
highlighted={toot.id === item.id}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
page='Toot'
|
||||
toot={toot.id}
|
||||
rootQueryKey={rootQueryKey}
|
||||
flRef={flRef}
|
||||
queryKey={queryKey}
|
||||
customProps={{ renderItem, ...(testState && onScrollToIndexFailed) }}
|
||||
disableRefresh
|
||||
disableInfinity
|
||||
/>
|
||||
|
15
src/startup/push.ts
Normal file
15
src/startup/push.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import log from './log'
|
||||
|
||||
const push = () => {
|
||||
log('log', 'Push', 'initializing')
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: false,
|
||||
shouldSetBadge: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default push
|
@ -1,24 +0,0 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useQuery, UseQueryOptions } from 'react-query'
|
||||
|
||||
export type QueryKey = ['Push']
|
||||
|
||||
const queryFunction = async () => {
|
||||
const res = await apiInstance<Mastodon.PushSubscription>({
|
||||
method: 'get',
|
||||
url: 'push/subscription'
|
||||
})
|
||||
return res.body
|
||||
}
|
||||
|
||||
const usePushQuery = <TData = Mastodon.PushSubscription>({
|
||||
options
|
||||
}: {
|
||||
options?: UseQueryOptions<Mastodon.PushSubscription, AxiosError, TData>
|
||||
}) => {
|
||||
const queryKey: QueryKey = ['Push']
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
}
|
||||
|
||||
export { usePushQuery }
|
@ -2,7 +2,7 @@ import apiInstance from '@api/instance'
|
||||
import { AxiosError } from 'axios'
|
||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
|
||||
|
||||
export type QueryKey = [
|
||||
export type QueryKeyRelationships = [
|
||||
'Relationships',
|
||||
{ type: 'following' | 'followers'; id: Mastodon.Account['id'] }
|
||||
]
|
||||
@ -11,7 +11,7 @@ const queryFunction = ({
|
||||
queryKey,
|
||||
pageParam
|
||||
}: {
|
||||
queryKey: QueryKey
|
||||
queryKey: QueryKeyRelationships
|
||||
pageParam?: { [key: string]: string }
|
||||
}) => {
|
||||
const { type, id } = queryKey[1]
|
||||
@ -27,7 +27,7 @@ const queryFunction = ({
|
||||
const useRelationshipsQuery = <TData = Mastodon.Account[]>({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKey[1] & {
|
||||
}: QueryKeyRelationships[1] & {
|
||||
options?: UseInfiniteQueryOptions<
|
||||
{
|
||||
body: Mastodon.Account[]
|
||||
@ -37,7 +37,10 @@ const useRelationshipsQuery = <TData = Mastodon.Account[]>({
|
||||
TData
|
||||
>
|
||||
}) => {
|
||||
const queryKey: QueryKey = ['Relationships', { ...queryKeyParams }]
|
||||
const queryKey: QueryKeyRelationships = [
|
||||
'Relationships',
|
||||
{ ...queryKeyParams }
|
||||
]
|
||||
return useInfiniteQuery(queryKey, queryFunction, options)
|
||||
}
|
||||
|
||||
|
@ -362,7 +362,7 @@ const useTimelineMutation = ({
|
||||
let oldData
|
||||
params.queryKey && (oldData = queryClient.getQueryData(params.queryKey))
|
||||
|
||||
haptics('Success')
|
||||
haptics('Light')
|
||||
switch (params.type) {
|
||||
case 'updateStatusProperty':
|
||||
updateStatusProperty({ queryClient, ...params })
|
||||
|
@ -24,7 +24,10 @@ const updateStatus = ({
|
||||
? !payload.currentValue
|
||||
: true
|
||||
if (payload.propertyCount) {
|
||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
||||
if (
|
||||
typeof payload.currentValue === 'boolean' &&
|
||||
payload.currentValue
|
||||
) {
|
||||
item.reblog![payload.propertyCount] = payload.countValue - 1
|
||||
} else {
|
||||
item.reblog![payload.propertyCount] = payload.countValue + 1
|
||||
@ -36,7 +39,10 @@ const updateStatus = ({
|
||||
? !payload.currentValue
|
||||
: true
|
||||
if (payload.propertyCount) {
|
||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
||||
if (
|
||||
typeof payload.currentValue === 'boolean' &&
|
||||
payload.currentValue
|
||||
) {
|
||||
item[payload.propertyCount] = payload.countValue - 1
|
||||
} else {
|
||||
item[payload.propertyCount] = payload.countValue + 1
|
||||
|
@ -75,8 +75,16 @@ const addInstance = createAsyncThunk(
|
||||
latestTime: undefined
|
||||
},
|
||||
push: {
|
||||
loading: false,
|
||||
enabled: false
|
||||
global: { loading: false, value: false },
|
||||
decode: { loading: false, value: false },
|
||||
alerts: {
|
||||
follow: { loading: false, value: true },
|
||||
favourite: { loading: false, value: true },
|
||||
reblog: { loading: false, value: true },
|
||||
mention: { loading: false, value: true },
|
||||
poll: { loading: false, value: true }
|
||||
},
|
||||
keys: undefined
|
||||
},
|
||||
drafts: []
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
|
||||
const serverUnregister = async () => {
|
||||
const deviceToken = (await Notifications.getDevicePushTokenAsync()).data
|
||||
|
||||
return apiGeneral<{ endpoint: string; publicKey: string; auth: string }>({
|
||||
method: 'post',
|
||||
domain: 'testpush.home.xmflsct.com',
|
||||
url: 'unregister',
|
||||
body: { deviceToken }
|
||||
})
|
||||
}
|
||||
|
||||
const pushDisable = async () => {
|
||||
await serverUnregister()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default pushDisable
|
@ -1,61 +0,0 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import apiInstance from '@api/instance'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
const serverRegister = async () => {
|
||||
const deviceToken = (await Notifications.getDevicePushTokenAsync()).data
|
||||
const expoToken = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
experienceId: '@xmflsct/tooot'
|
||||
})
|
||||
).data
|
||||
|
||||
return apiGeneral<{ endpoint: string; publicKey: string; auth: string }>({
|
||||
method: 'post',
|
||||
domain: 'testpush.home.xmflsct.com',
|
||||
url: 'register',
|
||||
body: { deviceToken, expoToken }
|
||||
})
|
||||
}
|
||||
|
||||
const pushEnable = async (): Promise<Mastodon.PushSubscription> => {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync()
|
||||
let finalStatus = existingStatus
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync()
|
||||
finalStatus = status
|
||||
}
|
||||
if (finalStatus !== 'granted') {
|
||||
alert('Failed to get push token for push notification!')
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const serverRes = (await serverRegister()).body
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append(
|
||||
'subscription[endpoint]',
|
||||
'https://testpush.home.xmflsct.com/test1'
|
||||
)
|
||||
formData.append('subscription[keys][p256dh]', serverRes.publicKey)
|
||||
formData.append('subscription[keys][auth]', serverRes.auth)
|
||||
|
||||
const res = await apiInstance<Mastodon.PushSubscription>({
|
||||
method: 'post',
|
||||
url: 'push/subscription',
|
||||
body: formData
|
||||
})
|
||||
return res.body
|
||||
|
||||
// if (Platform.OS === 'android') {
|
||||
// Notifications.setNotificationChannelAsync('default', {
|
||||
// name: 'default',
|
||||
// importance: Notifications.AndroidImportance.MAX,
|
||||
// vibrationPattern: [0, 250, 250, 250],
|
||||
// lightColor: '#FF231F7C'
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
export default pushEnable
|
127
src/utils/slices/instances/push/register.ts
Normal file
127
src/utils/slices/instances/push/register.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import apiInstance from '@api/instance'
|
||||
import { RootState } from '@root/store'
|
||||
import {
|
||||
getInstance,
|
||||
Instance,
|
||||
PUSH_SERVER
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
const register1 = async ({
|
||||
expoToken,
|
||||
instanceUrl,
|
||||
accountId,
|
||||
accountFull
|
||||
}: {
|
||||
expoToken: string
|
||||
instanceUrl: string
|
||||
accountId: Mastodon.Account['id']
|
||||
accountFull: string
|
||||
}) => {
|
||||
return apiGeneral<{
|
||||
endpoint: string
|
||||
keys: { public: string; private: string; auth: string }
|
||||
}>({
|
||||
method: 'post',
|
||||
domain: PUSH_SERVER,
|
||||
url: 'v1/register1',
|
||||
body: { expoToken, instanceUrl, accountId, accountFull }
|
||||
})
|
||||
}
|
||||
|
||||
const register2 = async ({
|
||||
expoToken,
|
||||
serverKey,
|
||||
instanceUrl,
|
||||
accountId,
|
||||
removeKeys
|
||||
}: {
|
||||
expoToken: string
|
||||
serverKey: Mastodon.PushSubscription['server_key']
|
||||
instanceUrl: string
|
||||
accountId: Mastodon.Account['id']
|
||||
removeKeys: boolean
|
||||
}) => {
|
||||
return apiGeneral({
|
||||
method: 'post',
|
||||
domain: PUSH_SERVER,
|
||||
url: 'v1/register2',
|
||||
body: { expoToken, instanceUrl, accountId, serverKey, removeKeys }
|
||||
})
|
||||
}
|
||||
|
||||
const pushRegister = async (
|
||||
state: RootState,
|
||||
expoToken: string
|
||||
): Promise<Instance['push']['keys']> => {
|
||||
const instance = getInstance(state)
|
||||
const instanceUrl = instance?.url
|
||||
const instanceUri = instance?.uri
|
||||
const instanceAccount = instance?.account
|
||||
const instancePush = instance?.push
|
||||
|
||||
if (!instanceUrl || !instanceUri || !instanceAccount || !instancePush) {
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync()
|
||||
let finalStatus = existingStatus
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync()
|
||||
finalStatus = status
|
||||
}
|
||||
if (finalStatus !== 'granted') {
|
||||
alert('Failed to get push token for push notification!')
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const accountId = instanceAccount.id
|
||||
const accountFull = `@${instanceAccount.acct}@${instanceUri}`
|
||||
const serverRes = await register1({
|
||||
expoToken,
|
||||
instanceUrl,
|
||||
accountId,
|
||||
accountFull
|
||||
})
|
||||
console.log('endpoint', serverRes.body.endpoint)
|
||||
console.log('token', instance?.token)
|
||||
|
||||
const alerts = instancePush.alerts
|
||||
const formData = new FormData()
|
||||
formData.append('subscription[endpoint]', serverRes.body.endpoint)
|
||||
formData.append('subscription[keys][p256dh]', serverRes.body.keys.public)
|
||||
formData.append('subscription[keys][auth]', serverRes.body.keys.auth)
|
||||
Object.keys(alerts).map(key =>
|
||||
// @ts-ignore
|
||||
formData.append(`data[alerts][${key}]`, alerts[key].value.toString())
|
||||
)
|
||||
|
||||
const res = await apiInstance<Mastodon.PushSubscription>({
|
||||
method: 'post',
|
||||
url: 'push/subscription',
|
||||
body: formData
|
||||
})
|
||||
|
||||
await register2({
|
||||
expoToken,
|
||||
serverKey: res.body.server_key,
|
||||
instanceUrl,
|
||||
accountId,
|
||||
removeKeys: instancePush.decode.value === false
|
||||
})
|
||||
|
||||
return Promise.resolve(serverRes.body.keys)
|
||||
|
||||
// if (Platform.OS === 'android') {
|
||||
// Notifications.setNotificationChannelAsync('default', {
|
||||
// name: 'default',
|
||||
// importance: Notifications.AndroidImportance.MAX,
|
||||
// vibrationPattern: [0, 250, 250, 250],
|
||||
// lightColor: '#FF231F7C'
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
export default pushRegister
|
32
src/utils/slices/instances/push/unregister.ts
Normal file
32
src/utils/slices/instances/push/unregister.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import apiInstance from '@api/instance'
|
||||
import { RootState } from '@root/store'
|
||||
import { getInstance, PUSH_SERVER } from '@utils/slices/instancesSlice'
|
||||
|
||||
const pushUnregister = async (state: RootState, expoToken: string) => {
|
||||
const instance = getInstance(state)
|
||||
|
||||
if (!instance?.url || !instance.account.id) {
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
await apiInstance<{}>({
|
||||
method: 'delete',
|
||||
url: 'push/subscription'
|
||||
})
|
||||
|
||||
await apiGeneral<{ endpoint: string; publicKey: string; auth: string }>({
|
||||
method: 'post',
|
||||
domain: PUSH_SERVER,
|
||||
url: 'v1/unregister',
|
||||
body: {
|
||||
expoToken,
|
||||
instanceUrl: instance.url,
|
||||
accountId: instance.account.id
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export default pushUnregister
|
@ -1,17 +1,27 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@root/store'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { Instance } from '../instancesSlice'
|
||||
import pushDisable from './push/disable'
|
||||
import pushEnable from './push/enable'
|
||||
import pushRegister from './push/register'
|
||||
import pushUnregister from './push/unregister'
|
||||
|
||||
export const updatePush = createAsyncThunk(
|
||||
export const updateInstancePush = createAsyncThunk(
|
||||
'instances/updatePush',
|
||||
async (
|
||||
enable: boolean
|
||||
): Promise<Instance['push']['subscription'] | boolean> => {
|
||||
if (enable) {
|
||||
return pushEnable()
|
||||
disable: boolean,
|
||||
{ getState }
|
||||
): Promise<Instance['push']['keys'] | undefined> => {
|
||||
const state = getState() as RootState
|
||||
const expoToken = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
experienceId: '@xmflsct/tooot'
|
||||
})
|
||||
).data
|
||||
|
||||
if (disable) {
|
||||
return await pushRegister(state, expoToken)
|
||||
} else {
|
||||
return pushDisable()
|
||||
return await pushUnregister(state, expoToken)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
27
src/utils/slices/instances/updatePushAlert.ts
Normal file
27
src/utils/slices/instances/updatePushAlert.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import apiInstance from '@api/instance'
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { Instance } from '../instancesSlice'
|
||||
|
||||
export const updateInstancePushAlert = createAsyncThunk(
|
||||
'instances/updatePushAlert',
|
||||
async ({
|
||||
alerts
|
||||
}: {
|
||||
changed: keyof Instance['push']['alerts']
|
||||
alerts: Instance['push']['alerts']
|
||||
}): Promise<Instance['push']['alerts']> => {
|
||||
const formData = new FormData()
|
||||
Object.keys(alerts).map(alert =>
|
||||
// @ts-ignore
|
||||
formData.append(`data[alerts][${alert}]`, alerts[alert].value.toString())
|
||||
)
|
||||
|
||||
await apiInstance<Mastodon.PushSubscription>({
|
||||
method: 'put',
|
||||
url: 'push/subscription',
|
||||
body: formData
|
||||
})
|
||||
|
||||
return Promise.resolve(alerts)
|
||||
}
|
||||
)
|
39
src/utils/slices/instances/updatePushDecode.ts
Normal file
39
src/utils/slices/instances/updatePushDecode.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import apiGeneral from '@api/general'
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@root/store'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { getInstance, Instance, PUSH_SERVER } from '../instancesSlice'
|
||||
|
||||
export const updateInstancePushDecode = createAsyncThunk(
|
||||
'instances/updatePushDecode',
|
||||
async (
|
||||
disalbe: boolean,
|
||||
{ getState }
|
||||
): Promise<Instance['push']['decode']['value']> => {
|
||||
const state = getState() as RootState
|
||||
const instance = getInstance(state)
|
||||
if (!instance?.url || !instance.account.id || !instance.push.keys) {
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
const expoToken = (
|
||||
await Notifications.getExpoPushTokenAsync({
|
||||
experienceId: '@xmflsct/tooot'
|
||||
})
|
||||
).data
|
||||
|
||||
await apiGeneral({
|
||||
method: 'post',
|
||||
domain: PUSH_SERVER,
|
||||
url: 'v1/update-decode',
|
||||
body: {
|
||||
expoToken,
|
||||
instanceUrl: instance.url,
|
||||
accountId: instance.account.id,
|
||||
...(disalbe && { keys: instance.push.keys })
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.resolve(disalbe)
|
||||
}
|
||||
)
|
@ -6,7 +6,11 @@ import { findIndex } from 'lodash'
|
||||
import addInstance from './instances/add'
|
||||
import removeInstance from './instances/remove'
|
||||
import { updateAccountPreferences } from './instances/updateAccountPreferences'
|
||||
import { updatePush } from './instances/updatePush'
|
||||
import { updateInstancePush } from './instances/updatePush'
|
||||
import { updateInstancePushAlert } from './instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from './instances/updatePushDecode'
|
||||
|
||||
export const PUSH_SERVER = __DEV__ ? 'testpush.tooot.app' : 'push.tooot.app'
|
||||
|
||||
export type Instance = {
|
||||
active: boolean
|
||||
@ -29,11 +33,65 @@ export type Instance = {
|
||||
readTime?: Mastodon.Notification['created_at']
|
||||
latestTime?: Mastodon.Notification['created_at']
|
||||
}
|
||||
push: {
|
||||
loading: boolean
|
||||
enabled: boolean
|
||||
subscription?: Mastodon.PushSubscription
|
||||
}
|
||||
push:
|
||||
| {
|
||||
global: { loading: boolean; value: true }
|
||||
decode: { loading: boolean; value: boolean }
|
||||
alerts: {
|
||||
follow: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['follow']
|
||||
}
|
||||
favourite: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['favourite']
|
||||
}
|
||||
reblog: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['reblog']
|
||||
}
|
||||
mention: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['mention']
|
||||
}
|
||||
poll: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['poll']
|
||||
}
|
||||
}
|
||||
keys: {
|
||||
auth: string
|
||||
public: string
|
||||
private: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
global: { loading: boolean; value: false }
|
||||
decode: { loading: boolean; value: boolean }
|
||||
alerts: {
|
||||
follow: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['follow']
|
||||
}
|
||||
favourite: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['favourite']
|
||||
}
|
||||
reblog: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['reblog']
|
||||
}
|
||||
mention: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['mention']
|
||||
}
|
||||
poll: {
|
||||
loading: boolean
|
||||
value: Mastodon.PushSubscription['alerts']['poll']
|
||||
}
|
||||
}
|
||||
keys: undefined
|
||||
}
|
||||
drafts: ComposeStateDraft[]
|
||||
}
|
||||
|
||||
@ -119,7 +177,6 @@ const instancesSlice = createSlice({
|
||||
state.instances.push(action.payload.data)
|
||||
break
|
||||
case 'overwrite':
|
||||
console.log('overwriting')
|
||||
state.instances = state.instances.map(instance => {
|
||||
if (
|
||||
instance.url === action.payload.data.url &&
|
||||
@ -152,6 +209,7 @@ const instancesSlice = createSlice({
|
||||
console.error(action.error)
|
||||
})
|
||||
|
||||
// Update Instance Account Preferences
|
||||
.addCase(updateAccountPreferences.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].account.preferences = action.payload
|
||||
@ -160,14 +218,56 @@ const instancesSlice = createSlice({
|
||||
console.error(action.error)
|
||||
})
|
||||
|
||||
.addCase(updatePush.fulfilled, (state, action) => {
|
||||
// Update Instance Push Global
|
||||
.addCase(updateInstancePush.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
if (typeof action.payload === 'boolean') {
|
||||
state.instances[activeIndex].push.enabled = action.payload
|
||||
} else {
|
||||
state.instances[activeIndex].push.enabled = true
|
||||
state.instances[activeIndex].push.subscription = action.payload
|
||||
}
|
||||
state.instances[activeIndex].push.global.loading = false
|
||||
state.instances[activeIndex].push.global.value = action.meta.arg
|
||||
state.instances[activeIndex].push.keys = action.payload
|
||||
})
|
||||
.addCase(updateInstancePush.rejected, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.global.loading = false
|
||||
})
|
||||
.addCase(updateInstancePush.pending, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.global.loading = true
|
||||
})
|
||||
|
||||
// Update Instance Push Decode
|
||||
.addCase(updateInstancePushDecode.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = false
|
||||
state.instances[activeIndex].push.decode.value = action.payload
|
||||
})
|
||||
.addCase(updateInstancePushDecode.rejected, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = false
|
||||
})
|
||||
.addCase(updateInstancePushDecode.pending, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = true
|
||||
})
|
||||
|
||||
// Update Instance Push Individual Alert
|
||||
.addCase(updateInstancePushAlert.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = false
|
||||
state.instances[activeIndex].push.alerts = action.payload
|
||||
})
|
||||
.addCase(updateInstancePushAlert.rejected, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = false
|
||||
})
|
||||
.addCase(updateInstancePushAlert.pending, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[
|
||||
action.meta.arg.changed
|
||||
].loading = true
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -217,6 +317,11 @@ export const getInstanceNotification = ({
|
||||
return instanceActive !== -1 ? instances[instanceActive].notification : null
|
||||
}
|
||||
|
||||
export const getInstancePush = ({ instances: { instances } }: RootState) => {
|
||||
const instanceActive = findInstanceActive(instances)
|
||||
return instanceActive !== -1 ? instances[instanceActive].push : null
|
||||
}
|
||||
|
||||
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
|
||||
const instanceActive = findInstanceActive(instances)
|
||||
return instanceActive !== -1 ? instances[instanceActive].drafts : null
|
||||
|
75
yarn.lock
75
yarn.lock
@ -968,6 +968,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.6.2":
|
||||
version "7.13.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
|
||||
integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3", "@babel/template@^7.8.6":
|
||||
version "7.12.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
|
||||
@ -3247,7 +3254,7 @@ bcrypt-pbkdf@^1.0.0:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
big-integer@^1.6.44:
|
||||
big-integer@^1.6.16, big-integer@^1.6.44:
|
||||
version "1.6.48"
|
||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
|
||||
integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
|
||||
@ -3319,6 +3326,19 @@ braces@^3.0.1:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
broadcast-channel@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.4.1.tgz#65b63068d0a5216026a19905c9b2d5e9adf0928a"
|
||||
integrity sha512-VXYivSkuBeQY+pL5hNQQNvBdKKQINBAROm4G8lAbWQfOZ7Yn4TMcgLNlJyEqlkxy5G8JJBsI3VJ1u8FUTOROcg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.2"
|
||||
detect-node "^2.0.4"
|
||||
js-sha3 "0.8.0"
|
||||
microseconds "0.2.0"
|
||||
nano-time "1.0.0"
|
||||
rimraf "3.0.2"
|
||||
unload "2.2.0"
|
||||
|
||||
browser-process-hrtime@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
|
||||
@ -4054,6 +4074,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
detect-node@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
|
||||
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
|
||||
|
||||
diff-sequences@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
||||
@ -6775,6 +6800,11 @@ jpeg-js@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
|
||||
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
|
||||
|
||||
js-sha3@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -7631,6 +7661,11 @@ micromatch@^4.0.2:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.0.5"
|
||||
|
||||
microseconds@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
|
||||
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
|
||||
|
||||
mime-db@1.45.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
||||
@ -7739,6 +7774,13 @@ nan@^2.12.1:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nano-time@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
|
||||
integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
|
||||
dependencies:
|
||||
big-integer "^1.6.16"
|
||||
|
||||
nanoid@^3.1.15:
|
||||
version "3.1.20"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
||||
@ -8790,12 +8832,13 @@ react-navigation@*, react-navigation@^4.4.3:
|
||||
"@react-navigation/core" "^3.7.9"
|
||||
"@react-navigation/native" "^3.8.3"
|
||||
|
||||
react-query@^3.9.7:
|
||||
version "3.9.7"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e"
|
||||
integrity sha512-vpQgRFOljd7Lr1wL8hOwxWzb7awLIjaeqaq6DJ1fzA8N9mK1fAkK+UVrt8WaXJGBfz7JEnfCiXuENQspk0N7Sw==
|
||||
react-query@^3.12.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.12.0.tgz#a2082a167f3e394e84dfd3cec0f8c7503abf33dc"
|
||||
integrity sha512-WJYECeZ6xT2oxIlgqXUjLNLWRvJbeelXscVnAFfyUFgO21OYEYHMWPG61V9W57EUUqrXioQsNPsU9XyddfEvXQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
broadcast-channel "^3.4.1"
|
||||
match-sorter "^6.0.2"
|
||||
|
||||
react-redux@^7.2.2:
|
||||
@ -9132,6 +9175,13 @@ ret@~0.1.10:
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
||||
|
||||
rimraf@3.0.2, rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^2.5.4, rimraf@^2.6.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
@ -9139,13 +9189,6 @@ rimraf@^2.5.4, rimraf@^2.6.1:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@~2.2.6:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
|
||||
@ -10215,6 +10258,14 @@ universalify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unload@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
|
||||
integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
detect-node "^2.0.4"
|
||||
|
||||
unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
Loading…
x
Reference in New Issue
Block a user