mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Remove most React memorization
Though added memo for timeline components making them (almost) pure
This commit is contained in:
parent
1ea6aff328
commit
4cddbb9bad
@ -32,7 +32,7 @@
|
|||||||
"@react-native-community/blur": "^4.3.0",
|
"@react-native-community/blur": "^4.3.0",
|
||||||
"@react-native-community/netinfo": "9.3.7",
|
"@react-native-community/netinfo": "9.3.7",
|
||||||
"@react-native-community/segmented-control": "^2.2.2",
|
"@react-native-community/segmented-control": "^2.2.2",
|
||||||
"@react-native-menu/menu": "^0.7.2",
|
"@react-native-menu/menu": "^0.7.3",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.2",
|
"@react-navigation/bottom-tabs": "^6.5.2",
|
||||||
"@react-navigation/native": "^6.1.1",
|
"@react-navigation/native": "^6.1.1",
|
||||||
"@react-navigation/native-stack": "^6.9.7",
|
"@react-navigation/native-stack": "^6.9.7",
|
||||||
|
13
src/App.tsx
13
src/App.tsx
@ -17,10 +17,7 @@ import {
|
|||||||
setAccount,
|
setAccount,
|
||||||
setGlobalStorage
|
setGlobalStorage
|
||||||
} from '@utils/storage/actions'
|
} from '@utils/storage/actions'
|
||||||
import {
|
import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
|
||||||
hasMigratedFromAsyncStorage,
|
|
||||||
migrateFromAsyncStorage
|
|
||||||
} from '@utils/storage/migrations/toMMKV'
|
|
||||||
import ThemeManager from '@utils/styles/ThemeManager'
|
import ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import * as Localization from 'expo-localization'
|
import * as Localization from 'expo-localization'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
@ -51,17 +48,15 @@ const App: React.FC = () => {
|
|||||||
const [appIsReady, setAppIsReady] = useState(false)
|
const [appIsReady, setAppIsReady] = useState(false)
|
||||||
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
const [localCorrupt, setLocalCorrupt] = useState<string>()
|
||||||
|
|
||||||
const [hasMigrated, setHasMigrated] = useState(hasMigratedFromAsyncStorage)
|
const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prepare = async () => {
|
const prepare = async () => {
|
||||||
if (!hasMigrated && !hasMigratedFromAsyncStorage) {
|
if (!hasMigrated) {
|
||||||
try {
|
try {
|
||||||
await migrateFromAsyncStorage()
|
await migrateFromAsyncStorage()
|
||||||
setHasMigrated(true)
|
setHasMigrated(true)
|
||||||
} catch (e) {
|
} catch {}
|
||||||
// TODO: fall back to AsyncStorage? Wipe storage clean and use MMKV? Crash app?
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
log('log', 'App', 'loading from MMKV')
|
log('log', 'App', 'loading from MMKV')
|
||||||
const account = getGlobalStorage.string('account.active')
|
const account = getGlobalStorage.string('account.active')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({
|
|||||||
overlay = false,
|
overlay = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<View style={{ position: 'absolute' }}>
|
||||||
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const mainColor = useMemo(() => {
|
const mainColor = () => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
return colors.blue
|
return colors.blue
|
||||||
} else if (overlay) {
|
} else if (overlay) {
|
||||||
@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({
|
|||||||
return colors.primaryDefault
|
return colors.primaryDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, disabled, loading, selected])
|
|
||||||
|
|
||||||
const colorBackground = useMemo(() => {
|
|
||||||
if (overlay) {
|
|
||||||
return colors.backgroundOverlayInvert
|
|
||||||
} else {
|
|
||||||
return colors.backgroundDefault
|
|
||||||
}
|
}
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name={content}
|
name={content}
|
||||||
color={mainColor}
|
color={mainColor()}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
style={{ opacity: loading ? 0 : 1 }}
|
style={{ opacity: loading ? 0 : 1 }}
|
||||||
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
@ -103,7 +93,7 @@ const Button: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<CustomText
|
<CustomText
|
||||||
style={{
|
style={{
|
||||||
color: mainColor,
|
color: mainColor(),
|
||||||
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
|
||||||
opacity: loading ? 0 : 1
|
opacity: loading ? 0 : 1
|
||||||
}}
|
}}
|
||||||
@ -111,11 +101,11 @@ const Button: React.FC<Props> = ({
|
|||||||
children={content}
|
children={content}
|
||||||
testID='text'
|
testID='text'
|
||||||
/>
|
/>
|
||||||
{loading ? loadingSpinkit : null}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, content, loading, disabled])
|
}
|
||||||
|
|
||||||
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
|
||||||
|
|
||||||
@ -136,8 +126,8 @@ const Button: React.FC<Props> = ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: overlay ? 0 : 1,
|
borderWidth: overlay ? 0 : 1,
|
||||||
borderColor: mainColor,
|
borderColor: mainColor(),
|
||||||
backgroundColor: colorBackground,
|
backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault,
|
||||||
paddingVertical: StyleConstants.Spacing[spacing],
|
paddingVertical: StyleConstants.Spacing[spacing],
|
||||||
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
|
||||||
width: round && layoutHeight ? layoutHeight : undefined
|
width: round && layoutHeight ? layoutHeight : undefined
|
||||||
@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
testID='base'
|
testID='base'
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={selected || disabled || loading}
|
disabled={selected || disabled || loading}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
AccessibilityProps,
|
||||||
Image,
|
Image,
|
||||||
@ -65,7 +65,7 @@ const GracefullyImage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blurhashView = useMemo(() => {
|
const blurhashView = () => {
|
||||||
if (hidden || !imageLoaded) {
|
if (hidden || !imageLoaded) {
|
||||||
if (blurhash) {
|
if (blurhash) {
|
||||||
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
|
||||||
@ -75,7 +75,7 @@ const GracefullyImage = ({
|
|||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}, [hidden, imageLoaded])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -98,7 +98,7 @@ const GracefullyImage = ({
|
|||||||
style={[{ flex: 1 }, imageStyle]}
|
style={[{ flex: 1 }, imageStyle]}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
/>
|
/>
|
||||||
{blurhashView}
|
{blurhashView()}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
|||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
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, { PropsWithChildren, useCallback, useState } from 'react'
|
import React, { PropsWithChildren, useState } from 'react'
|
||||||
import { Dimensions, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable, View } from 'react-native'
|
||||||
import Sparkline from './Sparkline'
|
import Sparkline from './Sparkline'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = () => {
|
||||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const padding = StyleConstants.Spacing.Global.PagePadding
|
const padding = StyleConstants.Spacing.Global.PagePadding
|
||||||
const width = Dimensions.get('window').width / 4
|
const width = Dimensions.get('window').width / 4
|
||||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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 from 'react'
|
||||||
import { Pressable } from 'react-native'
|
import { Pressable } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({
|
|||||||
background = false,
|
background = false,
|
||||||
onPress
|
onPress
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<CustomText
|
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} />
|
||||||
fontStyle='M'
|
|
||||||
style={{ color: colors.primaryDefault }}
|
|
||||||
children={content}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: background
|
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
|
||||||
? colors.backgroundOverlayDefault
|
|
||||||
: undefined,
|
|
||||||
minHeight: 44,
|
minHeight: 44,
|
||||||
minWidth: 44,
|
minWidth: 44,
|
||||||
marginLeft: native
|
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||||
? -StyleConstants.Spacing.S
|
|
||||||
: StyleConstants.Spacing.S,
|
|
||||||
...(type === 'icon' && {
|
...(type === 'icon' && {
|
||||||
borderRadius: 100
|
borderRadius: 100
|
||||||
}),
|
}),
|
||||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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 from 'react'
|
||||||
import { AccessibilityProps, Pressable, View } from 'react-native'
|
import { AccessibilityProps, Pressable, View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
|
|
||||||
@ -40,16 +40,14 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
const loadingSpinkit = () =>
|
||||||
() => (
|
loading ? (
|
||||||
<View style={{ position: 'absolute' }}>
|
<View style={{ position: 'absolute' }}>
|
||||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
),
|
) : null
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = () => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
@ -60,7 +58,7 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
size={StyleConstants.Spacing.M * 1.25}
|
size={StyleConstants.Spacing.M * 1.25}
|
||||||
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
|
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
case 'text':
|
case 'text':
|
||||||
@ -79,11 +77,11 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
children={content}
|
children={content}
|
||||||
/>
|
/>
|
||||||
{loading && loadingSpinkit}
|
{loadingSpinkit()}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [theme, loading, disabled])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -92,7 +90,7 @@ const HeaderRight: React.FC<Props> = ({
|
|||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityState={accessibilityState}
|
accessibilityState={accessibilityState}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
children={children}
|
children={children()}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
|||||||
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 { ColorDefinitions } from '@utils/styles/themes'
|
import { ColorDefinitions } from '@utils/styles/themes'
|
||||||
import React, { useMemo } from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { Flow } from 'react-native-animated-spinkit'
|
import { Flow } from 'react-native-animated-spinkit'
|
||||||
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
|
||||||
@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
const { colors, theme } = useTheme()
|
const { colors, theme } = useTheme()
|
||||||
const { screenReaderEnabled } = useAccessibility()
|
const { screenReaderEnabled } = useAccessibility()
|
||||||
|
|
||||||
const loadingSpinkit = useMemo(
|
|
||||||
() => (
|
|
||||||
<View style={{ position: 'absolute' }}>
|
|
||||||
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
[theme]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{ minHeight: 50 }}
|
style={{ minHeight: 50 }}
|
||||||
@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{loading && loadingSpinkit}
|
{loading ? (
|
||||||
|
<View style={{ position: 'absolute' }}>
|
||||||
|
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
@ -20,8 +20,14 @@ export interface Props {
|
|||||||
style?: TextStyle
|
style?: TextStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseEmojis = React.memo(
|
const ParseEmojis: React.FC<Props> = ({
|
||||||
({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
|
content,
|
||||||
|
emojis,
|
||||||
|
size = 'M',
|
||||||
|
adaptiveSize = false,
|
||||||
|
fontBold = false,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
if (!content) return null
|
if (!content) return null
|
||||||
|
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
@ -93,8 +99,6 @@ const ParseEmojis = React.memo(
|
|||||||
)}
|
)}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
(prev, next) => prev.content === next.content && prev.style?.color === next.style?.color
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ParseEmojis
|
export default ParseEmojis
|
||||||
|
@ -34,8 +34,7 @@ export interface Props {
|
|||||||
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParseHTML = React.memo(
|
const ParseHTML: React.FC<Props> = ({
|
||||||
({
|
|
||||||
content,
|
content,
|
||||||
size = 'M',
|
size = 'M',
|
||||||
textStyles,
|
textStyles,
|
||||||
@ -50,7 +49,7 @@ const ParseHTML = React.memo(
|
|||||||
disableDetails = false,
|
disableDetails = false,
|
||||||
selectable = false,
|
selectable = false,
|
||||||
setSpoilerExpanded
|
setSpoilerExpanded
|
||||||
}: Props) => {
|
}) => {
|
||||||
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
|
||||||
const adaptedFontsize = adaptiveScale(
|
const adaptedFontsize = adaptiveScale(
|
||||||
StyleConstants.Font.Size[size],
|
StyleConstants.Font.Size[size],
|
||||||
@ -143,8 +142,7 @@ const ParseHTML = React.memo(
|
|||||||
}
|
}
|
||||||
if (classes.includes('mention') && mentions?.length) {
|
if (classes.includes('mention') && mentions?.length) {
|
||||||
const mentionIndex = mentions.findIndex(mention => mention.url === href)
|
const mentionIndex = mentions.findIndex(mention => mention.url === href)
|
||||||
const paramsAccount = (params as { account: Mastodon.Account } | undefined)
|
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
|
||||||
?.account
|
|
||||||
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
|
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@ -263,8 +261,6 @@ const ParseHTML = React.memo(
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ParseHTML
|
export default ParseHTML
|
||||||
|
@ -115,4 +115,4 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineConversation
|
export default React.memo(TimelineConversation, () => true)
|
||||||
|
@ -221,4 +221,4 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineDefault
|
export default React.memo(TimelineDefault, () => true)
|
||||||
|
@ -18,7 +18,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|||||||
import { useAccountStorage } from '@utils/storage/actions'
|
import { useAccountStorage } from '@utils/storage/actions'
|
||||||
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, { Fragment, useCallback, useState } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import * as ContextMenu from 'zeego/context-menu'
|
import * as ContextMenu from 'zeego/context-menu'
|
||||||
import StatusContext from './Shared/Context'
|
import StatusContext from './Shared/Context'
|
||||||
@ -53,14 +53,6 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
|
||||||
notification.status &&
|
|
||||||
navigation.push('Tab-Shared-Toot', {
|
|
||||||
toot: notification.status,
|
|
||||||
rootQueryKey: queryKey
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -159,7 +151,13 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
backgroundColor: colors.backgroundDefault,
|
backgroundColor: colors.backgroundDefault,
|
||||||
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
|
||||||
}}
|
}}
|
||||||
onPress={onPress}
|
onPress={() =>
|
||||||
|
notification.status &&
|
||||||
|
navigation.push('Tab-Shared-Toot', {
|
||||||
|
toot: notification.status,
|
||||||
|
rootQueryKey: queryKey
|
||||||
|
})
|
||||||
|
}
|
||||||
onLongPress={() => {}}
|
onLongPress={() => {}}
|
||||||
children={main()}
|
children={main()}
|
||||||
/>
|
/>
|
||||||
@ -187,4 +185,4 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimelineNotifications
|
export default React.memo(TimelineNotifications, () => true)
|
||||||
|
@ -16,7 +16,7 @@ import { useAccountStorage } from '@utils/storage/actions'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { uniqBy } from 'lodash'
|
import { uniqBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
@ -76,7 +76,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [accountId] = useAccountStorage.string('auth.account.id')
|
const [accountId] = useAccountStorage.string('auth.account.id')
|
||||||
const onPressReply = useCallback(() => {
|
const onPressReply = () => {
|
||||||
const accts = uniqBy(
|
const accts = uniqBy(
|
||||||
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
|
||||||
.concat(status.mentions)
|
.concat(status.mentions)
|
||||||
@ -89,9 +89,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
accts,
|
accts,
|
||||||
queryKey
|
queryKey
|
||||||
})
|
})
|
||||||
}, [status.replies_count])
|
}
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const onPressReblog = useCallback(() => {
|
const onPressReblog = () => {
|
||||||
if (!status.reblogged) {
|
if (!status.reblogged) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@ -157,8 +157,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const onPressFavourite = useCallback(() => {
|
const onPressFavourite = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
@ -172,8 +172,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
countValue: status.favourites_count
|
countValue: status.favourites_count
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const onPressBookmark = useCallback(() => {
|
const onPressBookmark = () => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
type: 'updateStatusProperty',
|
type: 'updateStatusProperty',
|
||||||
queryKey,
|
queryKey,
|
||||||
@ -187,10 +187,9 @@ const TimelineActions: React.FC = () => {
|
|||||||
countValue: undefined
|
countValue: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
const childrenReply = useMemo(
|
const childrenReply = () => (
|
||||||
() => (
|
|
||||||
<>
|
<>
|
||||||
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||||
{status.replies_count > 0 ? (
|
{status.replies_count > 0 ? (
|
||||||
@ -205,10 +204,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
</CustomText>
|
</CustomText>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
),
|
|
||||||
[status.replies_count]
|
|
||||||
)
|
)
|
||||||
const childrenReblog = useMemo(() => {
|
const childrenReblog = () => {
|
||||||
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
const color = (state: boolean) => (state ? colors.green : colors.secondary)
|
||||||
const disabled =
|
const disabled =
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
@ -236,8 +233,8 @@ const TimelineActions: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.reblogged, status.reblogs_count])
|
}
|
||||||
const childrenFavourite = useMemo(() => {
|
const childrenFavourite = () => {
|
||||||
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
const color = (state: boolean) => (state ? colors.red : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -256,13 +253,13 @@ const TimelineActions: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [status.favourited, status.favourites_count])
|
}
|
||||||
const childrenBookmark = useMemo(() => {
|
const childrenBookmark = () => {
|
||||||
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
|
||||||
return (
|
return (
|
||||||
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
|
||||||
)
|
)
|
||||||
}, [status.bookmarked])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row' }}>
|
<View style={{ flexDirection: 'row' }}>
|
||||||
@ -275,7 +272,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReply}
|
onPress={onPressReply}
|
||||||
children={childrenReply}
|
children={childrenReply()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -289,7 +286,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressReblog}
|
onPress={onPressReblog}
|
||||||
children={childrenReblog}
|
children={childrenReblog()}
|
||||||
disabled={
|
disabled={
|
||||||
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
|
||||||
}
|
}
|
||||||
@ -306,7 +303,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressFavourite}
|
onPress={onPressFavourite}
|
||||||
children={childrenFavourite}
|
children={childrenFavourite()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -320,7 +317,7 @@ const TimelineActions: React.FC = () => {
|
|||||||
: { accessibilityLabel: '' })}
|
: { accessibilityLabel: '' })}
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={onPressBookmark}
|
onPress={onPressBookmark}
|
||||||
children={childrenBookmark}
|
children={childrenBookmark()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,7 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert
|
|||||||
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 { maxBy } from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
import React, { useContext, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable, View } from 'react-native'
|
||||||
import StatusContext from './Context'
|
import StatusContext from './Context'
|
||||||
@ -73,7 +73,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const pollButton = useMemo(() => {
|
const pollButton = () => {
|
||||||
if (!poll.expired) {
|
if (!poll.expired) {
|
||||||
if (!ownAccount && !poll.voted) {
|
if (!ownAccount && !poll.voted) {
|
||||||
return (
|
return (
|
||||||
@ -127,17 +127,14 @@ const TimelinePoll: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
|
}
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = (index: number): string =>
|
||||||
(index: number): string =>
|
|
||||||
allOptions[index]
|
allOptions[index]
|
||||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
: `${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
[allOptions]
|
|
||||||
)
|
|
||||||
|
|
||||||
const pollBodyDisallow = useMemo(() => {
|
const pollBodyDisallow = () => {
|
||||||
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
|
||||||
@ -191,8 +188,8 @@ const TimelinePoll: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
}, [theme, poll.options])
|
}
|
||||||
const pollBodyAllow = useMemo(() => {
|
const pollBodyAllow = () => {
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
@ -229,7 +226,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))
|
))
|
||||||
}, [theme, allOptions])
|
}
|
||||||
|
|
||||||
const pollVoteCounts = () => {
|
const pollVoteCounts = () => {
|
||||||
if (poll.voters_count !== null) {
|
if (poll.voters_count !== null) {
|
||||||
@ -263,7 +260,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
<View style={{ marginTop: StyleConstants.Spacing.M }}>
|
||||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
{poll.expired || poll.voted ? pollBodyDisallow() : pollBodyAllow()}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -272,7 +269,7 @@ const TimelinePoll: React.FC = () => {
|
|||||||
marginTop: StyleConstants.Spacing.XS
|
marginTop: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pollButton}
|
{pollButton()}
|
||||||
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
|
||||||
{pollVoteCounts()}
|
{pollVoteCounts()}
|
||||||
{pollExpiration()}
|
{pollExpiration()}
|
||||||
|
@ -5,7 +5,7 @@ import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
|||||||
import { useGlobalStorageListener } from '@utils/storage/actions'
|
import { useGlobalStorageListener } from '@utils/storage/actions'
|
||||||
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, { RefObject, useCallback, useRef } from 'react'
|
import React, { RefObject, useRef } from 'react'
|
||||||
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
|
||||||
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
|
||||||
import TimelineEmpty from './Empty'
|
import TimelineEmpty from './Empty'
|
||||||
@ -56,11 +56,6 @@ const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
|
||||||
|
|
||||||
const onEndReached = useCallback(
|
|
||||||
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
|
|
||||||
[isFetchingNextPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
const flRef = useRef<FlatList>(null)
|
const flRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
@ -120,7 +115,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
data={flattenData}
|
data={flattenData}
|
||||||
initialNumToRender={6}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={3}
|
maxToRenderPerBatch={3}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
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 } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@ -34,9 +34,8 @@ const ScreenActions = ({
|
|||||||
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const dismiss = useCallback(() => {
|
const dismiss = () => navigation.goBack()
|
||||||
navigation.goBack()
|
|
||||||
}, [])
|
|
||||||
const onGestureEvent = useAnimatedGestureHandler({
|
const onGestureEvent = useAnimatedGestureHandler({
|
||||||
onActive: ({ translationY }) => {
|
onActive: ({ translationY }) => {
|
||||||
panY.value = translationY
|
panY.value = translationY
|
||||||
|
@ -9,7 +9,7 @@ import { RootStackScreenProps } from '@utils/navigation/navigators'
|
|||||||
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
|
||||||
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, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@ -56,8 +56,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
|||||||
}
|
}
|
||||||
}, [query.data])
|
}, [query.data])
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = ({ item, index }: { item: Mastodon.Announcement; index: number }) => (
|
||||||
({ item, index }: { item: Mastodon.Announcement; index: number }) => (
|
|
||||||
<View
|
<View
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
@ -181,23 +180,16 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
),
|
|
||||||
[mode]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onMomentumScrollEnd = useCallback(
|
const onMomentumScrollEnd = ({
|
||||||
({
|
|
||||||
nativeEvent: {
|
nativeEvent: {
|
||||||
contentOffset: { x },
|
contentOffset: { x },
|
||||||
layoutMeasurement: { width }
|
layoutMeasurement: { width }
|
||||||
}
|
}
|
||||||
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
}: NativeSyntheticEvent<NativeScrollEvent>) => setIndex(Math.floor(x / width))
|
||||||
setIndex(Math.floor(x / width))
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const ListEmptyComponent = useCallback(() => {
|
const ListEmptyComponent = () => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -209,7 +201,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
|||||||
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
return Platform.OS === 'ios' ? (
|
return Platform.OS === 'ios' ? (
|
||||||
<BlurView
|
<BlurView
|
||||||
|
@ -6,7 +6,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
|
|||||||
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useMemo } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
|
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
@ -18,7 +18,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
const { t } = useTranslation(['common', 'screenCompose'])
|
const { t } = useTranslation(['common', 'screenCompose'])
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const attachmentColor = useMemo(() => {
|
const attachmentColor = () => {
|
||||||
if (composeState.poll.active) return colors.disabled
|
if (composeState.poll.active) return colors.disabled
|
||||||
|
|
||||||
if (composeState.attachments.uploads.length) {
|
if (composeState.attachments.uploads.length) {
|
||||||
@ -26,7 +26,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
const attachmentOnPress = () => {
|
const attachmentOnPress = () => {
|
||||||
if (composeState.poll.active) return
|
if (composeState.poll.active) return
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollColor = useMemo(() => {
|
const pollColor = () => {
|
||||||
if (composeState.attachments.uploads.length) return colors.disabled
|
if (composeState.attachments.uploads.length) return colors.disabled
|
||||||
|
|
||||||
if (composeState.poll.active) {
|
if (composeState.poll.active) {
|
||||||
@ -43,7 +43,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
const pollOnPress = () => {
|
const pollOnPress = () => {
|
||||||
if (!composeState.attachments.uploads.length) {
|
if (!composeState.attachments.uploads.length) {
|
||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
@ -57,7 +57,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityIcon = useMemo(() => {
|
const visibilityIcon = () => {
|
||||||
switch (composeState.visibility) {
|
switch (composeState.visibility) {
|
||||||
case 'public':
|
case 'public':
|
||||||
return 'Globe'
|
return 'Globe'
|
||||||
@ -68,7 +68,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
case 'direct':
|
case 'direct':
|
||||||
return 'Mail'
|
return 'Mail'
|
||||||
}
|
}
|
||||||
}, [composeState.visibility])
|
}
|
||||||
const visibilityOnPress = () => {
|
const visibilityOnPress = () => {
|
||||||
if (!composeState.visibilityLock) {
|
if (!composeState.visibilityLock) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
@ -116,7 +116,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||||
const emojiColor = useMemo(() => {
|
const emojiColor = () => {
|
||||||
if (!emojis.current?.length) return colors.disabled
|
if (!emojis.current?.length) return colors.disabled
|
||||||
|
|
||||||
if (emojisState.targetIndex !== -1) {
|
if (emojisState.targetIndex !== -1) {
|
||||||
@ -124,7 +124,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [emojis.current?.length, emojisState.targetIndex])
|
}
|
||||||
const emojiOnPress = () => {
|
const emojiOnPress = () => {
|
||||||
if (emojisState.targetIndex === -1) {
|
if (emojisState.targetIndex === -1) {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
@ -159,7 +159,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={attachmentOnPress}
|
onPress={attachmentOnPress}
|
||||||
children={<Icon name='Camera' size={24} color={attachmentColor} />}
|
children={<Icon name='Camera' size={24} color={attachmentColor()} />}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
@ -171,7 +171,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={pollOnPress}
|
onPress={pollOnPress}
|
||||||
children={<Icon name='BarChart2' size={24} color={pollColor} />}
|
children={<Icon name='BarChart2' size={24} color={pollColor()} />}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
@ -183,7 +183,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
onPress={visibilityOnPress}
|
onPress={visibilityOnPress}
|
||||||
children={
|
children={
|
||||||
<Icon
|
<Icon
|
||||||
name={visibilityIcon}
|
name={visibilityIcon()}
|
||||||
size={24}
|
size={24}
|
||||||
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
|
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
|
||||||
/>
|
/>
|
||||||
@ -213,7 +213,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={emojiOnPress}
|
onPress={emojiOnPress}
|
||||||
children={<Icon name='Smile' size={24} color={emojiColor} />}
|
children={<Icon name='Smile' size={24} color={emojiColor()} />}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ import { useNavigation } from '@react-navigation/native'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { RefObject, useCallback, useContext, useEffect, useMemo, useRef } from 'react'
|
import React, { RefObject, useContext, useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
|
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
@ -32,16 +32,13 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null)
|
const flatListRef = useRef<FlatList>(null)
|
||||||
|
|
||||||
const sensitiveOnPress = useCallback(
|
const sensitiveOnPress = () =>
|
||||||
() =>
|
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'attachments/sensitive',
|
type: 'attachments/sensitive',
|
||||||
payload: { sensitive: !composeState.attachments.sensitive }
|
payload: { sensitive: !composeState.attachments.sensitive }
|
||||||
}),
|
})
|
||||||
[composeState.attachments.sensitive]
|
|
||||||
)
|
|
||||||
|
|
||||||
const calculateWidth = useCallback((item: ExtendedAttachment) => {
|
const calculateWidth = (item: ExtendedAttachment) => {
|
||||||
if (item.local) {
|
if (item.local) {
|
||||||
return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT
|
return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT
|
||||||
} else {
|
} else {
|
||||||
@ -59,9 +56,9 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
return DEFAULT_HEIGHT
|
return DEFAULT_HEIGHT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
const snapToOffsets = useMemo(() => {
|
const snapToOffsets = () => {
|
||||||
const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => {
|
const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => {
|
||||||
let currentOffset = 0
|
let currentOffset = 0
|
||||||
Array.from(Array(index).keys()).map(
|
Array.from(Array(index).keys()).map(
|
||||||
@ -81,19 +78,19 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
StyleConstants.Spacing.Global.PagePadding
|
StyleConstants.Spacing.Global.PagePadding
|
||||||
]
|
]
|
||||||
: attachmentsOffsets
|
: attachmentsOffsets
|
||||||
}, [composeState.attachments.uploads.length])
|
}
|
||||||
let prevOffsets = useRef<number[]>()
|
let prevOffsets = useRef<number[]>()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (snapToOffsets.length > (prevOffsets.current ? prevOffsets.current.length : 0)) {
|
const snap = snapToOffsets()
|
||||||
|
if (snap.length > (prevOffsets.current ? prevOffsets.current.length : 0)) {
|
||||||
flatListRef.current?.scrollToOffset({
|
flatListRef.current?.scrollToOffset({
|
||||||
offset: snapToOffsets[snapToOffsets.length - 2] + snapToOffsets[snapToOffsets.length - 1]
|
offset: snap[snapToOffsets.length - 2] + snap[snapToOffsets.length - 1]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
prevOffsets.current = snapToOffsets
|
prevOffsets.current = snap
|
||||||
}, [snapToOffsets, prevOffsets.current])
|
}, [snapToOffsets, prevOffsets.current])
|
||||||
|
|
||||||
const renderAttachment = useCallback(
|
const renderAttachment = ({ item, index }: { item: ExtendedAttachment; index: number }) => {
|
||||||
({ item, index }: { item: ExtendedAttachment; index: number }) => {
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={index}
|
key={index}
|
||||||
@ -189,52 +186,8 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const listFooter = useMemo(
|
|
||||||
() => (
|
|
||||||
<Pressable
|
|
||||||
accessible
|
|
||||||
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
|
|
||||||
style={{
|
|
||||||
height: DEFAULT_HEIGHT,
|
|
||||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
width: DEFAULT_HEIGHT,
|
|
||||||
backgroundColor: colors.backgroundOverlayInvert
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
|
||||||
await chooseAndUploadAttachment({
|
|
||||||
composeDispatch,
|
|
||||||
showActionSheetWithOptions
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type='icon'
|
|
||||||
content='UploadCloud'
|
|
||||||
spacing='M'
|
|
||||||
round
|
|
||||||
overlay
|
|
||||||
onPress={async () => {
|
|
||||||
await chooseAndUploadAttachment({
|
|
||||||
composeDispatch,
|
|
||||||
showActionSheetWithOptions
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2,
|
|
||||||
left: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -276,13 +229,54 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||||||
pagingEnabled={false}
|
pagingEnabled={false}
|
||||||
snapToAlignment='center'
|
snapToAlignment='center'
|
||||||
renderItem={renderAttachment}
|
renderItem={renderAttachment}
|
||||||
snapToOffsets={snapToOffsets}
|
snapToOffsets={snapToOffsets()}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
data={composeState.attachments.uploads}
|
data={composeState.attachments.uploads}
|
||||||
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
|
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? listFooter : null
|
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? (
|
||||||
|
<Pressable
|
||||||
|
accessible
|
||||||
|
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
|
||||||
|
style={{
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
width: DEFAULT_HEIGHT,
|
||||||
|
backgroundColor: colors.backgroundOverlayInvert
|
||||||
|
}}
|
||||||
|
onPress={async () => {
|
||||||
|
await chooseAndUploadAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
showActionSheetWithOptions
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='icon'
|
||||||
|
content='UploadCloud'
|
||||||
|
spacing='M'
|
||||||
|
round
|
||||||
|
overlay
|
||||||
|
onPress={async () => {
|
||||||
|
await chooseAndUploadAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
showActionSheetWithOptions
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top:
|
||||||
|
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) /
|
||||||
|
2,
|
||||||
|
left:
|
||||||
|
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
@ -2,7 +2,7 @@ import ComponentSeparator from '@components/Separator'
|
|||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
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, { useContext, useEffect, useMemo, useRef } from 'react'
|
import React, { useContext, useEffect, useRef } from 'react'
|
||||||
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import ComposePosting from '../Posting'
|
import ComposePosting from '../Posting'
|
||||||
@ -53,29 +53,22 @@ const ComposeRoot = () => {
|
|||||||
}
|
}
|
||||||
}, [composeState.tag])
|
}, [composeState.tag])
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
|
||||||
if (isFetching) {
|
|
||||||
return (
|
|
||||||
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [isFetching])
|
|
||||||
|
|
||||||
const Footer = useMemo(
|
|
||||||
() => <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />,
|
|
||||||
[accessibleRefAttachments.current]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
|
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={
|
||||||
|
isFetching ? (
|
||||||
|
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
||||||
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
ListHeaderComponent={ComposeRootHeader}
|
ListHeaderComponent={ComposeRootHeader}
|
||||||
ListFooterComponent={Footer}
|
ListFooterComponent={
|
||||||
|
<ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />
|
||||||
|
}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data={data ? data[mapSchemaToType()] : undefined}
|
data={data ? data[mapSchemaToType()] : undefined}
|
||||||
|
@ -21,7 +21,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import * as StoreReview from 'expo-store-review'
|
import * as StoreReview from 'expo-store-review'
|
||||||
import { filter } from 'lodash'
|
import { filter } from 'lodash'
|
||||||
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
import React, { useEffect, useMemo, useReducer, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, Keyboard, Platform } from 'react-native'
|
import { Alert, Keyboard, Platform } from 'react-native'
|
||||||
import ComposeDraftsList, { removeDraft } from './DraftsList'
|
import ComposeDraftsList, { removeDraft } from './DraftsList'
|
||||||
@ -202,8 +202,74 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
return () => (autoSave ? clearInterval(autoSave) : undefined)
|
return () => (autoSave ? clearInterval(autoSave) : undefined)
|
||||||
}, [composeState])
|
}, [composeState])
|
||||||
|
|
||||||
const headerLeft = useCallback(
|
const headerRightDisabled = () => {
|
||||||
() => (
|
if (totalTextCount > maxTootChars) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const mutateTimeline = useTimelineMutation({ onMutate: true })
|
||||||
|
|
||||||
|
const inputProps: EmojisState['inputProps'] = [
|
||||||
|
{
|
||||||
|
value: [
|
||||||
|
composeState.text.raw,
|
||||||
|
content => {
|
||||||
|
formatText({ textInput: 'text', composeDispatch, content })
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selection: [
|
||||||
|
composeState.text.selection,
|
||||||
|
selection => composeDispatch({ type: 'text', payload: { selection } })
|
||||||
|
],
|
||||||
|
isFocused: composeState.textInputFocus.isFocused.text,
|
||||||
|
maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0),
|
||||||
|
ref: composeState.textInputFocus.refs.text
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: [
|
||||||
|
composeState.spoiler.raw,
|
||||||
|
content => formatText({ textInput: 'spoiler', composeDispatch, content })
|
||||||
|
],
|
||||||
|
selection: [
|
||||||
|
composeState.spoiler.selection,
|
||||||
|
selection => composeDispatch({ type: 'spoiler', payload: { selection } })
|
||||||
|
],
|
||||||
|
isFocused: composeState.textInputFocus.isFocused.spoiler,
|
||||||
|
maxLength: maxTootChars - composeState.text.count,
|
||||||
|
ref: composeState.textInputFocus.refs.spoiler
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComponentEmojis
|
||||||
|
inputProps={inputProps}
|
||||||
|
customButton
|
||||||
|
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
||||||
|
>
|
||||||
|
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
||||||
|
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Compose-Root'
|
||||||
|
component={ComposeRoot}
|
||||||
|
options={{
|
||||||
|
title: `${totalTextCount} / ${maxTootChars}`,
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontWeight:
|
||||||
|
totalTextCount > maxTootChars
|
||||||
|
? StyleConstants.Font.Weight.Bold
|
||||||
|
: StyleConstants.Font.Weight.Normal,
|
||||||
|
fontSize: StyleConstants.Font.Size.M
|
||||||
|
},
|
||||||
|
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
||||||
|
headerLeft: () => (
|
||||||
<HeaderLeft
|
<HeaderLeft
|
||||||
type='text'
|
type='text'
|
||||||
content={t('common:buttons.cancel')}
|
content={t('common:buttons.cancel')}
|
||||||
@ -237,23 +303,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[composeState]
|
headerRight: () => (
|
||||||
)
|
|
||||||
const headerRightDisabled = useMemo(() => {
|
|
||||||
if (totalTextCount > maxTootChars) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}, [totalTextCount, composeState.attachments.uploads, composeState.text.raw])
|
|
||||||
const mutateTimeline = useTimelineMutation({ onMutate: true })
|
|
||||||
const headerRight = useCallback(
|
|
||||||
() => (
|
|
||||||
<HeaderRight
|
<HeaderRight
|
||||||
type='text'
|
type='text'
|
||||||
content={t(
|
content={t(
|
||||||
@ -276,7 +326,9 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
|
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
|
||||||
// https://github.com/tooot-app/app/issues/59
|
// https://github.com/tooot-app/app/issues/59
|
||||||
} else {
|
} else {
|
||||||
const currentCount = getGlobalStorage.number('app.count_till_store_review')
|
const currentCount = getGlobalStorage.number(
|
||||||
|
'app.count_till_store_review'
|
||||||
|
)
|
||||||
if (currentCount === 10) {
|
if (currentCount === 10) {
|
||||||
StoreReview?.isAvailableAsync()
|
StoreReview?.isAvailableAsync()
|
||||||
.then(() => StoreReview.requestReview())
|
.then(() => StoreReview.requestReview())
|
||||||
@ -332,78 +384,18 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
haptics('Error')
|
haptics('Error')
|
||||||
handleError({ message: 'Posting error', captureResponse: true })
|
handleError({ message: 'Posting error', captureResponse: true })
|
||||||
composeDispatch({ type: 'posting', payload: false })
|
composeDispatch({ type: 'posting', payload: false })
|
||||||
Alert.alert(t('screenCompose:heading.right.alert.default.title'), undefined, [
|
Alert.alert(
|
||||||
{ text: t('screenCompose:heading.right.alert.default.button') }
|
t('screenCompose:heading.right.alert.default.title'),
|
||||||
])
|
undefined,
|
||||||
|
[{ text: t('screenCompose:heading.right.alert.default.button') }]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
loading={composeState.posting}
|
loading={composeState.posting}
|
||||||
disabled={headerRightDisabled}
|
disabled={headerRightDisabled()}
|
||||||
/>
|
/>
|
||||||
),
|
|
||||||
[totalTextCount, composeState]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerContent = useMemo(() => {
|
|
||||||
return `${totalTextCount} / ${maxTootChars}`
|
|
||||||
}, [totalTextCount, maxTootChars, composeState.dirty])
|
|
||||||
|
|
||||||
const inputProps: EmojisState['inputProps'] = [
|
|
||||||
{
|
|
||||||
value: [
|
|
||||||
composeState.text.raw,
|
|
||||||
content => {
|
|
||||||
formatText({ textInput: 'text', composeDispatch, content })
|
|
||||||
}
|
|
||||||
],
|
|
||||||
selection: [
|
|
||||||
composeState.text.selection,
|
|
||||||
selection => composeDispatch({ type: 'text', payload: { selection } })
|
|
||||||
],
|
|
||||||
isFocused: composeState.textInputFocus.isFocused.text,
|
|
||||||
maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0),
|
|
||||||
ref: composeState.textInputFocus.refs.text
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: [
|
|
||||||
composeState.spoiler.raw,
|
|
||||||
content => formatText({ textInput: 'spoiler', composeDispatch, content })
|
|
||||||
],
|
|
||||||
selection: [
|
|
||||||
composeState.spoiler.selection,
|
|
||||||
selection => composeDispatch({ type: 'spoiler', payload: { selection } })
|
|
||||||
],
|
|
||||||
isFocused: composeState.textInputFocus.isFocused.spoiler,
|
|
||||||
maxLength: maxTootChars - composeState.text.count,
|
|
||||||
ref: composeState.textInputFocus.refs.spoiler
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComponentEmojis
|
|
||||||
inputProps={inputProps}
|
|
||||||
customButton
|
|
||||||
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
||||||
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
|
||||||
>
|
|
||||||
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
|
||||||
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
|
||||||
<Stack.Screen
|
|
||||||
name='Screen-Compose-Root'
|
|
||||||
component={ComposeRoot}
|
|
||||||
options={{
|
|
||||||
title: headerContent,
|
|
||||||
headerTitleStyle: {
|
|
||||||
fontWeight:
|
|
||||||
totalTextCount > maxTootChars
|
|
||||||
? StyleConstants.Font.Weight.Bold
|
|
||||||
: StyleConstants.Font.Weight.Normal,
|
|
||||||
fontSize: StyleConstants.Font.Size.M
|
|
||||||
},
|
|
||||||
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
|
||||||
headerLeft,
|
|
||||||
headerRight
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
@ -4,7 +4,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet'
|
|||||||
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
|
||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
@ -40,14 +40,41 @@ const ScreenImagesViewer = ({
|
|||||||
|
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
const { mode, colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const { t } = useTranslation(['common', 'screenImageViewer'])
|
const { t } = useTranslation(['common', 'screenImageViewer'])
|
||||||
|
|
||||||
const initialIndex = imageUrls.findIndex(image => image.id === id)
|
const initialIndex = imageUrls.findIndex(image => image.id === id)
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
||||||
|
|
||||||
const { showActionSheetWithOptions } = useActionSheet()
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const onPress = useCallback(() => {
|
|
||||||
|
const isZoomed = useSharedValue(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ backgroundColor: 'black' }}>
|
||||||
|
<StatusBar hidden />
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: insets.top,
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
zIndex: 999
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeaderLeft content='X' native={false} background onPress={() => navigation.goBack()} />
|
||||||
|
{!hideCounter ? (
|
||||||
|
<HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} />
|
||||||
|
) : null}
|
||||||
|
<HeaderRight
|
||||||
|
accessibilityLabel={t('screenImageViewer:content.actions.accessibilityLabel')}
|
||||||
|
accessibilityHint={t('screenImageViewer:content.actions.accessibilityHint')}
|
||||||
|
content='MoreHorizontal'
|
||||||
|
native={false}
|
||||||
|
background
|
||||||
|
onPress={() =>
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: [
|
options: [
|
||||||
@ -76,100 +103,7 @@ const ScreenImagesViewer = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, [currentIndex])
|
|
||||||
|
|
||||||
const isZoomed = useSharedValue(false)
|
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
|
||||||
({
|
|
||||||
item
|
|
||||||
}: {
|
|
||||||
item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
|
|
||||||
}) => {
|
|
||||||
const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT
|
|
||||||
const imageRatio = item.width && item.height ? item.width / item.height : 1
|
|
||||||
const imageWidth = item.width || 100
|
|
||||||
const imageHeight = item.height || 100
|
|
||||||
|
|
||||||
const maxWidthScale = item.width ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
|
|
||||||
const maxHeightScale = item.height ? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
|
|
||||||
const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Zoom
|
|
||||||
onZoomBegin={() => (isZoomed.value = true)}
|
|
||||||
onZoomEnd={() => (isZoomed.value = false)}
|
|
||||||
maximumZoomScale={max > 8 ? 8 : max}
|
|
||||||
simultaneousGesture={Gesture.Fling()
|
|
||||||
.direction(Directions.DOWN)
|
|
||||||
.onStart(() => {
|
|
||||||
if (isZoomed.value === false) {
|
|
||||||
runOnJS(navigation.goBack)()
|
|
||||||
}
|
}
|
||||||
})}
|
|
||||||
children={
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: WINDOW_WIDTH,
|
|
||||||
height: WINDOW_HEIGHT,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<GracefullyImage
|
|
||||||
uri={{ preview: item.preview_url, remote: item.remote_url, original: item.url }}
|
|
||||||
dimension={{
|
|
||||||
width:
|
|
||||||
screenRatio > imageRatio
|
|
||||||
? (WINDOW_HEIGHT / imageHeight) * imageWidth
|
|
||||||
: WINDOW_WIDTH,
|
|
||||||
height:
|
|
||||||
screenRatio > imageRatio
|
|
||||||
? WINDOW_HEIGHT
|
|
||||||
: (WINDOW_WIDTH / imageWidth) * imageHeight
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[isZoomed.value]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onViewableItemsChanged = useCallback(
|
|
||||||
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
|
||||||
setCurrentIndex(viewableItems[0]?.index || 0)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ backgroundColor: 'black' }}>
|
|
||||||
<StatusBar hidden />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: insets.top,
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
zIndex: 999
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HeaderLeft content='X' native={false} background onPress={() => navigation.goBack()} />
|
|
||||||
{!hideCounter ? (
|
|
||||||
<HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} />
|
|
||||||
) : null}
|
|
||||||
<HeaderRight
|
|
||||||
accessibilityLabel={t('screenImageViewer:content.actions.accessibilityLabel')}
|
|
||||||
accessibilityHint={t('screenImageViewer:content.actions.accessibilityHint')}
|
|
||||||
content='MoreHorizontal'
|
|
||||||
native={false}
|
|
||||||
background
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<LongPressGestureHandler
|
<LongPressGestureHandler
|
||||||
@ -211,8 +145,71 @@ const ScreenImagesViewer = ({
|
|||||||
pagingEnabled
|
pagingEnabled
|
||||||
horizontal
|
horizontal
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderItem}
|
renderItem={({
|
||||||
onViewableItemsChanged={onViewableItemsChanged}
|
item
|
||||||
|
}: {
|
||||||
|
item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
|
||||||
|
}) => {
|
||||||
|
const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT
|
||||||
|
const imageRatio = item.width && item.height ? item.width / item.height : 1
|
||||||
|
const imageWidth = item.width || 100
|
||||||
|
const imageHeight = item.height || 100
|
||||||
|
|
||||||
|
const maxWidthScale = item.width
|
||||||
|
? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4
|
||||||
|
: 0
|
||||||
|
const maxHeightScale = item.height
|
||||||
|
? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4
|
||||||
|
: 0
|
||||||
|
const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Zoom
|
||||||
|
onZoomBegin={() => (isZoomed.value = true)}
|
||||||
|
onZoomEnd={() => (isZoomed.value = false)}
|
||||||
|
maximumZoomScale={max > 8 ? 8 : max}
|
||||||
|
simultaneousGesture={Gesture.Fling()
|
||||||
|
.direction(Directions.DOWN)
|
||||||
|
.onStart(() => {
|
||||||
|
if (isZoomed.value === false) {
|
||||||
|
runOnJS(navigation.goBack)()
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
children={
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: WINDOW_WIDTH,
|
||||||
|
height: WINDOW_HEIGHT,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GracefullyImage
|
||||||
|
uri={{
|
||||||
|
preview: item.preview_url,
|
||||||
|
remote: item.remote_url,
|
||||||
|
original: item.url
|
||||||
|
}}
|
||||||
|
dimension={{
|
||||||
|
width:
|
||||||
|
screenRatio > imageRatio
|
||||||
|
? (WINDOW_HEIGHT / imageHeight) * imageWidth
|
||||||
|
: WINDOW_WIDTH,
|
||||||
|
height:
|
||||||
|
screenRatio > imageRatio
|
||||||
|
? WINDOW_HEIGHT
|
||||||
|
: (WINDOW_WIDTH / imageWidth) * imageHeight
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onViewableItemsChanged={({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
||||||
|
setCurrentIndex(viewableItems[0]?.index || 0)
|
||||||
|
}}
|
||||||
viewabilityConfig={{
|
viewabilityConfig={{
|
||||||
itemVisiblePercentThreshold: 50
|
itemVisiblePercentThreshold: 50
|
||||||
}}
|
}}
|
||||||
|
@ -6,8 +6,8 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
|||||||
import { useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { 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 } from 'react'
|
import React from 'react'
|
||||||
import { Dimensions, ListRenderItem, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable, View } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||||
|
|
||||||
@ -39,8 +39,25 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
.splice(0, DISPLAY_AMOUNT)
|
.splice(0, DISPLAY_AMOUNT)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const renderItem = useCallback<ListRenderItem<Mastodon.Status>>(
|
const styleContainer = useAnimatedStyle(() => {
|
||||||
({ item, index }) => {
|
if (flattenData.length) {
|
||||||
|
return {
|
||||||
|
height: withTiming(width + StyleConstants.Spacing.Global.PagePadding * 2),
|
||||||
|
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}, [flattenData.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View style={[{ flex: 1 }, styleContainer]}>
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
data={flattenData}
|
||||||
|
renderItem={({ item, index }) => {
|
||||||
if (index === DISPLAY_AMOUNT - 1) {
|
if (index === DISPLAY_AMOUNT - 1) {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -72,7 +89,8 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
return (
|
return (
|
||||||
<GracefullyImage
|
<GracefullyImage
|
||||||
uri={{
|
uri={{
|
||||||
original: item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url,
|
original:
|
||||||
|
item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url,
|
||||||
remote: item.media_attachments[0]?.remote_url
|
remote: item.media_attachments[0]?.remote_url
|
||||||
}}
|
}}
|
||||||
blurhash={
|
blurhash={
|
||||||
@ -84,29 +102,7 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}}
|
||||||
[account]
|
|
||||||
)
|
|
||||||
|
|
||||||
const styleContainer = useAnimatedStyle(() => {
|
|
||||||
if (flattenData.length) {
|
|
||||||
return {
|
|
||||||
height: withTiming(width + StyleConstants.Spacing.Global.PagePadding * 2),
|
|
||||||
paddingVertical: StyleConstants.Spacing.Global.PagePadding,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.border
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}, [flattenData.length])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View style={[{ flex: 1 }, styleContainer]}>
|
|
||||||
<FlatList
|
|
||||||
horizontal
|
|
||||||
data={flattenData}
|
|
||||||
renderItem={renderItem}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
@ -2,7 +2,7 @@ import { ParseEmojis } from '@components/Parse'
|
|||||||
import CustomText from '@components/Text'
|
import CustomText from '@components/Text'
|
||||||
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 from 'react'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { PlaceholderLine } from 'rn-placeholder'
|
import { PlaceholderLine } from 'rn-placeholder'
|
||||||
|
|
||||||
@ -13,21 +13,6 @@ export interface Props {
|
|||||||
const AccountInformationName: React.FC<Props> = ({ account }) => {
|
const AccountInformationName: React.FC<Props> = ({ account }) => {
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
|
||||||
const movedContent = useMemo(() => {
|
|
||||||
if (account?.moved) {
|
|
||||||
return (
|
|
||||||
<View style={{ marginLeft: StyleConstants.Spacing.S }}>
|
|
||||||
<ParseEmojis
|
|
||||||
content={account.moved.display_name || account.moved.username}
|
|
||||||
emojis={account.moved.emojis}
|
|
||||||
size='L'
|
|
||||||
fontBold
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [account?.moved])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -51,7 +36,16 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
|
|||||||
fontBold
|
fontBold
|
||||||
/>
|
/>
|
||||||
</CustomText>
|
</CustomText>
|
||||||
{movedContent}
|
{account.moved ? (
|
||||||
|
<View style={{ marginLeft: StyleConstants.Spacing.S }}>
|
||||||
|
<ParseEmojis
|
||||||
|
content={account.moved.display_name || account.moved.username}
|
||||||
|
emojis={account.moved.emojis}
|
||||||
|
size='L'
|
||||||
|
fontBold
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<PlaceholderLine
|
<PlaceholderLine
|
||||||
|
@ -9,7 +9,7 @@ import { SearchResult } from '@utils/queryHooks/search'
|
|||||||
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
|
import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users'
|
||||||
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, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { Circle, Flow } from 'react-native-animated-spinkit'
|
import { Circle, Flow } from 'react-native-animated-spinkit'
|
||||||
@ -41,11 +41,6 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
|
|||||||
})
|
})
|
||||||
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
|
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
|
||||||
|
|
||||||
const onEndReached = useCallback(
|
|
||||||
() => hasNextPage && !isFetchingNextPage && fetchNextPage(),
|
|
||||||
[hasNextPage, isFetchingNextPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -90,7 +85,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
|
|||||||
children={<Flow size={StyleConstants.Font.Size.L} color={colors.secondary} />}
|
children={<Flow size={StyleConstants.Font.Size.L} color={colors.secondary} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onEndReached={onEndReached}
|
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.75}
|
onEndReachedThreshold={0.75}
|
||||||
ItemSeparatorComponent={ComponentSeparator}
|
ItemSeparatorComponent={ComponentSeparator}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
|
@ -5,7 +5,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
|||||||
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
|
import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators'
|
||||||
import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React from 'react'
|
||||||
import { Platform } from 'react-native'
|
import { Platform } from 'react-native'
|
||||||
import TabLocal from './Local'
|
import TabLocal from './Local'
|
||||||
import TabMe from './Me'
|
import TabMe from './Me'
|
||||||
@ -20,31 +20,6 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
|
|||||||
const [accountActive] = useGlobalStorage.string('account.active')
|
const [accountActive] = useGlobalStorage.string('account.active')
|
||||||
const [avatarStatic] = useAccountStorage.string('auth.account.avatar_static')
|
const [avatarStatic] = useAccountStorage.string('auth.account.avatar_static')
|
||||||
|
|
||||||
const composeListeners = useMemo(
|
|
||||||
() => ({
|
|
||||||
tabPress: (e: any) => {
|
|
||||||
e.preventDefault()
|
|
||||||
haptics('Light')
|
|
||||||
navigation.navigate('Screen-Compose')
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const composeComponent = useCallback(() => null, [])
|
|
||||||
|
|
||||||
const meListeners = useMemo(
|
|
||||||
() => ({
|
|
||||||
tabLongPress: () => {
|
|
||||||
haptics('Light')
|
|
||||||
//@ts-ignore
|
|
||||||
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' })
|
|
||||||
//@ts-ignore
|
|
||||||
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' })
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={accountActive ? getGlobalStorage.string('app.prev_tab') : 'Tab-Me'}
|
initialRouteName={accountActive ? getGlobalStorage.string('app.prev_tab') : 'Tab-Me'}
|
||||||
@ -97,9 +72,32 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => {
|
|||||||
>
|
>
|
||||||
<Tab.Screen name='Tab-Local' component={TabLocal} />
|
<Tab.Screen name='Tab-Local' component={TabLocal} />
|
||||||
<Tab.Screen name='Tab-Public' component={TabPublic} />
|
<Tab.Screen name='Tab-Public' component={TabPublic} />
|
||||||
<Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} />
|
<Tab.Screen
|
||||||
|
name='Tab-Compose'
|
||||||
|
listeners={{
|
||||||
|
tabPress: e => {
|
||||||
|
e.preventDefault()
|
||||||
|
haptics('Light')
|
||||||
|
navigation.navigate('Screen-Compose')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{() => null}
|
||||||
|
</Tab.Screen>
|
||||||
<Tab.Screen name='Tab-Notifications' component={TabNotifications} />
|
<Tab.Screen name='Tab-Notifications' component={TabNotifications} />
|
||||||
<Tab.Screen name='Tab-Me' component={TabMe} listeners={meListeners} />
|
<Tab.Screen
|
||||||
|
name='Tab-Me'
|
||||||
|
component={TabMe}
|
||||||
|
listeners={{
|
||||||
|
tabLongPress: () => {
|
||||||
|
haptics('Light')
|
||||||
|
//@ts-ignore
|
||||||
|
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' })
|
||||||
|
//@ts-ignore
|
||||||
|
navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||||||
import { themes } from '@utils/styles/themes'
|
import { themes } from '@utils/styles/themes'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import { addScreenshotListener } from 'expo-screen-capture'
|
import { addScreenshotListener } from 'expo-screen-capture'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { IntlProvider } from 'react-intl'
|
import { IntlProvider } from 'react-intl'
|
||||||
import { Alert, Platform, StatusBar } from 'react-native'
|
import { Alert, Platform, StatusBar } from 'react-native'
|
||||||
@ -90,7 +90,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
useEmojisQuery({ options: { enabled: !!accountActive } })
|
useEmojisQuery({ options: { enabled: !!accountActive } })
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
const navigationContainerOnStateChange = useCallback(() => {
|
const navigationContainerOnStateChange = () => {
|
||||||
const currentRoute = navigationRef.getCurrentRoute()
|
const currentRoute = navigationRef.getCurrentRoute()
|
||||||
|
|
||||||
const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
|
const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
|
||||||
@ -98,7 +98,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setGlobalStorage('app.prev_tab', matchTabName[1])
|
setGlobalStorage('app.prev_tab', matchTabName[1])
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
// Deep linking for compose
|
// Deep linking for compose
|
||||||
const [deeplinked, setDeeplinked] = useState(false)
|
const [deeplinked, setDeeplinked] = useState(false)
|
||||||
@ -128,8 +128,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
}, [accounts, accountActive, deeplinked])
|
}, [accounts, accountActive, deeplinked])
|
||||||
|
|
||||||
// Share Extension
|
// Share Extension
|
||||||
const handleShare = useCallback(
|
const handleShare = (
|
||||||
(
|
|
||||||
item?:
|
item?:
|
||||||
| {
|
| {
|
||||||
data: { mimeType: string; data: string }[]
|
data: { mimeType: string; data: string }[]
|
||||||
@ -228,9 +227,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[]
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ShareMenu.getInitialShare(handleShare)
|
ShareMenu.getInitialShare(handleShare)
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -13,6 +13,8 @@ export type GlobalV0 = {
|
|||||||
// number
|
// number
|
||||||
'app.count_till_store_review'?: number
|
'app.count_till_store_review'?: number
|
||||||
'app.font_size'?: -1 | 0 | 1 | 2 | 3
|
'app.font_size'?: -1 | 0 | 1 | 2 | 3
|
||||||
|
'version.global': number
|
||||||
|
'version.account': number
|
||||||
// boolean
|
// boolean
|
||||||
'app.auto_play_gifv'?: boolean
|
'app.auto_play_gifv'?: boolean
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import log from '@utils/startup/log'
|
|||||||
import { secureStorage, storage } from '@utils/storage'
|
import { secureStorage, storage } from '@utils/storage'
|
||||||
import { MMKV } from 'react-native-mmkv'
|
import { MMKV } from 'react-native-mmkv'
|
||||||
|
|
||||||
export const hasMigratedFromAsyncStorage = storage.global.getBoolean('hasMigratedFromAsyncStorage')
|
export const versionStorageGlobal = storage.global.getNumber('version.global')
|
||||||
|
|
||||||
export async function migrateFromAsyncStorage(): Promise<void> {
|
export async function migrateFromAsyncStorage(): Promise<void> {
|
||||||
log('log', 'Migration', 'Migrating...')
|
log('log', 'Migration', 'Migrating...')
|
||||||
@ -107,7 +107,7 @@ export async function migrateFromAsyncStorage(): Promise<void> {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.global.set('hasMigratedFromAsyncStorage', true)
|
storage.global.set('version.global', 0)
|
||||||
|
|
||||||
const end = global.performance.now()
|
const end = global.performance.now()
|
||||||
log('log', 'Migration', `Migrated in ${end - start}ms`)
|
log('log', 'Migration', `Migrated in ${end - start}ms`)
|
||||||
|
@ -19,7 +19,7 @@ const ManageThemeContext = createContext<ContextType>({
|
|||||||
|
|
||||||
export const useTheme = () => useContext(ManageThemeContext)
|
export const useTheme = () => useContext(ManageThemeContext)
|
||||||
|
|
||||||
const useColorSchemeDelay = (delay = 500) => {
|
const useColorSchemeDelay = (delay = 50) => {
|
||||||
const [colorScheme, setColorScheme] = React.useState(Appearance.getColorScheme())
|
const [colorScheme, setColorScheme] = React.useState(Appearance.getColorScheme())
|
||||||
const onColorSchemeChange = React.useCallback(
|
const onColorSchemeChange = React.useCallback(
|
||||||
throttle(
|
throttle(
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -2922,13 +2922,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@react-native-menu/menu@npm:^0.7.2":
|
"@react-native-menu/menu@npm:^0.7.3":
|
||||||
version: 0.7.2
|
version: 0.7.3
|
||||||
resolution: "@react-native-menu/menu@npm:0.7.2"
|
resolution: "@react-native-menu/menu@npm:0.7.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: "*"
|
react: "*"
|
||||||
react-native: "*"
|
react-native: "*"
|
||||||
checksum: b82402d183a58427cf0cc0dd617a81019e559d828b370f37d2df2690254dfa1e53f4da228e8c63e53d7c29423fd939f9a4b0c2b2930dc7d6cc5fdc5858b523f6
|
checksum: 419b2e500c49248b3cc2202ceda7cdb35008dfc3a4d67becb5a1276a9d306c654eccfa041a1d040b97ebb188478e565c658fa5dee1d7db834a786d67e32461e1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -11232,7 +11232,7 @@ __metadata:
|
|||||||
"@react-native-community/blur": ^4.3.0
|
"@react-native-community/blur": ^4.3.0
|
||||||
"@react-native-community/netinfo": 9.3.7
|
"@react-native-community/netinfo": 9.3.7
|
||||||
"@react-native-community/segmented-control": ^2.2.2
|
"@react-native-community/segmented-control": ^2.2.2
|
||||||
"@react-native-menu/menu": ^0.7.2
|
"@react-native-menu/menu": ^0.7.3
|
||||||
"@react-navigation/bottom-tabs": ^6.5.2
|
"@react-navigation/bottom-tabs": ^6.5.2
|
||||||
"@react-navigation/native": ^6.1.1
|
"@react-navigation/native": ^6.1.1
|
||||||
"@react-navigation/native-stack": ^6.9.7
|
"@react-navigation/native-stack": ^6.9.7
|
||||||
|
Loading…
x
Reference in New Issue
Block a user