mirror of https://github.com/tooot-app/app
Rewrite timeline logic
This commit is contained in:
parent
45681fc1f5
commit
f3fa6bc662
|
@ -2,7 +2,7 @@ name: Publish production
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- production
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
@ -152,7 +152,7 @@ lane :build do
|
||||||
else
|
else
|
||||||
puts("Release #{GITHUB_RELEASE} does not exist. Create new release as well as new native build.")
|
puts("Release #{GITHUB_RELEASE} does not exist. Create new release as well as new native build.")
|
||||||
build_ios
|
build_ios
|
||||||
build_android
|
# build_android
|
||||||
case ENVIRONMENT
|
case ENVIRONMENT
|
||||||
when "staging"
|
when "staging"
|
||||||
github_release(prerelease: true)
|
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": {
|
"scripts": {
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
@ -65,7 +80,7 @@
|
||||||
"react-native-tab-view-viewpager-adapter": "^1.1.0",
|
"react-native-tab-view-viewpager-adapter": "^1.1.0",
|
||||||
"react-native-toast-message": "^1.4.3",
|
"react-native-toast-message": "^1.4.3",
|
||||||
"react-native-unimodules": "~0.12.0",
|
"react-native-unimodules": "~0.12.0",
|
||||||
"react-query": "^3.9.7",
|
"react-query": "^3.12.0",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-timeago": "^5.2.0",
|
"react-timeago": "^5.2.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
@ -104,14 +119,5 @@
|
||||||
"react-navigation-stack": "^2.10.2",
|
"react-navigation-stack": "^2.10.2",
|
||||||
"react-test-renderer": "^16.13.1",
|
"react-test-renderer": "^16.13.1",
|
||||||
"typescript": "~4.1.3"
|
"typescript": "~4.1.3"
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"name": "tooot",
|
|
||||||
"versions": {
|
|
||||||
"native": "210201",
|
|
||||||
"major": 0,
|
|
||||||
"minor": 5,
|
|
||||||
"patch": 0,
|
|
||||||
"expo": "40.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -71,7 +71,7 @@ declare namespace Nav {
|
||||||
'Tab-Local': undefined
|
'Tab-Local': undefined
|
||||||
'Tab-Public': undefined
|
'Tab-Public': undefined
|
||||||
'Tab-Compose': undefined
|
'Tab-Compose': undefined
|
||||||
'Tab-Notifications': undefined
|
'Tab-Notifications': { id?: Mastodon.Notification['id'] }
|
||||||
'Tab-Me': undefined
|
'Tab-Me': undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ declare namespace Nav {
|
||||||
title: Mastodon.List['title']
|
title: Mastodon.List['title']
|
||||||
}
|
}
|
||||||
'Tab-Me-Settings': undefined
|
'Tab-Me-Settings': undefined
|
||||||
'Tab-Me-Settings-Notification': undefined
|
'Tab-Me-Settings-Push': undefined
|
||||||
'Tab-Me-Switch': undefined
|
'Tab-Me-Switch': undefined
|
||||||
} & TabSharedStackParamList
|
} & TabSharedStackParamList
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,16 @@ import { enableScreens } from 'react-native-screens'
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
import push from './startup/push'
|
||||||
|
|
||||||
if (Platform.OS === 'android') {
|
Platform.select({
|
||||||
LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||||
}
|
})
|
||||||
|
|
||||||
dev()
|
dev()
|
||||||
sentry()
|
sentry()
|
||||||
audio()
|
audio()
|
||||||
|
push()
|
||||||
onlineStatus()
|
onlineStatus()
|
||||||
|
|
||||||
log('log', 'react-query', 'initializing')
|
log('log', 'react-query', 'initializing')
|
||||||
|
|
|
@ -15,16 +15,47 @@ import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { themes } from '@utils/styles/themes'
|
import { themes } from '@utils/styles/themes'
|
||||||
import * as Analytics from 'expo-firebase-analytics'
|
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 React, { createRef, useCallback, useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import * as Sentry from 'sentry-expo'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<Nav.RootStackParamList>()
|
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 {
|
export interface Props {
|
||||||
localCorrupt?: string
|
localCorrupt?: string
|
||||||
}
|
}
|
||||||
|
@ -57,15 +88,15 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
// }, [isConnected, firstRender])
|
// }, [isConnected, firstRender])
|
||||||
|
|
||||||
// Prevent screenshot alert
|
// Prevent screenshot alert
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// const screenshotListener = addScreenshotListener(() =>
|
const screenshotListener = addScreenshotListener(() =>
|
||||||
// Alert.alert(t('screenshot.title'), t('screenshot.message'), [
|
Alert.alert(t('screenshot.title'), t('screenshot.message'), [
|
||||||
// { text: t('screenshot.button'), style: 'destructive' }
|
{ text: t('screenshot.button'), style: 'destructive' }
|
||||||
// ])
|
])
|
||||||
// )
|
)
|
||||||
// Platform.OS === 'ios' && screenshotListener
|
Platform.select({ ios: screenshotListener })
|
||||||
// return () => screenshotListener.remove()
|
return () => screenshotListener.remove()
|
||||||
// }, [])
|
}, [])
|
||||||
|
|
||||||
// On launch display login credentials corrupt information
|
// On launch display login credentials corrupt information
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -127,6 +158,10 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
|
|
||||||
if (previousRouteName !== currentRouteName) {
|
if (previousRouteName !== currentRouteName) {
|
||||||
Analytics.setCurrentScreen(currentRouteName)
|
Analytics.setCurrentScreen(currentRouteName)
|
||||||
|
Sentry.Native.setContext('page', {
|
||||||
|
previous: previousRouteName,
|
||||||
|
current: currentRouteName
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
routeNameRef.current = currentRouteName
|
routeNameRef.current = currentRouteName
|
||||||
|
@ -140,6 +175,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
theme={themes[mode]}
|
theme={themes[mode]}
|
||||||
onReady={navigationContainerOnReady}
|
onReady={navigationContainerOnReady}
|
||||||
onStateChange={navigationContainerOnStateChange}
|
onStateChange={navigationContainerOnStateChange}
|
||||||
|
linking={linking}
|
||||||
>
|
>
|
||||||
<Stack.Navigator initialRouteName='Screen-Tabs'>
|
<Stack.Navigator initialRouteName='Screen-Tabs'>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
@ -185,9 +221,9 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
|
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.select({
|
||||||
<Toast ref={Toast.setRef} config={toastConfig} />
|
ios: <Toast ref={Toast.setRef} config={toastConfig} />
|
||||||
) : null}
|
})}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,13 +57,9 @@ const MenuRow: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<TapGestureHandler
|
<TapGestureHandler
|
||||||
onHandlerStateChange={({ nativeEvent }) => {
|
onHandlerStateChange={({ nativeEvent }) =>
|
||||||
if (nativeEvent.state === State.ACTIVE) {
|
nativeEvent.state === State.ACTIVE && !loading && onPress && onPress()
|
||||||
if (!loading) {
|
}
|
||||||
onPress && onPress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<View style={styles.core}>
|
<View style={styles.core}>
|
||||||
<View style={styles.front}>
|
<View style={styles.front}>
|
||||||
|
@ -82,11 +78,6 @@ const MenuRow: React.FC<Props> = ({
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description ? (
|
|
||||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
@ -94,21 +85,18 @@ const MenuRow: React.FC<Props> = ({
|
||||||
<View style={styles.back}>
|
<View style={styles.back}>
|
||||||
{content ? (
|
{content ? (
|
||||||
typeof content === 'string' ? (
|
typeof content === 'string' ? (
|
||||||
<>
|
<Text
|
||||||
<Text
|
style={[
|
||||||
style={[
|
styles.content,
|
||||||
styles.content,
|
{
|
||||||
{
|
color: theme.secondary,
|
||||||
color: theme.secondary,
|
opacity: !iconBack && loading ? 0 : 1
|
||||||
opacity: !iconBack && loading ? 0 : 1
|
}
|
||||||
}
|
]}
|
||||||
]}
|
numberOfLines={1}
|
||||||
numberOfLines={1}
|
>
|
||||||
>
|
{content}
|
||||||
{content}
|
</Text>
|
||||||
</Text>
|
|
||||||
{loading && !iconBack && loadingSpinkit}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
)
|
)
|
||||||
|
@ -119,23 +107,27 @@ const MenuRow: React.FC<Props> = ({
|
||||||
onValueChange={switchOnValueChange}
|
onValueChange={switchOnValueChange}
|
||||||
disabled={switchDisabled}
|
disabled={switchDisabled}
|
||||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||||
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{iconBack ? (
|
{iconBack ? (
|
||||||
<>
|
<Icon
|
||||||
<Icon
|
name={iconBack}
|
||||||
name={iconBack}
|
size={StyleConstants.Font.Size.L}
|
||||||
size={StyleConstants.Font.Size.L}
|
color={theme[iconBackColor]}
|
||||||
color={theme[iconBackColor]}
|
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
/>
|
||||||
/>
|
|
||||||
{loading && loadingSpinkit}
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{loading && loadingSpinkit}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</TapGestureHandler>
|
</TapGestureHandler>
|
||||||
|
{description ? (
|
||||||
|
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -147,9 +139,7 @@ const styles = StyleSheet.create({
|
||||||
core: {
|
core: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
|
||||||
},
|
},
|
||||||
front: {
|
front: {
|
||||||
flex: 2,
|
flex: 2,
|
||||||
|
@ -174,7 +164,8 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
...StyleConstants.FontStyle.S,
|
...StyleConstants.FontStyle.S,
|
||||||
marginTop: StyleConstants.Spacing.XS
|
marginTop: StyleConstants.Spacing.XS,
|
||||||
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
...StyleConstants.FontStyle.M
|
...StyleConstants.FontStyle.M
|
||||||
|
|
|
@ -1,93 +1,61 @@
|
||||||
import ComponentSeparator from '@components/Separator'
|
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 { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import React, { RefObject, useCallback, useRef } from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import {
|
import {
|
||||||
|
FlatList,
|
||||||
FlatListProps,
|
FlatListProps,
|
||||||
Platform,
|
Platform,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
StyleSheet
|
StyleSheet
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedScrollHandler,
|
||||||
useSharedValue,
|
useSharedValue
|
||||||
withTiming
|
|
||||||
} from 'react-native-reanimated'
|
} 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 TimelineEmpty from './Timeline/Empty'
|
||||||
import TimelineEnd from './Timeline/End'
|
import TimelineFooter from './Timeline/Footer'
|
||||||
import TimelineNotifications from './Timeline/Notifications'
|
import TimelineRefresh, {
|
||||||
import TimelineRefresh from './Timeline/Refresh'
|
SEPARATION_Y_1,
|
||||||
|
SEPARATION_Y_2
|
||||||
|
} from './Timeline/Refresh'
|
||||||
|
|
||||||
|
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
page: App.Pages
|
flRef?: RefObject<FlatList<any>>
|
||||||
hashtag?: Mastodon.Tag['name']
|
queryKey: QueryKeyTimeline
|
||||||
list?: Mastodon.List['id']
|
|
||||||
toot?: Mastodon.Status['id']
|
|
||||||
rootQueryKey?: QueryKeyTimeline
|
|
||||||
account?: Mastodon.Account['id']
|
|
||||||
disableRefresh?: boolean
|
disableRefresh?: boolean
|
||||||
disableInfinity?: boolean
|
disableInfinity?: boolean
|
||||||
customProps?: Partial<FlatListProps<any>>
|
customProps: Partial<FlatListProps<any>> &
|
||||||
|
Pick<FlatListProps<any>, 'renderItem'>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Timeline: React.FC<Props> = ({
|
const Timeline: React.FC<Props> = ({
|
||||||
page,
|
flRef: customFLRef,
|
||||||
hashtag,
|
queryKey,
|
||||||
list,
|
|
||||||
toot,
|
|
||||||
rootQueryKey,
|
|
||||||
account,
|
|
||||||
disableRefresh = false,
|
disableRefresh = false,
|
||||||
disableInfinity = false,
|
disableInfinity = false,
|
||||||
customProps
|
customProps
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
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 {
|
const {
|
||||||
status,
|
|
||||||
data,
|
data,
|
||||||
refetch,
|
refetch,
|
||||||
isSuccess,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasPreviousPage,
|
|
||||||
fetchPreviousPage,
|
|
||||||
isFetchingPreviousPage,
|
|
||||||
hasNextPage,
|
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useTimelineQuery({
|
} = useTimelineQuery({
|
||||||
...queryKeyParams,
|
...queryKey[1],
|
||||||
options: {
|
options: {
|
||||||
getPreviousPageParam: firstPage =>
|
notifyOnChangeProps: Platform.select({
|
||||||
firstPage?.links?.prev && {
|
ios: ['data', 'isFetching'],
|
||||||
min_id: firstPage.links.prev,
|
android: ['data', 'isFetching', 'isLoading']
|
||||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
}),
|
||||||
limit: '5'
|
|
||||||
},
|
|
||||||
|
|
||||||
getNextPageParam: lastPage =>
|
getNextPageParam: lastPage =>
|
||||||
lastPage?.links?.next && { max_id: lastPage.links.next }
|
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]) : []
|
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(
|
const ItemSeparatorComponent = useCallback(
|
||||||
({ leadingItem }) => (
|
({ leadingItem }) =>
|
||||||
<ComponentSeparator
|
queryKey[1].page === 'Toot' && queryKey[1].toot === leadingItem.id ? (
|
||||||
{...(toot === leadingItem.id
|
<ComponentSeparator extraMarginLeft={0} />
|
||||||
? { extraMarginLeft: 0 }
|
) : (
|
||||||
: {
|
<ComponentSeparator
|
||||||
extraMarginLeft:
|
extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
|
||||||
StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
/>
|
||||||
})}
|
),
|
||||||
/>
|
|
||||||
),
|
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const flItemEmptyComponent = useMemo(
|
|
||||||
() => <TimelineEmpty status={status} refetch={refetch} />,
|
|
||||||
[status]
|
|
||||||
)
|
|
||||||
const onEndReached = useCallback(
|
const onEndReached = useCallback(
|
||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
||||||
[isFetchingNextPage]
|
[isFetchingNextPage]
|
||||||
)
|
)
|
||||||
const ListFooterComponent = useMemo(
|
|
||||||
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
|
||||||
[hasNextPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
useScrollToTop(flRef)
|
const flRef = useRef<FlatList>(null)
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const [isFetchingLatest, setIsFetchingLatest] = useState(0)
|
const fetchingType = useSharedValue<0 | 1 | 2>(0)
|
||||||
useEffect(() => {
|
|
||||||
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
|
const onScroll = useAnimatedScrollHandler(
|
||||||
if (isFetchingLatest !== 0) {
|
{
|
||||||
if (!isFetchingPreviousPage) {
|
onScroll: ({ contentOffset: { y } }) => {
|
||||||
fetchPreviousPage()
|
scrollY.value = y
|
||||||
setIsFetchingLatest(isFetchingLatest + 1)
|
},
|
||||||
} else {
|
onEndDrag: ({ contentOffset: { y } }) => {
|
||||||
if (isFetchingLatest === 8) {
|
if (!disableRefresh && !isFetching) {
|
||||||
setIsFetchingLatest(0)
|
if (y <= SEPARATION_Y_2) {
|
||||||
if (data?.pages[0].body.length === 0) {
|
fetchingType.value = 2
|
||||||
queryClient.setQueryData<InfiniteData<any> | undefined>(
|
} else if (y <= SEPARATION_Y_1) {
|
||||||
queryKey,
|
fetchingType.value = 1
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, [isFetchingPreviousPage, isFetchingLatest, data?.pages[0].body])
|
[isFetching]
|
||||||
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} />,
|
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const androidRefreshControl = useMemo(
|
const androidRefreshControl = Platform.select({
|
||||||
() =>
|
android: {
|
||||||
Platform.OS === 'android' && {
|
refreshControl: (
|
||||||
refreshControl: (
|
<RefreshControl
|
||||||
<RefreshControl
|
enabled
|
||||||
enabled
|
colors={[theme.primary]}
|
||||||
colors={[theme.primary]}
|
progressBackgroundColor={theme.background}
|
||||||
progressBackgroundColor={theme.background}
|
refreshing={isFetching || isLoading}
|
||||||
refreshing={isFetching || isLoading}
|
onRefresh={() => refetch()}
|
||||||
onRefresh={() => refetch()}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
},
|
})
|
||||||
[isFetching, isLoading]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
useScrollToTop(flRef)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimelineRefresh
|
<TimelineRefresh
|
||||||
|
queryKey={queryKey}
|
||||||
scrollY={scrollY}
|
scrollY={scrollY}
|
||||||
isLoading={isLoading}
|
fetchingType={fetchingType}
|
||||||
isFetching={isFetching}
|
disableRefresh={disableRefresh}
|
||||||
disable={disableRefresh}
|
|
||||||
/>
|
/>
|
||||||
<FlatList
|
<AnimatedFlatList
|
||||||
|
// @ts-ignore
|
||||||
|
ref={customFLRef || flRef}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
onResponderRelease={onResponderRelease}
|
|
||||||
ref={flRef}
|
|
||||||
windowSize={8}
|
windowSize={8}
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
initialNumToRender={6}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={3}
|
maxToRenderPerBatch={3}
|
||||||
style={styles.flatList}
|
style={styles.flatList}
|
||||||
renderItem={renderItem}
|
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
keyExtractor={keyExtractor}
|
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ListHeaderComponent={ListHeaderComponent}
|
ListFooterComponent={
|
||||||
ListFooterComponent={ListFooterComponent}
|
<TimelineFooter
|
||||||
ListEmptyComponent={flItemEmptyComponent}
|
queryKey={queryKey}
|
||||||
|
disableInfinity={disableInfinity}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
|
||||||
ItemSeparatorComponent={ItemSeparatorComponent}
|
ItemSeparatorComponent={ItemSeparatorComponent}
|
||||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0
|
minIndexForVisible: 0
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -41,10 +41,7 @@ const TimelineDefault: React.FC<Props> = ({
|
||||||
pinned
|
pinned
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const instanceAccount = useSelector(
|
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => true)
|
||||||
getInstanceAccount,
|
|
||||||
(prev, next) => prev?.id === next?.id
|
|
||||||
)
|
|
||||||
const navigation = useNavigation<
|
const navigation = useNavigation<
|
||||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||||
>()
|
>()
|
||||||
|
|
|
@ -1,72 +1,79 @@
|
||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import { QueryStatus } from 'react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
status: QueryStatus
|
queryKey: QueryKeyTimeline
|
||||||
refetch: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
const TimelineEmpty = React.memo(
|
||||||
const { mode, theme } = useTheme()
|
({ queryKey }: Props) => {
|
||||||
const { t, i18n } = useTranslation('componentTimeline')
|
const { status, refetch } = useTimelineQuery({
|
||||||
|
...queryKey[1],
|
||||||
|
options: { notifyOnChangeProps: ['status'] }
|
||||||
|
})
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const { mode, theme } = useTheme()
|
||||||
switch (status) {
|
const { t, i18n } = useTranslation('componentTimeline')
|
||||||
case 'loading':
|
|
||||||
return (
|
const children = useMemo(() => {
|
||||||
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
switch (status) {
|
||||||
)
|
case 'loading':
|
||||||
case 'error':
|
return (
|
||||||
return (
|
<Circle size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||||
<>
|
)
|
||||||
<Icon
|
case 'error':
|
||||||
name='Frown'
|
return (
|
||||||
size={StyleConstants.Font.Size.L}
|
<>
|
||||||
color={theme.primary}
|
<Icon
|
||||||
/>
|
name='Frown'
|
||||||
<Text style={[styles.error, { color: theme.primary }]}>
|
size={StyleConstants.Font.Size.L}
|
||||||
{t('empty.error.message')}
|
color={theme.primary}
|
||||||
</Text>
|
/>
|
||||||
<Button
|
<Text style={[styles.error, { color: theme.primary }]}>
|
||||||
type='text'
|
{t('empty.error.message')}
|
||||||
content={t('empty.error.button')}
|
</Text>
|
||||||
onPress={() => {
|
<Button
|
||||||
analytics('timeline_error_press_refetch')
|
type='text'
|
||||||
refetch()
|
content={t('empty.error.button')}
|
||||||
}}
|
onPress={() => {
|
||||||
/>
|
analytics('timeline_error_press_refetch')
|
||||||
</>
|
refetch()
|
||||||
)
|
}}
|
||||||
case 'success':
|
/>
|
||||||
return (
|
</>
|
||||||
<>
|
)
|
||||||
<Icon
|
case 'success':
|
||||||
name='Smartphone'
|
return (
|
||||||
size={StyleConstants.Font.Size.L}
|
<>
|
||||||
color={theme.primary}
|
<Icon
|
||||||
/>
|
name='Smartphone'
|
||||||
<Text style={[styles.error, { color: theme.primary }]}>
|
size={StyleConstants.Font.Size.L}
|
||||||
{t('empty.success.message')}
|
color={theme.primary}
|
||||||
</Text>
|
/>
|
||||||
</>
|
<Text style={[styles.error, { color: theme.primary }]}>
|
||||||
)
|
{t('empty.success.message')}
|
||||||
}
|
</Text>
|
||||||
}, [mode, i18n.language, status])
|
</>
|
||||||
return (
|
)
|
||||||
<View
|
}
|
||||||
style={[styles.base, { backgroundColor: theme.background }]}
|
}, [mode, i18n.language, status])
|
||||||
children={children}
|
return (
|
||||||
/>
|
<View
|
||||||
)
|
style={[styles.base, { backgroundColor: theme.background }]}
|
||||||
}
|
children={children}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
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
|
|
|
@ -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 haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import {
|
||||||
|
QueryKeyTimeline,
|
||||||
|
TimelineData,
|
||||||
|
useTimelineQuery
|
||||||
|
} from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
|
@ -15,104 +20,201 @@ import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming
|
withTiming
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
import { InfiniteData, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
queryKey: QueryKeyTimeline
|
||||||
scrollY: Animated.SharedValue<number>
|
scrollY: Animated.SharedValue<number>
|
||||||
isLoading: boolean
|
fetchingType: Animated.SharedValue<0 | 1 | 2>
|
||||||
isFetching: boolean
|
disableRefresh?: boolean
|
||||||
disable?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
|
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(
|
const TimelineRefresh: React.FC<Props> = ({
|
||||||
({ scrollY, isLoading, isFetching, disable = false }: Props) => {
|
queryKey,
|
||||||
if (disable || isLoading) {
|
scrollY,
|
||||||
return null
|
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 { t } = useTranslation('componentTimeline')
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const separation01 = -(
|
const queryClient = useQueryClient()
|
||||||
CONTAINER_HEIGHT / 2 +
|
const clearFirstPage = useCallback(() => {
|
||||||
StyleConstants.Font.Size.S / 2
|
if (data?.pages[0].body.length === 0) {
|
||||||
)
|
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
|
||||||
const separation02 = -(
|
queryKey,
|
||||||
CONTAINER_HEIGHT * 1.5 +
|
data => {
|
||||||
StyleConstants.Font.Size.S / 2
|
if (data?.pages[0].body.length === 0) {
|
||||||
)
|
return {
|
||||||
const [textRight, setTextRight] = useState(0)
|
pages: data.pages.slice(1),
|
||||||
const arrowY = useAnimatedStyle(() => ({
|
pageParams: data.pageParams.slice(1)
|
||||||
transform: [
|
}
|
||||||
{
|
} else {
|
||||||
translateY: interpolate(
|
return data
|
||||||
scrollY.value,
|
}
|
||||||
[0, separation01],
|
|
||||||
[
|
|
||||||
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
|
|
||||||
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
|
|
||||||
],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
}))
|
}
|
||||||
const arrowTop = useAnimatedStyle(() => ({
|
}, [data?.pages.length && data?.pages[0].body.length])
|
||||||
marginTop:
|
|
||||||
scrollY.value < separation02
|
|
||||||
? withTiming(CONTAINER_HEIGHT)
|
|
||||||
: withTiming(0)
|
|
||||||
}))
|
|
||||||
|
|
||||||
const arrowStage = useSharedValue(0)
|
const [textRight, setTextRight] = useState(0)
|
||||||
const onLayout = useCallback(
|
const arrowY = useAnimatedStyle(() => ({
|
||||||
({ nativeEvent }) => {
|
transform: [
|
||||||
if (nativeEvent.layout.x + nativeEvent.layout.width > textRight) {
|
{
|
||||||
setTextRight(nativeEvent.layout.x + nativeEvent.layout.width)
|
translateY: interpolate(
|
||||||
}
|
scrollY.value,
|
||||||
},
|
[0, SEPARATION_Y_1],
|
||||||
[textRight]
|
[
|
||||||
)
|
-CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.M / 2,
|
||||||
useAnimatedReaction(
|
CONTAINER_HEIGHT / 2 - StyleConstants.Font.Size.S / 2
|
||||||
() => {
|
],
|
||||||
if (isFetching) {
|
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
|
return false
|
||||||
}
|
case 1:
|
||||||
switch (arrowStage.value) {
|
if (scrollY.value < SEPARATION_Y_2) {
|
||||||
case 0:
|
arrowStage.value = 2
|
||||||
if (scrollY.value < separation01) {
|
return true
|
||||||
arrowStage.value = 1
|
}
|
||||||
return true
|
if (scrollY.value > SEPARATION_Y_1) {
|
||||||
}
|
arrowStage.value = 0
|
||||||
return false
|
return false
|
||||||
case 1:
|
}
|
||||||
if (scrollY.value < separation02) {
|
return false
|
||||||
arrowStage.value = 2
|
case 2:
|
||||||
return true
|
if (scrollY.value > SEPARATION_Y_2) {
|
||||||
}
|
arrowStage.value = 1
|
||||||
if (scrollY.value > separation01) {
|
|
||||||
arrowStage.value = 0
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
case 2:
|
}
|
||||||
if (scrollY.value > separation02) {
|
return false
|
||||||
arrowStage.value = 1
|
}
|
||||||
return false
|
},
|
||||||
}
|
data => {
|
||||||
return false
|
if (data) {
|
||||||
}
|
runOnJS(haptics)('Light')
|
||||||
},
|
}
|
||||||
data => {
|
},
|
||||||
if (data) {
|
[isFetching]
|
||||||
runOnJS(haptics)('Light')
|
)
|
||||||
}
|
const wrapper = () => {
|
||||||
},
|
fetchingLatestIndex.current = 1
|
||||||
[isFetching]
|
}
|
||||||
)
|
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}>
|
<View style={styles.base}>
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<View style={styles.container2}>
|
<View style={styles.container2}>
|
||||||
|
@ -154,11 +256,9 @@ const TimelineRefresh = React.memo(
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
</Animated.View>
|
||||||
},
|
)
|
||||||
(prev, next) =>
|
}
|
||||||
prev.isLoading === next.isLoading && prev.isFetching === next.isFetching
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
|
|
|
@ -15,140 +15,139 @@ export interface Props {
|
||||||
notification?: boolean
|
notification?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineActioned: React.FC<Props> = ({
|
const TimelineActioned = React.memo(
|
||||||
account,
|
({ account, action, notification = false }: Props) => {
|
||||||
action,
|
const { t } = useTranslation('componentTimeline')
|
||||||
notification = false
|
const { theme } = useTheme()
|
||||||
}) => {
|
const navigation = useNavigation<
|
||||||
const { t } = useTranslation('componentTimeline')
|
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||||
const { theme } = useTheme()
|
>()
|
||||||
const navigation = useNavigation<
|
const name = account.display_name || account.username
|
||||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
const iconColor = theme.primary
|
||||||
>()
|
|
||||||
const name = account.display_name || account.username
|
|
||||||
const iconColor = theme.primary
|
|
||||||
|
|
||||||
const content = (content: string) => (
|
const content = (content: string) => (
|
||||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
analytics('timeline_shared_actioned_press', { action })
|
analytics('timeline_shared_actioned_press', { action })
|
||||||
navigation.push('Tab-Shared-Account', { account })
|
navigation.push('Tab-Shared-Account', { account })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = useMemo(() => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'pinned':
|
case 'pinned':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Anchor'
|
name='Anchor'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
{content(t('shared.actioned.pinned'))}
|
{content(t('shared.actioned.pinned'))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Heart'
|
name='Heart'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
{content(t('shared.actioned.favourite', { name }))}
|
{content(t('shared.actioned.favourite', { name }))}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'follow':
|
case 'follow':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='UserPlus'
|
name='UserPlus'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
{content(t('shared.actioned.follow', { name }))}
|
{content(t('shared.actioned.follow', { name }))}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'follow_request':
|
case 'follow_request':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='UserPlus'
|
name='UserPlus'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
{content(t('shared.actioned.follow_request', { name }))}
|
{content(t('shared.actioned.follow_request', { name }))}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='BarChart2'
|
name='BarChart2'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
{content(t('shared.actioned.poll'))}
|
{content(t('shared.actioned.poll'))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Repeat'
|
name='Repeat'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
{content(
|
{content(
|
||||||
notification
|
notification
|
||||||
? t('shared.actioned.reblog.notification', { name })
|
? t('shared.actioned.reblog.notification', { name })
|
||||||
: t('shared.actioned.reblog.default', { name })
|
: t('shared.actioned.reblog.default', { name })
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case 'status':
|
case 'status':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name='Activity'
|
name='Activity'
|
||||||
size={StyleConstants.Font.Size.S}
|
size={StyleConstants.Font.Size.S}
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={onPress}>
|
<Pressable onPress={onPress}>
|
||||||
{content(t('shared.actioned.status', { name }))}
|
{content(t('shared.actioned.status', { name }))}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <View style={styles.actioned} children={children} />
|
return <View style={styles.actioned} children={children} />
|
||||||
}
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
actioned: {
|
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)
|
theParams.payload.currentValue === true)
|
||||||
) {
|
) {
|
||||||
queryClient.invalidateQueries(queryKey)
|
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
|
// When reblogged, update cache of following page
|
||||||
const tempQueryKey: QueryKeyTimeline = [
|
const tempQueryKey: QueryKeyTimeline = [
|
||||||
'Timeline',
|
'Timeline',
|
||||||
|
|
|
@ -16,129 +16,132 @@ export interface Props {
|
||||||
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
status: Pick<Mastodon.Status, 'media_attachments' | 'sensitive'>
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
const TimelineAttachment = React.memo(
|
||||||
const { t } = useTranslation('componentTimeline')
|
({ status }: Props) => {
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
||||||
const onPressBlurView = useCallback(() => {
|
const onPressBlurView = useCallback(() => {
|
||||||
analytics('timeline_shared_attachment_blurview_press_show')
|
analytics('timeline_shared_attachment_blurview_press_show')
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
setSensitiveShown(false)
|
setSensitiveShown(false)
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
}, [])
|
}, [])
|
||||||
const onPressShow = useCallback(() => {
|
const onPressShow = useCallback(() => {
|
||||||
analytics('timeline_shared_attachment_blurview_press_hide')
|
analytics('timeline_shared_attachment_blurview_press_hide')
|
||||||
setSensitiveShown(true)
|
setSensitiveShown(true)
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = []
|
let imageUrls: Nav.RootStackParamList['Screen-ImagesViewer']['imageUrls'] = []
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const navigateToImagesViewer = (imageIndex: number) =>
|
const navigateToImagesViewer = (imageIndex: number) =>
|
||||||
navigation.navigate('Screen-ImagesViewer', {
|
navigation.navigate('Screen-ImagesViewer', {
|
||||||
imageUrls,
|
imageUrls,
|
||||||
imageIndex
|
imageIndex
|
||||||
})
|
})
|
||||||
const attachments = useMemo(
|
const attachments = useMemo(
|
||||||
() =>
|
() =>
|
||||||
status.media_attachments.map((attachment, index) => {
|
status.media_attachments.map((attachment, index) => {
|
||||||
switch (attachment.type) {
|
switch (attachment.type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
imageUrls.push({
|
imageUrls.push({
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
preview_url: attachment.preview_url,
|
preview_url: attachment.preview_url,
|
||||||
remote_url: attachment.remote_url,
|
remote_url: attachment.remote_url,
|
||||||
width: attachment.meta?.original?.width,
|
width: attachment.meta?.original?.width,
|
||||||
height: attachment.meta?.original?.height,
|
height: attachment.meta?.original?.height,
|
||||||
imageIndex: index
|
imageIndex: index
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<AttachmentImage
|
<AttachmentImage
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
image={attachment}
|
image={attachment}
|
||||||
navigateToImagesViewer={navigateToImagesViewer}
|
navigateToImagesViewer={navigateToImagesViewer}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'video':
|
case 'video':
|
||||||
return (
|
return (
|
||||||
<AttachmentVideo
|
<AttachmentVideo
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
video={attachment}
|
video={attachment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'gifv':
|
case 'gifv':
|
||||||
return (
|
return (
|
||||||
<AttachmentVideo
|
<AttachmentVideo
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
video={attachment}
|
video={attachment}
|
||||||
gifv
|
gifv
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'audio':
|
case 'audio':
|
||||||
return (
|
return (
|
||||||
<AttachmentAudio
|
<AttachmentAudio
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
audio={attachment}
|
audio={attachment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<AttachmentUnsupported
|
<AttachmentUnsupported
|
||||||
key={index}
|
key={index}
|
||||||
total={status.media_attachments.length}
|
total={status.media_attachments.length}
|
||||||
index={index}
|
index={index}
|
||||||
sensitiveShown={sensitiveShown}
|
sensitiveShown={sensitiveShown}
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[sensitiveShown]
|
[sensitiveShown]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.container} children={attachments} />
|
<View style={styles.container} children={attachments} />
|
||||||
|
|
||||||
{status.sensitive &&
|
{status.sensitive &&
|
||||||
(sensitiveShown ? (
|
(sensitiveShown ? (
|
||||||
<Pressable style={styles.sensitiveBlur}>
|
<Pressable style={styles.sensitiveBlur}>
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
content={t('shared.attachment.sensitive.button')}
|
||||||
|
overlay
|
||||||
|
onPress={onPressBlurView}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='icon'
|
||||||
content={t('shared.attachment.sensitive.button')}
|
content='EyeOff'
|
||||||
|
round
|
||||||
overlay
|
overlay
|
||||||
onPress={onPressBlurView}
|
onPress={onPressShow}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: StyleConstants.Spacing.S * 2,
|
||||||
|
left: StyleConstants.Spacing.S
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
))}
|
||||||
) : (
|
</View>
|
||||||
<Button
|
)
|
||||||
type='icon'
|
},
|
||||||
content='EyeOff'
|
() => true
|
||||||
round
|
)
|
||||||
overlay
|
|
||||||
onPress={onPressShow}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: StyleConstants.Spacing.S * 2,
|
|
||||||
left: StyleConstants.Spacing.S
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
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
|
navigateToImagesViewer: (imageIndex: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentImage: React.FC<Props> = ({
|
const AttachmentImage = React.memo(
|
||||||
total,
|
({ total, index, sensitiveShown, image, navigateToImagesViewer }: Props) => {
|
||||||
index,
|
const onPress = useCallback(() => {
|
||||||
sensitiveShown,
|
analytics('timeline_shared_attachment_image_press', { id: image.id })
|
||||||
image,
|
navigateToImagesViewer(index)
|
||||||
navigateToImagesViewer
|
}, [])
|
||||||
}) => {
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
analytics('timeline_shared_attachment_image_press', { id: image.id })
|
|
||||||
navigateToImagesViewer(index)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
hidden={sensitiveShown}
|
hidden={sensitiveShown}
|
||||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||||
blurhash={image.blurhash}
|
blurhash={image.blurhash}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[
|
style={[
|
||||||
styles.base,
|
styles.base,
|
||||||
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
{ aspectRatio: attachmentAspectRatio({ total, index }) }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
(prev, next) => prev.sensitiveShown === next.sensitiveShown
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
|
@ -47,7 +44,4 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo(
|
export default AttachmentImage
|
||||||
AttachmentImage,
|
|
||||||
(prev, next) => prev.sensitiveShown === next.sensitiveShown
|
|
||||||
)
|
|
||||||
|
|
|
@ -11,33 +11,36 @@ export interface Props {
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineAvatar: React.FC<Props> = ({ queryKey, account }) => {
|
const TimelineAvatar = React.memo(
|
||||||
const navigation = useNavigation<
|
({ queryKey, account }: Props) => {
|
||||||
StackNavigationProp<Nav.TabLocalStackParamList>
|
const navigation = useNavigation<
|
||||||
>()
|
StackNavigationProp<Nav.TabLocalStackParamList>
|
||||||
// Need to fix go back root
|
>()
|
||||||
const onPress = useCallback(() => {
|
// Need to fix go back root
|
||||||
analytics('timeline_shared_avatar_press', {
|
const onPress = useCallback(() => {
|
||||||
page: queryKey && queryKey[1].page
|
analytics('timeline_shared_avatar_press', {
|
||||||
})
|
page: queryKey && queryKey[1].page
|
||||||
queryKey && navigation.push('Tab-Shared-Account', { account })
|
})
|
||||||
}, [])
|
queryKey && navigation.push('Tab-Shared-Account', { account })
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
uri={{ original: account.avatar_static }}
|
uri={{ original: account.avatar_static }}
|
||||||
dimension={{
|
dimension={{
|
||||||
width: StyleConstants.Avatar.M,
|
width: StyleConstants.Avatar.M,
|
||||||
height: StyleConstants.Avatar.M
|
height: StyleConstants.Avatar.M
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginRight: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
export default React.memo(TimelineAvatar, () => true)
|
export default TimelineAvatar
|
||||||
|
|
|
@ -10,53 +10,56 @@ export interface Props {
|
||||||
card: Mastodon.Card
|
card: Mastodon.Card
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineCard: React.FC<Props> = ({ card }) => {
|
const TimelineCard = React.memo(
|
||||||
const { theme } = useTheme()
|
({ card }: Props) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.card, { borderColor: theme.border }]}
|
style={[styles.card, { borderColor: theme.border }]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
analytics('timeline_shared_card_press')
|
analytics('timeline_shared_card_press')
|
||||||
await openLink(card.url)
|
await openLink(card.url)
|
||||||
}}
|
}}
|
||||||
testID='base'
|
testID='base'
|
||||||
>
|
>
|
||||||
{card.image && (
|
{card.image && (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
uri={{ original: card.image }}
|
uri={{ original: card.image }}
|
||||||
blurhash={card.blurhash}
|
blurhash={card.blurhash}
|
||||||
style={styles.left}
|
style={styles.left}
|
||||||
imageStyle={styles.image}
|
imageStyle={styles.image}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.right}>
|
<View style={styles.right}>
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
style={[styles.rightTitle, { color: theme.primary }]}
|
style={[styles.rightTitle, { color: theme.primary }]}
|
||||||
testID='title'
|
testID='title'
|
||||||
>
|
>
|
||||||
{card.title}
|
{card.title}
|
||||||
</Text>
|
</Text>
|
||||||
{card.description ? (
|
{card.description ? (
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[styles.rightDescription, { color: theme.primary }]}
|
||||||
|
testID='description'
|
||||||
|
>
|
||||||
|
{card.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[styles.rightDescription, { color: theme.primary }]}
|
style={[styles.rightLink, { color: theme.secondary }]}
|
||||||
testID='description'
|
|
||||||
>
|
>
|
||||||
{card.description}
|
{card.url}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
</View>
|
||||||
<Text
|
</Pressable>
|
||||||
numberOfLines={1}
|
)
|
||||||
style={[styles.rightLink, { color: theme.secondary }]}
|
},
|
||||||
>
|
() => true
|
||||||
{card.url}
|
)
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
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
|
disableDetails?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineContent: React.FC<Props> = ({
|
const TimelineContent = React.memo(
|
||||||
status,
|
({
|
||||||
numberOfLines,
|
status,
|
||||||
highlighted = false,
|
numberOfLines,
|
||||||
disableDetails = false
|
highlighted = false,
|
||||||
}) => {
|
disableDetails = false
|
||||||
const { t } = useTranslation('componentTimeline')
|
}: Props) => {
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{status.spoiler_text ? (
|
{status.spoiler_text ? (
|
||||||
<>
|
<>
|
||||||
<View style={{ marginBottom: StyleConstants.Font.Size.M }}>
|
<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
|
<ParseHTML
|
||||||
content={status.spoiler_text}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
emojis={status.emojis}
|
emojis={status.emojis}
|
||||||
mentions={status.mentions}
|
mentions={status.mentions}
|
||||||
tags={status.tags}
|
tags={status.tags}
|
||||||
numberOfLines={999}
|
numberOfLines={0}
|
||||||
|
expandHint={t('shared.content.expandHint')}
|
||||||
disableDetails={disableDetails}
|
disableDetails={disableDetails}
|
||||||
/>
|
/>
|
||||||
</View>
|
</>
|
||||||
|
) : (
|
||||||
<ParseHTML
|
<ParseHTML
|
||||||
content={status.content}
|
content={status.content}
|
||||||
size={highlighted ? 'L' : 'M'}
|
size={highlighted ? 'L' : 'M'}
|
||||||
emojis={status.emojis}
|
emojis={status.emojis}
|
||||||
mentions={status.mentions}
|
mentions={status.mentions}
|
||||||
tags={status.tags}
|
tags={status.tags}
|
||||||
numberOfLines={0}
|
numberOfLines={highlighted ? 999 : numberOfLines}
|
||||||
expandHint={t('shared.content.expandHint')}
|
|
||||||
disableDetails={disableDetails}
|
disableDetails={disableDetails}
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
) : (
|
</>
|
||||||
<ParseHTML
|
)
|
||||||
content={status.content}
|
},
|
||||||
size={highlighted ? 'L' : 'M'}
|
() => true
|
||||||
emojis={status.emojis}
|
)
|
||||||
mentions={status.mentions}
|
|
||||||
tags={status.tags}
|
|
||||||
numberOfLines={highlighted ? 999 : numberOfLines}
|
|
||||||
disableDetails={disableDetails}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(TimelineContent, () => true)
|
export default TimelineContent
|
||||||
|
|
|
@ -44,78 +44,81 @@ export interface Props {
|
||||||
conversation: Mastodon.Conversation
|
conversation: Mastodon.Conversation
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
const HeaderConversation = React.memo(
|
||||||
const { t } = useTranslation('componentTimeline')
|
({ queryKey, conversation }: Props) => {
|
||||||
|
const { t } = useTranslation('componentTimeline')
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const mutation = useTimelineMutation({
|
const mutation = useTimelineMutation({
|
||||||
queryClient,
|
queryClient,
|
||||||
onMutate: true,
|
onMutate: true,
|
||||||
onError: (err: any, _, oldData) => {
|
onError: (err: any, _, oldData) => {
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
toast({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:toastMessage.error.message', {
|
message: t('common:toastMessage.error.message', {
|
||||||
function: t(`shared.header.conversation.delete.function`)
|
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
|
|
||||||
}),
|
}),
|
||||||
autoHide: false
|
...(err.status &&
|
||||||
})
|
typeof err.status === 'number' &&
|
||||||
queryClient.setQueryData(queryKey, oldData)
|
err.data &&
|
||||||
}
|
err.data.error &&
|
||||||
})
|
typeof err.data.error === 'string' && {
|
||||||
|
description: err.data.error
|
||||||
const { theme } = useTheme()
|
}),
|
||||||
|
autoHide: false
|
||||||
const actionOnPress = useCallback(() => {
|
})
|
||||||
analytics('timeline_conversation_delete_press')
|
queryClient.setQueryData(queryKey, oldData)
|
||||||
mutation.mutate({
|
}
|
||||||
type: 'deleteItem',
|
|
||||||
source: 'conversations',
|
|
||||||
queryKey,
|
|
||||||
id: conversation.id
|
|
||||||
})
|
})
|
||||||
}, [])
|
|
||||||
|
|
||||||
const actionChildren = useMemo(
|
const { theme } = useTheme()
|
||||||
() => (
|
|
||||||
<Icon
|
|
||||||
name='Trash'
|
|
||||||
color={theme.secondary}
|
|
||||||
size={StyleConstants.Font.Size.L}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
const actionOnPress = useCallback(() => {
|
||||||
<View style={styles.base}>
|
analytics('timeline_conversation_delete_press')
|
||||||
<View style={styles.nameAndMeta}>
|
mutation.mutate({
|
||||||
<Names accounts={conversation.accounts} />
|
type: 'deleteItem',
|
||||||
<View style={styles.meta}>
|
source: 'conversations',
|
||||||
{conversation.last_status?.created_at ? (
|
queryKey,
|
||||||
<HeaderSharedCreated
|
id: conversation.id
|
||||||
created_at={conversation.last_status?.created_at}
|
})
|
||||||
/>
|
}, [])
|
||||||
) : null}
|
|
||||||
<HeaderSharedMuted muted={conversation.last_status?.muted} />
|
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>
|
||||||
</View>
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={actionOnPress}
|
onPress={actionOnPress}
|
||||||
children={actionChildren}
|
children={actionChildren}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
|
|
|
@ -17,50 +17,49 @@ export interface Props {
|
||||||
status: Mastodon.Status
|
status: Mastodon.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineHeaderDefault: React.FC<Props> = ({
|
const TimelineHeaderDefault = React.memo(
|
||||||
queryKey,
|
({ queryKey, rootQueryKey, status }: Props) => {
|
||||||
rootQueryKey,
|
const navigation = useNavigation()
|
||||||
status
|
const { theme } = useTheme()
|
||||||
}) => {
|
|
||||||
const navigation = useNavigation()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<View style={styles.accountAndMeta}>
|
<View style={styles.accountAndMeta}>
|
||||||
<HeaderSharedAccount account={status.account} />
|
<HeaderSharedAccount account={status.account} />
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<HeaderSharedCreated created_at={status.created_at} />
|
<HeaderSharedCreated created_at={status.created_at} />
|
||||||
<HeaderSharedVisibility visibility={status.visibility} />
|
<HeaderSharedVisibility visibility={status.visibility} />
|
||||||
<HeaderSharedMuted muted={status.muted} />
|
<HeaderSharedMuted muted={status.muted} />
|
||||||
<HeaderSharedApplication application={status.application} />
|
<HeaderSharedApplication application={status.application} />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
{queryKey ? (
|
{queryKey ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Screen-Actions', {
|
navigation.navigate('Screen-Actions', {
|
||||||
queryKey,
|
queryKey,
|
||||||
rootQueryKey,
|
rootQueryKey,
|
||||||
status,
|
status,
|
||||||
url: status.url || status.uri,
|
url: status.url || status.uri,
|
||||||
type: 'status'
|
type: 'status'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
children={
|
children={
|
||||||
<Icon
|
<Icon
|
||||||
name='MoreHorizontal'
|
name='MoreHorizontal'
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
size={StyleConstants.Font.Size.L}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
|
@ -87,7 +86,4 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default React.memo(
|
export default TimelineHeaderDefault
|
||||||
TimelineHeaderDefault,
|
|
||||||
(prev, next) => prev.status.muted !== next.status.muted
|
|
||||||
)
|
|
||||||
|
|
|
@ -20,98 +20,98 @@ export interface Props {
|
||||||
notification: Mastodon.Notification
|
notification: Mastodon.Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineHeaderNotification: React.FC<Props> = ({
|
const TimelineHeaderNotification = React.memo(
|
||||||
queryKey,
|
({ queryKey, notification }: Props) => {
|
||||||
notification
|
const navigation = useNavigation()
|
||||||
}) => {
|
const { theme } = useTheme()
|
||||||
const navigation = useNavigation()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
const actions = useMemo(() => {
|
const actions = useMemo(() => {
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
return <RelationshipOutgoing id={notification.account.id} />
|
return <RelationshipOutgoing id={notification.account.id} />
|
||||||
case 'follow_request':
|
case 'follow_request':
|
||||||
return <RelationshipIncoming id={notification.account.id} />
|
return <RelationshipIncoming id={notification.account.id} />
|
||||||
default:
|
default:
|
||||||
if (notification.status) {
|
if (notification.status) {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingBottom: StyleConstants.Spacing.S
|
paddingBottom: StyleConstants.Spacing.S
|
||||||
}}
|
}}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Screen-Actions', {
|
navigation.navigate('Screen-Actions', {
|
||||||
queryKey,
|
queryKey,
|
||||||
status: notification.status,
|
status: notification.status,
|
||||||
url: notification.status?.url || notification.status?.uri,
|
url: notification.status?.url || notification.status?.uri,
|
||||||
type: 'status'
|
type: 'status'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
children={
|
children={
|
||||||
<Icon
|
<Icon
|
||||||
name='MoreHorizontal'
|
name='MoreHorizontal'
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
size={StyleConstants.Font.Size.L}
|
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}
|
||||||
/>
|
/>
|
||||||
)
|
</View>
|
||||||
}
|
</View>
|
||||||
}
|
|
||||||
}, [notification.type])
|
|
||||||
|
|
||||||
return (
|
<View
|
||||||
<View style={styles.base}>
|
style={[
|
||||||
<View
|
styles.relationship,
|
||||||
style={{
|
|
||||||
flex:
|
|
||||||
notification.type === 'follow' ||
|
notification.type === 'follow' ||
|
||||||
notification.type === 'follow_request'
|
notification.type === 'follow_request'
|
||||||
? 1
|
? { flexShrink: 1 }
|
||||||
: 4
|
: { flex: 1 }
|
||||||
}}
|
]}
|
||||||
>
|
>
|
||||||
<HeaderSharedAccount
|
{actions}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
)
|
||||||
<View
|
},
|
||||||
style={[
|
() => true
|
||||||
styles.relationship,
|
)
|
||||||
notification.type === 'follow' ||
|
|
||||||
notification.type === 'follow_request'
|
|
||||||
? { flexShrink: 1 }
|
|
||||||
: { flex: 1 }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{actions}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
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,
|
meLists: require('./screens/meLists').default,
|
||||||
meListsList: require('./screens/meListsList').default,
|
meListsList: require('./screens/meListsList').default,
|
||||||
meSettings: require('./screens/meSettings').default,
|
meSettings: require('./screens/meSettings').default,
|
||||||
|
meSettingsPush: require('./screens/meSettingsPush').default,
|
||||||
meSwitch: require('./screens/meSwitch').default,
|
meSwitch: require('./screens/meSwitch').default,
|
||||||
|
|
||||||
sharedAccount: require('./screens/sharedAccount').default,
|
sharedAccount: require('./screens/sharedAccount').default,
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
export default {
|
export default {
|
||||||
heading: 'Settings',
|
heading: 'Settings',
|
||||||
content: {
|
content: {
|
||||||
|
push: {
|
||||||
|
heading: '$t(meSettingsPush:heading)',
|
||||||
|
content: {
|
||||||
|
enabled: 'Enabled',
|
||||||
|
disabled: 'Disabled'
|
||||||
|
}
|
||||||
|
},
|
||||||
language: {
|
language: {
|
||||||
heading: 'Language',
|
heading: 'Language',
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -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,
|
meLists: require('./screens/meLists').default,
|
||||||
meListsList: require('./screens/meListsList').default,
|
meListsList: require('./screens/meListsList').default,
|
||||||
meSettings: require('./screens/meSettings').default,
|
meSettings: require('./screens/meSettings').default,
|
||||||
meSettingsNotification: require('./screens/meSettingsNotification').default,
|
meSettingsPush: require('./screens/meSettingsPush').default,
|
||||||
meSwitch: require('./screens/meSwitch').default,
|
meSwitch: require('./screens/meSwitch').default,
|
||||||
|
|
||||||
sharedAccount: require('./screens/sharedAccount').default,
|
sharedAccount: require('./screens/sharedAccount').default,
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default {
|
||||||
message: '居然刷到底了,喝杯 <0 /> 吧'
|
message: '居然刷到底了,喝杯 <0 /> 吧'
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
fetchPreviousPage: '较新于此嘟嘟',
|
fetchPreviousPage: '较新于此的嘟嘟',
|
||||||
refetch: '最新的嘟嘟'
|
refetch: '最新的嘟嘟'
|
||||||
},
|
},
|
||||||
shared: {
|
shared: {
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
export default {
|
export default {
|
||||||
heading: '设置',
|
heading: '设置',
|
||||||
content: {
|
content: {
|
||||||
notification: {
|
push: {
|
||||||
heading: '$t(meSettingsNotification:heading)'
|
heading: '$t(meSettingsPush:heading)',
|
||||||
|
content: {
|
||||||
|
enabled: '已开启',
|
||||||
|
disabled: '已关闭'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
heading: '切换语言',
|
heading: '切换语言',
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
export default {
|
|
||||||
heading: '通知',
|
|
||||||
content: {
|
|
||||||
global: {
|
|
||||||
heading: '启用通知',
|
|
||||||
description: 'blahblahblah'
|
|
||||||
},
|
|
||||||
follow: {
|
|
||||||
heading: '新关注者'
|
|
||||||
},
|
|
||||||
favourite: {
|
|
||||||
heading: '嘟文被喜欢'
|
|
||||||
},
|
|
||||||
reblog: {
|
|
||||||
heading: '嘟文被转嘟'
|
|
||||||
},
|
|
||||||
mention: {
|
|
||||||
heading: '提及你'
|
|
||||||
},
|
|
||||||
poll: {
|
|
||||||
heading: '投票'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 analytics from '@components/analytics'
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { StackScreenProps } from '@react-navigation/stack'
|
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 { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||||
|
@ -38,7 +41,7 @@ const ScreenActionsRoot = React.memo(
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const localAccount = useSelector(
|
const localAccount = useSelector(
|
||||||
getLocalAccount,
|
getInstanceAccount,
|
||||||
(prev, next) => prev?.id === next?.id
|
(prev, next) => prev?.id === next?.id
|
||||||
)
|
)
|
||||||
let sameAccount = false
|
let sameAccount = false
|
||||||
|
@ -51,7 +54,7 @@ const ScreenActionsRoot = React.memo(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const localDomain = useSelector(getLocalUrl)
|
const localDomain = useSelector(getInstanceUrl)
|
||||||
let sameDomain = true
|
let sameDomain = true
|
||||||
let statusDomain: string
|
let statusDomain: string
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { forEach, groupBy, sortBy } from 'lodash'
|
import { forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
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 { Circle } from 'react-native-animated-spinkit'
|
||||||
import ComposeActions from './Root/Actions'
|
import ComposeActions from './Root/Actions'
|
||||||
import ComposePosting from './Posting'
|
import ComposePosting from './Posting'
|
||||||
|
@ -15,6 +15,21 @@ import ComposeRootSuggestion from './Root/Suggestion'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
import ComposeDrafts from './Root/Drafts'
|
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 ComposeRoot: React.FC = () => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
@ -52,6 +67,7 @@ const ComposeRoot: React.FC = () => {
|
||||||
type: 'emoji',
|
type: 'emoji',
|
||||||
payload: { ...composeState.emoji, emojis: sortedEmojis }
|
payload: { ...composeState.emoji, emojis: sortedEmojis }
|
||||||
})
|
})
|
||||||
|
prefetchEmojis(sortedEmojis)
|
||||||
}
|
}
|
||||||
}, [emojisData])
|
}, [emojisData])
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,15 @@ import analytics from '@components/analytics'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useCallback, useContext, useEffect, useMemo } from 'react'
|
||||||
import { Pressable, SectionList, StyleSheet, Text, View } from 'react-native'
|
import {
|
||||||
import FastImage from 'react-native-fast-image'
|
Image,
|
||||||
|
Pressable,
|
||||||
|
SectionList,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
import updateText from '../../updateText'
|
import updateText from '../../updateText'
|
||||||
|
|
||||||
|
@ -25,7 +31,12 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
|
||||||
haptics('Light')
|
haptics('Light')
|
||||||
}, [composeState])
|
}, [composeState])
|
||||||
const children = useMemo(
|
const children = useMemo(
|
||||||
() => <FastImage source={{ uri: emoji.url }} style={styles.emoji} />,
|
() => (
|
||||||
|
<Image
|
||||||
|
source={{ uri: emoji.url, cache: 'force-cache' }}
|
||||||
|
style={styles.emoji}
|
||||||
|
/>
|
||||||
|
),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import { HeaderCenter, HeaderRight } from '@components/Header'
|
import { HeaderCenter, HeaderRight } from '@components/Header'
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'
|
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'
|
||||||
import { ScreenTabsParamList } from '@screens/Tabs'
|
import { ScreenTabsParamList } from '@screens/Tabs'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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(
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import ScreenMeSettingsNotification from './Me/Notification'
|
import ScreenMeSettingsPush from './Me/Push'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
|
const Stack = createNativeStackNavigator<Nav.TabMeStackParamList>()
|
||||||
|
|
||||||
|
@ -116,13 +116,13 @@ const TabMe = React.memo(
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Tab-Me-Settings-Notification'
|
name='Tab-Me-Settings-Push'
|
||||||
component={ScreenMeSettingsNotification}
|
component={ScreenMeSettingsPush}
|
||||||
options={({ navigation }: any) => ({
|
options={({ navigation }: any) => ({
|
||||||
headerTitle: t('meSettingsNotification:heading'),
|
headerTitle: t('meSettingsPush:heading'),
|
||||||
...(Platform.OS === 'android' && {
|
...(Platform.OS === 'android' && {
|
||||||
headerCenter: () => (
|
headerCenter: () => (
|
||||||
<HeaderCenter content={t('meSettingsNotification:heading')} />
|
<HeaderCenter content={t('meSettingsPush:heading')} />
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ScreenMeBookmarks = React.memo(
|
const ScreenMeBookmarks = React.memo(
|
||||||
() => {
|
() => {
|
||||||
return <Timeline page='Bookmarks' />
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Bookmarks' }]
|
||||||
|
|
||||||
|
return <Timeline queryKey={queryKey} />
|
||||||
},
|
},
|
||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import Timeline from '@components/Timeline'
|
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(
|
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
|
() => true
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ScreenMeFavourites = React.memo(
|
const ScreenMeFavourites = React.memo(
|
||||||
() => {
|
() => {
|
||||||
return <Timeline page='Favourites' />
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Favourites' }]
|
||||||
|
|
||||||
|
return <Timeline queryKey={queryKey} />
|
||||||
},
|
},
|
||||||
() => true
|
() => 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
|
|
|
@ -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 Timeline from '@components/Timeline'
|
||||||
import { StackScreenProps } from '@react-navigation/stack'
|
import { StackScreenProps } from '@react-navigation/stack'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const ScreenMeListsList: React.FC<StackScreenProps<
|
const ScreenMeListsList: React.FC<StackScreenProps<
|
||||||
|
@ -10,7 +11,9 @@ const ScreenMeListsList: React.FC<StackScreenProps<
|
||||||
params: { list }
|
params: { list }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
return <Timeline page='List' list={list} />
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }]
|
||||||
|
|
||||||
|
return <Timeline queryKey={queryKey} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeListsList
|
export default ScreenMeListsList
|
||||||
|
|
|
@ -4,7 +4,10 @@ import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import i18n from '@root/i18n/i18n'
|
import i18n from '@root/i18n/i18n'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import {
|
||||||
|
getInstanceActive,
|
||||||
|
getInstancePush
|
||||||
|
} from '@utils/slices/instancesSlice'
|
||||||
import {
|
import {
|
||||||
changeBrowser,
|
changeBrowser,
|
||||||
changeLanguage,
|
changeLanguage,
|
||||||
|
@ -29,15 +32,24 @@ const SettingsApp: React.FC = () => {
|
||||||
const settingsLanguage = useSelector(getSettingsLanguage)
|
const settingsLanguage = useSelector(getSettingsLanguage)
|
||||||
const settingsTheme = useSelector(getSettingsTheme)
|
const settingsTheme = useSelector(getSettingsTheme)
|
||||||
const settingsBrowser = useSelector(getSettingsBrowser)
|
const settingsBrowser = useSelector(getSettingsBrowser)
|
||||||
|
const instancePush = useSelector(
|
||||||
|
getInstancePush,
|
||||||
|
(prev, next) => prev?.global.value === next?.global.value
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
{instanceActive !== -1 ? (
|
{instanceActive !== -1 ? (
|
||||||
<MenuRow
|
<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'
|
iconBack='ChevronRight'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Tab-Me-Settings-Notification')
|
navigation.navigate('Tab-Me-Settings-Push')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -4,16 +4,29 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { persistor } from '@root/store'
|
import { persistor } from '@root/store'
|
||||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Text } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
const SettingsDev: React.FC = () => {
|
const SettingsDev: React.FC = () => {
|
||||||
|
const { theme } = useTheme()
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
const instanceActive = useSelector(getInstanceActive)
|
||||||
const instances = useSelector(getInstances)
|
const instances = useSelector(getInstances, () => true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
|
<Text
|
||||||
|
selectable
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
...StyleConstants.FontStyle.S,
|
||||||
|
color: theme.primary
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{instances[instanceActive].token}
|
||||||
|
</Text>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={'Local active index'}
|
title={'Local active index'}
|
||||||
content={typeof instanceActive + ' - ' + instanceActive}
|
content={typeof instanceActive + ' - ' + instanceActive}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { HeaderCenter } from '@components/Header'
|
import { HeaderCenter } from '@components/Header'
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import TimelineNotifications from '@components/Timeline/Notifications'
|
||||||
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { updateInstanceNotification } from '@utils/slices/instancesSlice'
|
import { updateInstanceNotification } from '@utils/slices/instancesSlice'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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(
|
const children = useCallback(
|
||||||
({ navigation }) => (
|
({ navigation }) => (
|
||||||
<Timeline
|
<Timeline
|
||||||
page='Notifications'
|
queryKey={queryKey}
|
||||||
customProps={{
|
customProps={{
|
||||||
|
renderItem,
|
||||||
viewabilityConfigCallbackPairs: [
|
viewabilityConfigCallbackPairs: [
|
||||||
{
|
{
|
||||||
onViewableItemsChanged: ({
|
onViewableItemsChanged: ({
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import { HeaderRight } from '@components/Header'
|
import { HeaderRight } from '@components/Header'
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import SegmentedControl from '@react-native-community/segmented-control'
|
import SegmentedControl from '@react-native-community/segmented-control'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
import sharedScreens from '@screens/Tabs/Shared/sharedScreens'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
@ -24,9 +26,18 @@ const TabPublic = React.memo(
|
||||||
const instanceActive = useSelector(getInstanceActive)
|
const instanceActive = useSelector(getInstanceActive)
|
||||||
|
|
||||||
const [segment, setSegment] = useState(0)
|
const [segment, setSegment] = useState(0)
|
||||||
const pages: { title: string; page: App.Pages }[] = [
|
const pages: {
|
||||||
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
title: string
|
||||||
{ title: t('public:heading.segments.right'), page: 'Local' }
|
key: App.Pages
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
title: t('public:heading.segments.left'),
|
||||||
|
key: 'LocalPublic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('public:heading.segments.right'),
|
||||||
|
key: 'Local'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
const screenOptions = useMemo(
|
const screenOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -52,7 +63,7 @@ const TabPublic = React.memo(
|
||||||
<HeaderRight
|
<HeaderRight
|
||||||
content='Search'
|
content='Search'
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('search_tap', { page: pages[segment].page })
|
analytics('search_tap', { page: pages[segment].key })
|
||||||
navigation.navigate('Tab-Public', { screen: 'Tab-Shared-Search' })
|
navigation.navigate('Tab-Public', { screen: 'Tab-Shared-Search' })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -61,37 +72,42 @@ const TabPublic = React.memo(
|
||||||
[mode, segment, i18n.language]
|
[mode, segment, i18n.language]
|
||||||
)
|
)
|
||||||
|
|
||||||
const routes = pages.map(p => ({ key: p.page }))
|
const routes = pages.map(p => ({ key: p.key }))
|
||||||
const renderPager = useCallback(
|
const renderPager = useCallback(
|
||||||
props => <ViewPagerAdapter {...props} />,
|
props => <ViewPagerAdapter {...props} />,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const renderScene = useCallback(
|
const renderScene = useCallback(
|
||||||
({
|
({
|
||||||
route
|
route: { key: page }
|
||||||
}: {
|
}: {
|
||||||
route: {
|
route: {
|
||||||
key: App.Pages
|
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(
|
const children = useCallback(
|
||||||
() => (
|
() =>
|
||||||
<TabView
|
instanceActive !== -1 ? (
|
||||||
lazy
|
<TabView
|
||||||
swipeEnabled
|
lazy
|
||||||
renderPager={renderPager}
|
swipeEnabled
|
||||||
renderScene={renderScene}
|
renderPager={renderPager}
|
||||||
renderTabBar={() => null}
|
renderScene={renderScene}
|
||||||
onIndexChange={index => setSegment(index)}
|
renderTabBar={() => null}
|
||||||
navigationState={{ index: segment, routes }}
|
onIndexChange={index => setSegment(index)}
|
||||||
initialLayout={{ width: Dimensions.get('screen').width }}
|
navigationState={{ index: segment, routes }}
|
||||||
/>
|
initialLayout={{ width: Dimensions.get('screen').width }}
|
||||||
),
|
/>
|
||||||
[segment]
|
) : null,
|
||||||
|
[segment, instanceActive]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import { HeaderRight } from '@components/Header'
|
import { HeaderRight } from '@components/Header'
|
||||||
import Timeline from '@components/Timeline'
|
import Timeline from '@components/Timeline'
|
||||||
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { useAccountQuery } from '@utils/queryHooks/account'
|
import { useAccountQuery } from '@utils/queryHooks/account'
|
||||||
|
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react'
|
import React, { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
@ -67,15 +69,24 @@ const TabSharedAccount: React.FC<SharedAccountProp> = ({
|
||||||
)
|
)
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
|
const queryKey: QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{ page: 'Account_Default', account: account.id }
|
||||||
|
]
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} account={data} />
|
||||||
|
|
||||||
<Timeline
|
<Timeline
|
||||||
page='Account_Default'
|
queryKey={queryKey}
|
||||||
account={account.id}
|
|
||||||
disableRefresh
|
disableRefresh
|
||||||
customProps={{
|
customProps={{
|
||||||
|
renderItem,
|
||||||
onScroll,
|
onScroll,
|
||||||
scrollEventThrottle: 16,
|
scrollEventThrottle: 16,
|
||||||
ListHeaderComponent
|
ListHeaderComponent
|
||||||
|
|
|
@ -63,7 +63,7 @@ const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => {
|
||||||
]}
|
]}
|
||||||
selectable
|
selectable
|
||||||
>
|
>
|
||||||
@{myInfo ? instanceAccount.acct : account?.acct}
|
@{myInfo ? instanceAccount?.acct : account?.acct}
|
||||||
{myInfo ? `@${instanceUri}` : null}
|
{myInfo ? `@${instanceUri}` : null}
|
||||||
</Text>
|
</Text>
|
||||||
{movedContent}
|
{movedContent}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import Timeline from '@components/Timeline'
|
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'
|
import { SharedAttachmentsProp } from './sharedScreens'
|
||||||
|
|
||||||
const TabSharedAttachments: React.FC<SharedAttachmentsProp> = ({
|
const TabSharedAttachments: React.FC<SharedAttachmentsProp> = ({
|
||||||
|
@ -7,7 +9,15 @@ const TabSharedAttachments: React.FC<SharedAttachmentsProp> = ({
|
||||||
params: { account }
|
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
|
export default TabSharedAttachments
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import Timeline from '@components/Timeline'
|
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'
|
import { SharedHashtagProp } from './sharedScreens'
|
||||||
|
|
||||||
const TabSharedHashtag: React.FC<SharedHashtagProp> = ({
|
const TabSharedHashtag: React.FC<SharedHashtagProp> = ({
|
||||||
|
@ -7,7 +9,12 @@ const TabSharedHashtag: React.FC<SharedHashtagProp> = ({
|
||||||
params: { hashtag }
|
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
|
export default TabSharedHashtag
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import ComponentAccount from '@components/Account'
|
import ComponentAccount from '@components/Account'
|
||||||
import ComponentSeparator from '@components/Separator'
|
import ComponentSeparator from '@components/Separator'
|
||||||
import TimelineEmpty from '@components/Timeline/Empty'
|
|
||||||
import TimelineEnd from '@components/Timeline/End'
|
|
||||||
import { useScrollToTop } from '@react-navigation/native'
|
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 React, { useCallback, useMemo, useRef } from 'react'
|
||||||
import { RefreshControl, StyleSheet } from 'react-native'
|
import { RefreshControl, StyleSheet } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
|
@ -14,17 +15,15 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||||
|
const queryKey: QueryKeyRelationships = ['Relationships', { type, id }]
|
||||||
const {
|
const {
|
||||||
status,
|
|
||||||
data,
|
data,
|
||||||
isFetching,
|
isFetching,
|
||||||
refetch,
|
refetch,
|
||||||
hasNextPage,
|
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useRelationshipsQuery({
|
} = useRelationshipsQuery({
|
||||||
type,
|
...queryKey[1],
|
||||||
id,
|
|
||||||
options: {
|
options: {
|
||||||
getPreviousPageParam: firstPage =>
|
getPreviousPageParam: firstPage =>
|
||||||
firstPage.links?.prev && { since_id: firstPage.links.next },
|
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' />,
|
({ item }) => <ComponentAccount account={item} origin='relationship' />,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
const flItemEmptyComponent = useMemo(
|
|
||||||
() => <TimelineEmpty status={status} refetch={refetch} />,
|
|
||||||
[status]
|
|
||||||
)
|
|
||||||
const onEndReached = useCallback(
|
const onEndReached = useCallback(
|
||||||
() => !isFetchingNextPage && fetchNextPage(),
|
() => !isFetchingNextPage && fetchNextPage(),
|
||||||
[isFetchingNextPage]
|
[isFetchingNextPage]
|
||||||
)
|
)
|
||||||
const ListFooterComponent = useCallback(
|
|
||||||
() => <TimelineEnd hasNextPage={hasNextPage} />,
|
|
||||||
[hasNextPage]
|
|
||||||
)
|
|
||||||
const refreshControl = useMemo(
|
const refreshControl = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<RefreshControl refreshing={isFetching} onRefresh={() => refetch()} />
|
<RefreshControl refreshing={isFetching} onRefresh={() => refetch()} />
|
||||||
|
@ -74,8 +65,6 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ListFooterComponent={ListFooterComponent}
|
|
||||||
ListEmptyComponent={flItemEmptyComponent}
|
|
||||||
refreshControl={refreshControl}
|
refreshControl={refreshControl}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import Timeline from '@components/Timeline'
|
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'
|
import { SharedTootProp } from './sharedScreens'
|
||||||
|
|
||||||
const TabSharedToot: React.FC<SharedTootProp> = ({
|
const TabSharedToot: React.FC<SharedTootProp> = ({
|
||||||
|
@ -7,11 +13,73 @@ const TabSharedToot: React.FC<SharedTootProp> = ({
|
||||||
params: { toot, rootQueryKey }
|
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 (
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
page='Toot'
|
flRef={flRef}
|
||||||
toot={toot.id}
|
queryKey={queryKey}
|
||||||
rootQueryKey={rootQueryKey}
|
customProps={{ renderItem, ...(testState && onScrollToIndexFailed) }}
|
||||||
disableRefresh
|
disableRefresh
|
||||||
disableInfinity
|
disableInfinity
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 { AxiosError } from 'axios'
|
||||||
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
|
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query'
|
||||||
|
|
||||||
export type QueryKey = [
|
export type QueryKeyRelationships = [
|
||||||
'Relationships',
|
'Relationships',
|
||||||
{ type: 'following' | 'followers'; id: Mastodon.Account['id'] }
|
{ type: 'following' | 'followers'; id: Mastodon.Account['id'] }
|
||||||
]
|
]
|
||||||
|
@ -11,7 +11,7 @@ const queryFunction = ({
|
||||||
queryKey,
|
queryKey,
|
||||||
pageParam
|
pageParam
|
||||||
}: {
|
}: {
|
||||||
queryKey: QueryKey
|
queryKey: QueryKeyRelationships
|
||||||
pageParam?: { [key: string]: string }
|
pageParam?: { [key: string]: string }
|
||||||
}) => {
|
}) => {
|
||||||
const { type, id } = queryKey[1]
|
const { type, id } = queryKey[1]
|
||||||
|
@ -27,7 +27,7 @@ const queryFunction = ({
|
||||||
const useRelationshipsQuery = <TData = Mastodon.Account[]>({
|
const useRelationshipsQuery = <TData = Mastodon.Account[]>({
|
||||||
options,
|
options,
|
||||||
...queryKeyParams
|
...queryKeyParams
|
||||||
}: QueryKey[1] & {
|
}: QueryKeyRelationships[1] & {
|
||||||
options?: UseInfiniteQueryOptions<
|
options?: UseInfiniteQueryOptions<
|
||||||
{
|
{
|
||||||
body: Mastodon.Account[]
|
body: Mastodon.Account[]
|
||||||
|
@ -37,7 +37,10 @@ const useRelationshipsQuery = <TData = Mastodon.Account[]>({
|
||||||
TData
|
TData
|
||||||
>
|
>
|
||||||
}) => {
|
}) => {
|
||||||
const queryKey: QueryKey = ['Relationships', { ...queryKeyParams }]
|
const queryKey: QueryKeyRelationships = [
|
||||||
|
'Relationships',
|
||||||
|
{ ...queryKeyParams }
|
||||||
|
]
|
||||||
return useInfiniteQuery(queryKey, queryFunction, options)
|
return useInfiniteQuery(queryKey, queryFunction, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ const useTimelineMutation = ({
|
||||||
let oldData
|
let oldData
|
||||||
params.queryKey && (oldData = queryClient.getQueryData(params.queryKey))
|
params.queryKey && (oldData = queryClient.getQueryData(params.queryKey))
|
||||||
|
|
||||||
haptics('Success')
|
haptics('Light')
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
case 'updateStatusProperty':
|
case 'updateStatusProperty':
|
||||||
updateStatusProperty({ queryClient, ...params })
|
updateStatusProperty({ queryClient, ...params })
|
||||||
|
|
|
@ -24,7 +24,10 @@ const updateStatus = ({
|
||||||
? !payload.currentValue
|
? !payload.currentValue
|
||||||
: true
|
: true
|
||||||
if (payload.propertyCount) {
|
if (payload.propertyCount) {
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
if (
|
||||||
|
typeof payload.currentValue === 'boolean' &&
|
||||||
|
payload.currentValue
|
||||||
|
) {
|
||||||
item.reblog![payload.propertyCount] = payload.countValue - 1
|
item.reblog![payload.propertyCount] = payload.countValue - 1
|
||||||
} else {
|
} else {
|
||||||
item.reblog![payload.propertyCount] = payload.countValue + 1
|
item.reblog![payload.propertyCount] = payload.countValue + 1
|
||||||
|
@ -36,7 +39,10 @@ const updateStatus = ({
|
||||||
? !payload.currentValue
|
? !payload.currentValue
|
||||||
: true
|
: true
|
||||||
if (payload.propertyCount) {
|
if (payload.propertyCount) {
|
||||||
if (typeof payload.currentValue === 'boolean' && payload.currentValue) {
|
if (
|
||||||
|
typeof payload.currentValue === 'boolean' &&
|
||||||
|
payload.currentValue
|
||||||
|
) {
|
||||||
item[payload.propertyCount] = payload.countValue - 1
|
item[payload.propertyCount] = payload.countValue - 1
|
||||||
} else {
|
} else {
|
||||||
item[payload.propertyCount] = payload.countValue + 1
|
item[payload.propertyCount] = payload.countValue + 1
|
||||||
|
|
|
@ -75,8 +75,16 @@ const addInstance = createAsyncThunk(
|
||||||
latestTime: undefined
|
latestTime: undefined
|
||||||
},
|
},
|
||||||
push: {
|
push: {
|
||||||
loading: false,
|
global: { loading: false, value: false },
|
||||||
enabled: 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: []
|
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
|
|
|
@ -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
|
|
@ -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 { createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
import { RootState } from '@root/store'
|
||||||
|
import * as Notifications from 'expo-notifications'
|
||||||
import { Instance } from '../instancesSlice'
|
import { Instance } from '../instancesSlice'
|
||||||
import pushDisable from './push/disable'
|
import pushRegister from './push/register'
|
||||||
import pushEnable from './push/enable'
|
import pushUnregister from './push/unregister'
|
||||||
|
|
||||||
export const updatePush = createAsyncThunk(
|
export const updateInstancePush = createAsyncThunk(
|
||||||
'instances/updatePush',
|
'instances/updatePush',
|
||||||
async (
|
async (
|
||||||
enable: boolean
|
disable: boolean,
|
||||||
): Promise<Instance['push']['subscription'] | boolean> => {
|
{ getState }
|
||||||
if (enable) {
|
): Promise<Instance['push']['keys'] | undefined> => {
|
||||||
return pushEnable()
|
const state = getState() as RootState
|
||||||
|
const expoToken = (
|
||||||
|
await Notifications.getExpoPushTokenAsync({
|
||||||
|
experienceId: '@xmflsct/tooot'
|
||||||
|
})
|
||||||
|
).data
|
||||||
|
|
||||||
|
if (disable) {
|
||||||
|
return await pushRegister(state, expoToken)
|
||||||
} else {
|
} else {
|
||||||
return pushDisable()
|
return await pushUnregister(state, expoToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
)
|
|
@ -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 addInstance from './instances/add'
|
||||||
import removeInstance from './instances/remove'
|
import removeInstance from './instances/remove'
|
||||||
import { updateAccountPreferences } from './instances/updateAccountPreferences'
|
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 = {
|
export type Instance = {
|
||||||
active: boolean
|
active: boolean
|
||||||
|
@ -29,11 +33,65 @@ export type Instance = {
|
||||||
readTime?: Mastodon.Notification['created_at']
|
readTime?: Mastodon.Notification['created_at']
|
||||||
latestTime?: Mastodon.Notification['created_at']
|
latestTime?: Mastodon.Notification['created_at']
|
||||||
}
|
}
|
||||||
push: {
|
push:
|
||||||
loading: boolean
|
| {
|
||||||
enabled: boolean
|
global: { loading: boolean; value: true }
|
||||||
subscription?: Mastodon.PushSubscription
|
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[]
|
drafts: ComposeStateDraft[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +177,6 @@ const instancesSlice = createSlice({
|
||||||
state.instances.push(action.payload.data)
|
state.instances.push(action.payload.data)
|
||||||
break
|
break
|
||||||
case 'overwrite':
|
case 'overwrite':
|
||||||
console.log('overwriting')
|
|
||||||
state.instances = state.instances.map(instance => {
|
state.instances = state.instances.map(instance => {
|
||||||
if (
|
if (
|
||||||
instance.url === action.payload.data.url &&
|
instance.url === action.payload.data.url &&
|
||||||
|
@ -152,6 +209,7 @@ const instancesSlice = createSlice({
|
||||||
console.error(action.error)
|
console.error(action.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update Instance Account Preferences
|
||||||
.addCase(updateAccountPreferences.fulfilled, (state, action) => {
|
.addCase(updateAccountPreferences.fulfilled, (state, action) => {
|
||||||
const activeIndex = findInstanceActive(state.instances)
|
const activeIndex = findInstanceActive(state.instances)
|
||||||
state.instances[activeIndex].account.preferences = action.payload
|
state.instances[activeIndex].account.preferences = action.payload
|
||||||
|
@ -160,14 +218,56 @@ const instancesSlice = createSlice({
|
||||||
console.error(action.error)
|
console.error(action.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
.addCase(updatePush.fulfilled, (state, action) => {
|
// Update Instance Push Global
|
||||||
|
.addCase(updateInstancePush.fulfilled, (state, action) => {
|
||||||
const activeIndex = findInstanceActive(state.instances)
|
const activeIndex = findInstanceActive(state.instances)
|
||||||
if (typeof action.payload === 'boolean') {
|
state.instances[activeIndex].push.global.loading = false
|
||||||
state.instances[activeIndex].push.enabled = action.payload
|
state.instances[activeIndex].push.global.value = action.meta.arg
|
||||||
} else {
|
state.instances[activeIndex].push.keys = action.payload
|
||||||
state.instances[activeIndex].push.enabled = true
|
})
|
||||||
state.instances[activeIndex].push.subscription = 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
|
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) => {
|
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
|
||||||
const instanceActive = findInstanceActive(instances)
|
const instanceActive = findInstanceActive(instances)
|
||||||
return instanceActive !== -1 ? instances[instanceActive].drafts : null
|
return instanceActive !== -1 ? instances[instanceActive].drafts : null
|
||||||
|
|
75
yarn.lock
75
yarn.lock
|
@ -968,6 +968,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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":
|
"@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"
|
version "7.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
|
||||||
|
@ -3247,7 +3254,7 @@ bcrypt-pbkdf@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
tweetnacl "^0.14.3"
|
||||||
|
|
||||||
big-integer@^1.6.44:
|
big-integer@^1.6.16, big-integer@^1.6.44:
|
||||||
version "1.6.48"
|
version "1.6.48"
|
||||||
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
|
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
|
||||||
integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
|
integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
|
||||||
|
@ -3319,6 +3326,19 @@ braces@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
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:
|
browser-process-hrtime@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
|
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"
|
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
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:
|
diff-sequences@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
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"
|
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"
|
||||||
integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==
|
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:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
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"
|
braces "^3.0.1"
|
||||||
picomatch "^2.0.5"
|
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":
|
mime-db@1.45.0, "mime-db@>= 1.43.0 < 2":
|
||||||
version "1.45.0"
|
version "1.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
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"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
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:
|
nanoid@^3.1.15:
|
||||||
version "3.1.20"
|
version "3.1.20"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
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/core" "^3.7.9"
|
||||||
"@react-navigation/native" "^3.8.3"
|
"@react-navigation/native" "^3.8.3"
|
||||||
|
|
||||||
react-query@^3.9.7:
|
react-query@^3.12.0:
|
||||||
version "3.9.7"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.9.7.tgz#324c697f418827c129c8c126d233c6052bb1e35e"
|
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.12.0.tgz#a2082a167f3e394e84dfd3cec0f8c7503abf33dc"
|
||||||
integrity sha512-vpQgRFOljd7Lr1wL8hOwxWzb7awLIjaeqaq6DJ1fzA8N9mK1fAkK+UVrt8WaXJGBfz7JEnfCiXuENQspk0N7Sw==
|
integrity sha512-WJYECeZ6xT2oxIlgqXUjLNLWRvJbeelXscVnAFfyUFgO21OYEYHMWPG61V9W57EUUqrXioQsNPsU9XyddfEvXQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.5.5"
|
"@babel/runtime" "^7.5.5"
|
||||||
|
broadcast-channel "^3.4.1"
|
||||||
match-sorter "^6.0.2"
|
match-sorter "^6.0.2"
|
||||||
|
|
||||||
react-redux@^7.2.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"
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
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:
|
rimraf@^2.5.4, rimraf@^2.6.1:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
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:
|
dependencies:
|
||||||
glob "^7.1.3"
|
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:
|
rimraf@~2.2.6:
|
||||||
version "2.2.8"
|
version "2.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
|
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"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
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:
|
unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
|
Loading…
Reference in New Issue