1
0
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:
xmflsct
2022-12-29 00:36:35 +01:00
parent 1ea6aff328
commit 4cddbb9bad
32 changed files with 1116 additions and 1210 deletions

View File

@ -1,7 +1,7 @@
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
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 { Flow } from 'react-native-animated-spinkit'
import CustomText from './Text'
@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({
overlay = false,
onPress
}) => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const loadingSpinkit = useMemo(
() => (
const loadingSpinkit = () =>
loading ? (
<View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} />
</View>
),
[theme]
)
) : null
const mainColor = useMemo(() => {
const mainColor = () => {
if (selected) {
return colors.blue
} else if (overlay) {
@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({
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) {
case 'icon':
return (
<>
<Icon
name={content}
color={mainColor}
color={mainColor()}
strokeWidth={strokeWidth}
style={{ opacity: loading ? 0 : 1 }}
size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)}
/>
{loading ? loadingSpinkit : null}
{loadingSpinkit()}
</>
)
case 'text':
@ -103,7 +93,7 @@ const Button: React.FC<Props> = ({
<>
<CustomText
style={{
color: mainColor,
color: mainColor(),
fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
opacity: loading ? 0 : 1
}}
@ -111,11 +101,11 @@ const Button: React.FC<Props> = ({
children={content}
testID='text'
/>
{loading ? loadingSpinkit : null}
{loadingSpinkit()}
</>
)
}
}, [theme, content, loading, disabled])
}
const [layoutHeight, setLayoutHeight] = useState<number | undefined>()
@ -136,8 +126,8 @@ const Button: React.FC<Props> = ({
justifyContent: 'center',
alignItems: 'center',
borderWidth: overlay ? 0 : 1,
borderColor: mainColor,
backgroundColor: colorBackground,
borderColor: mainColor(),
backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault,
paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
width: round && layoutHeight ? layoutHeight : undefined
@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({
})}
testID='base'
onPress={onPress}
children={children}
children={children()}
disabled={selected || disabled || loading}
/>
)

View File

@ -1,6 +1,6 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo, useState } from 'react'
import React, { useState } from 'react'
import {
AccessibilityProps,
Image,
@ -65,7 +65,7 @@ const GracefullyImage = ({
}
}
const blurhashView = useMemo(() => {
const blurhashView = () => {
if (hidden || !imageLoaded) {
if (blurhash) {
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
@ -75,7 +75,7 @@ const GracefullyImage = ({
} else {
return null
}
}, [hidden, imageLoaded])
}
return (
<Pressable
@ -98,7 +98,7 @@ const GracefullyImage = ({
style={[{ flex: 1 }, imageStyle]}
onLoad={onLoad}
/>
{blurhashView}
{blurhashView()}
</Pressable>
)
}

View File

@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
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 Sparkline from './Sparkline'
import CustomText from './Text'
@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
const onPress = () => {
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
}, [])
}
const padding = StyleConstants.Spacing.Global.PagePadding
const width = Dimensions.get('window').width / 4

View File

@ -2,7 +2,7 @@ import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import React from 'react'
import { Pressable } from 'react-native'
export interface Props {
@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({
background = false,
onPress
}) => {
const { colors, theme } = useTheme()
const { colors } = useTheme()
const children = useMemo(() => {
const children = () => {
switch (type) {
case 'icon':
return (
@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({
)
case 'text':
return (
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault }}
children={content}
/>
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} />
)
}
}, [theme])
}
return (
<Pressable
onPress={onPress}
children={children}
children={children()}
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
minHeight: 44,
minWidth: 44,
marginLeft: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),

View File

@ -2,7 +2,7 @@ import Icon from '@components/Icon'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import React from 'react'
import { AccessibilityProps, Pressable, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
@ -40,16 +40,14 @@ const HeaderRight: React.FC<Props> = ({
}) => {
const { colors, theme } = useTheme()
const loadingSpinkit = useMemo(
() => (
const loadingSpinkit = () =>
loading ? (
<View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
),
[theme]
)
) : null
const children = useMemo(() => {
const children = () => {
switch (type) {
case 'icon':
return (
@ -60,7 +58,7 @@ const HeaderRight: React.FC<Props> = ({
size={StyleConstants.Spacing.M * 1.25}
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
/>
{loading && loadingSpinkit}
{loadingSpinkit()}
</>
)
case 'text':
@ -79,11 +77,11 @@ const HeaderRight: React.FC<Props> = ({
}}
children={content}
/>
{loading && loadingSpinkit}
{loadingSpinkit()}
</>
)
}
}, [theme, loading, disabled])
}
return (
<Pressable
@ -92,7 +90,7 @@ const HeaderRight: React.FC<Props> = ({
accessibilityRole='button'
accessibilityState={accessibilityState}
onPress={onPress}
children={children}
children={children()}
disabled={disabled || loading}
style={{
flexDirection: 'row',

View File

@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react'
import React from 'react'
import { View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({
const { colors, theme } = useTheme()
const { screenReaderEnabled } = useAccessibility()
const loadingSpinkit = useMemo(
() => (
<View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
),
[theme]
)
return (
<View
style={{ minHeight: 50 }}
@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({
style={{ marginLeft: 8, opacity: loading ? 0 : 1 }}
/>
) : null}
{loading && loadingSpinkit}
{loading ? (
<View style={{ position: 'absolute' }}>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
) : null}
</View>
) : null}
</View>

View File

@ -20,81 +20,85 @@ export interface Props {
style?: TextStyle
}
const ParseEmojis = React.memo(
({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
if (!content) return null
const ParseEmojis: React.FC<Props> = ({
content,
emojis,
size = 'M',
adaptiveSize = false,
fontBold = false,
style
}) => {
if (!content) return null
const { reduceMotionEnabled } = useAccessibility()
const { reduceMotionEnabled } = useAccessibility()
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(
StyleConstants.Font.Size[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight = adaptiveScale(
StyleConstants.Font.LineHeight[size],
adaptiveSize ? adaptiveFontsize : 0
)
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(
StyleConstants.Font.Size[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight = adaptiveScale(
StyleConstants.Font.LineHeight[size],
adaptiveSize ? adaptiveFontsize : 0
)
const { colors, theme } = useTheme()
const { colors, theme } = useTheme()
return (
<CustomText
style={[
{
color: colors.primaryDefault,
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
},
style
]}
fontWeight={fontBold ? 'Bold' : undefined}
>
{emojis ? (
content
.split(regexEmoji)
.filter(f => f)
.map((str, i) => {
if (str.match(regexEmoji)) {
const emojiShortcode = str.split(regexEmoji)[1]
const emojiIndex = emojis.findIndex(emoji => {
return emojiShortcode === `:${emoji.shortcode}:`
})
if (emojiIndex === -1) {
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
} else {
const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url
: emojis[emojiIndex].url
if (validUrl.isHttpsUri(uri)) {
return (
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage
source={{ uri }}
style={{
width: adaptedFontsize,
height: adaptedFontsize,
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
}}
/>
</CustomText>
)
} else {
return null
}
}
return (
<CustomText
style={[
{
color: colors.primaryDefault,
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
},
style
]}
fontWeight={fontBold ? 'Bold' : undefined}
>
{emojis ? (
content
.split(regexEmoji)
.filter(f => f)
.map((str, i) => {
if (str.match(regexEmoji)) {
const emojiShortcode = str.split(regexEmoji)[1]
const emojiIndex = emojis.findIndex(emoji => {
return emojiShortcode === `:${emoji.shortcode}:`
})
if (emojiIndex === -1) {
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
} else {
return <CustomText key={i}>{str}</CustomText>
const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url
: emojis[emojiIndex].url
if (validUrl.isHttpsUri(uri)) {
return (
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage
source={{ uri }}
style={{
width: adaptedFontsize,
height: adaptedFontsize,
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
}}
/>
</CustomText>
)
} else {
return null
}
}
})
) : (
<CustomText>{content}</CustomText>
)}
</CustomText>
)
},
(prev, next) => prev.content === next.content && prev.style?.color === next.style?.color
)
} else {
return <CustomText key={i}>{str}</CustomText>
}
})
) : (
<CustomText>{content}</CustomText>
)}
</CustomText>
)
}
export default ParseEmojis

View File

@ -34,237 +34,233 @@ export interface Props {
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
}
const ParseHTML = React.memo(
({
content,
size = 'M',
textStyles,
adaptiveSize = false,
emojis,
mentions,
tags,
showFullLink = false,
numberOfLines = 10,
expandHint,
highlighted = false,
disableDetails = false,
selectable = false,
setSpoilerExpanded
}: Props) => {
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(
StyleConstants.Font.Size[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight = adaptiveScale(
StyleConstants.Font.LineHeight[size],
adaptiveSize ? adaptiveFontsize : 0
)
const ParseHTML: React.FC<Props> = ({
content,
size = 'M',
textStyles,
adaptiveSize = false,
emojis,
mentions,
tags,
showFullLink = false,
numberOfLines = 10,
expandHint,
highlighted = false,
disableDetails = false,
selectable = false,
setSpoilerExpanded
}) => {
const [adaptiveFontsize] = useGlobalStorage.number('app.font_size')
const adaptedFontsize = adaptiveScale(
StyleConstants.Font.Size[size],
adaptiveSize ? adaptiveFontsize : 0
)
const adaptedLineheight = adaptiveScale(
StyleConstants.Font.LineHeight[size],
adaptiveSize ? adaptiveFontsize : 0
)
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { params } = useRoute()
const { colors } = useTheme()
const { t } = useTranslation('componentParse')
if (!expandHint) {
expandHint = t('HTML.defaultHint')
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { params } = useRoute()
const { colors } = useTheme()
const { t } = useTranslation('componentParse')
if (!expandHint) {
expandHint = t('HTML.defaultHint')
}
if (disableDetails) {
numberOfLines = 4
}
const followedTags = useFollowedTagsQuery()
const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted)
const document = parseDocument(content)
const unwrapNode = (node: ChildNode): string => {
switch (node.type) {
case ElementType.Text:
return node.data
case ElementType.Tag:
if (node.name === 'span') {
if (node.attribs.class?.includes('invisible')) return ''
if (node.attribs.class?.includes('ellipsis'))
return node.children.map(child => unwrapNode(child)).join('') + '...'
}
return node.children.map(child => unwrapNode(child)).join('')
default:
return ''
}
if (disableDetails) {
numberOfLines = 4
}
const followedTags = useFollowedTagsQuery()
const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted)
const document = parseDocument(content)
const unwrapNode = (node: ChildNode): string => {
switch (node.type) {
case ElementType.Text:
return node.data
case ElementType.Tag:
if (node.name === 'span') {
if (node.attribs.class?.includes('invisible')) return ''
if (node.attribs.class?.includes('ellipsis'))
return node.children.map(child => unwrapNode(child)).join('') + '...'
}
return node.children.map(child => unwrapNode(child)).join('')
default:
return ''
}
}
const renderNode = (node: ChildNode, index: number) => {
switch (node.type) {
case ElementType.Text:
return (
<ParseEmojis
key={index}
content={node.data}
emojis={emojis}
size={size}
adaptiveSize={adaptiveSize}
/>
)
case ElementType.Tag:
switch (node.name) {
case 'a':
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1]
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
?.hashtag
const sameHashtag = paramsHashtag === tag
const isFollowing = followedTags.data?.pages[0]?.body.find(t => t.name === tag)
return (
<Text
key={index}
style={[
{ color: tag?.length ? colors.blue : colors.red },
isFollowing
? {
textDecorationColor: tag?.length ? colors.blue : colors.red,
textDecorationLine: 'underline',
textDecorationStyle: 'dotted'
}
: null
]}
onPress={() =>
tag?.length &&
!disableDetails &&
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
}
children={node.children.map(unwrapNode).join('')}
/>
)
}
if (classes.includes('mention') && mentions?.length) {
const mentionIndex = mentions.findIndex(mention => mention.url === href)
const paramsAccount = (params as { account: Mastodon.Account } | undefined)
?.account
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
return (
<Text
key={index}
style={{ color: mentionIndex > -1 ? colors.blue : undefined }}
onPress={() =>
mentionIndex > -1 &&
!disableDetails &&
!sameAccount &&
navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] })
}
children={node.children.map(unwrapNode).join('')}
/>
)
}
}
const content = node.children.map(child => unwrapNode(child)).join('')
const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content)
return (
<Text
key={index}
style={{ color: colors.blue }}
onPress={async () => {
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1)
})
} else {
await openLink(href, navigation)
}
}
}}
children={content !== href ? content : showFullLink ? href : content}
/>
)
break
case 'p':
if (index < document.children.length - 1) {
}
const renderNode = (node: ChildNode, index: number) => {
switch (node.type) {
case ElementType.Text:
return (
<ParseEmojis
key={index}
content={node.data}
emojis={emojis}
size={size}
adaptiveSize={adaptiveSize}
/>
)
case ElementType.Tag:
switch (node.name) {
case 'a':
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1]
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
?.hashtag
const sameHashtag = paramsHashtag === tag
const isFollowing = followedTags.data?.pages[0]?.body.find(t => t.name === tag)
return (
<Text key={index}>
{node.children.map((c, i) => renderNode(c, i))}
<Text style={{ lineHeight: adaptedLineheight / 2 }}>{'\n\n'}</Text>
</Text>
<Text
key={index}
style={[
{ color: tag?.length ? colors.blue : colors.red },
isFollowing
? {
textDecorationColor: tag?.length ? colors.blue : colors.red,
textDecorationLine: 'underline',
textDecorationStyle: 'dotted'
}
: null
]}
onPress={() =>
tag?.length &&
!disableDetails &&
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
}
children={node.children.map(unwrapNode).join('')}
/>
)
} else {
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
}
default:
if (classes.includes('mention') && mentions?.length) {
const mentionIndex = mentions.findIndex(mention => mention.url === href)
const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
return (
<Text
key={index}
style={{ color: mentionIndex > -1 ? colors.blue : undefined }}
onPress={() =>
mentionIndex > -1 &&
!disableDetails &&
!sameAccount &&
navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] })
}
children={node.children.map(unwrapNode).join('')}
/>
)
}
}
const content = node.children.map(child => unwrapNode(child)).join('')
const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content)
return (
<Text
key={index}
style={{ color: colors.blue }}
onPress={async () => {
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1)
})
} else {
await openLink(href, navigation)
}
}
}}
children={content !== href ? content : showFullLink ? href : content}
/>
)
break
case 'p':
if (index < document.children.length - 1) {
return (
<Text key={index}>
{node.children.map((c, i) => renderNode(c, i))}
<Text style={{ lineHeight: adaptedLineheight / 2 }}>{'\n\n'}</Text>
</Text>
)
} else {
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
}
}
return null
}
default:
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
}
}
return (
<View style={{ overflow: 'hidden' }}>
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
<Pressable
accessibilityLabel={t('HTML.accessibilityHint')}
onPress={() => {
layoutAnimation()
setExpanded(!expanded)
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
}}
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: 44,
backgroundColor: colors.backgroundDefault
}}
>
<Text
style={{
textAlign: 'center',
...StyleConstants.FontStyle.S,
color: colors.primaryDefault,
marginRight: StyleConstants.Spacing.S
}}
children={t('HTML.expanded', {
hint: expandHint,
moreLines:
numberOfLines > 1 && typeof totalLines === 'number'
? t('HTML.moreLines', { count: totalLines - numberOfLines })
: ''
})}
/>
<Icon
name={expanded ? 'Minimize2' : 'Maximize2'}
color={colors.primaryDefault}
strokeWidth={2}
size={StyleConstants.Font.Size[size]}
/>
</Pressable>
) : null}
<Text
children={document.children.map(renderNode)}
onTextLayout={({ nativeEvent }) => {
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
setTotalLines(nativeEvent.lines.length)
return null
}
return (
<View style={{ overflow: 'hidden' }}>
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
<Pressable
accessibilityLabel={t('HTML.accessibilityHint')}
onPress={() => {
layoutAnimation()
setExpanded(!expanded)
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
}}
style={{
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight,
...textStyles,
height: numberOfLines === 1 && !expanded ? 0 : undefined
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: 44,
backgroundColor: colors.backgroundDefault
}}
numberOfLines={
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
>
<Text
style={{
textAlign: 'center',
...StyleConstants.FontStyle.S,
color: colors.primaryDefault,
marginRight: StyleConstants.Spacing.S
}}
children={t('HTML.expanded', {
hint: expandHint,
moreLines:
numberOfLines > 1 && typeof totalLines === 'number'
? t('HTML.moreLines', { count: totalLines - numberOfLines })
: ''
})}
/>
<Icon
name={expanded ? 'Minimize2' : 'Maximize2'}
color={colors.primaryDefault}
strokeWidth={2}
size={StyleConstants.Font.Size[size]}
/>
</Pressable>
) : null}
<Text
children={document.children.map(renderNode)}
onTextLayout={({ nativeEvent }) => {
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
setTotalLines(nativeEvent.lines.length)
}
selectable={selectable}
/>
</View>
)
},
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
)
}}
style={{
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight,
...textStyles,
height: numberOfLines === 1 && !expanded ? 0 : undefined
}}
numberOfLines={
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
}
selectable={selectable}
/>
</View>
)
}
export default ParseHTML

View File

@ -115,4 +115,4 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
)
}
export default TimelineConversation
export default React.memo(TimelineConversation, () => true)

View File

@ -221,4 +221,4 @@ const TimelineDefault: React.FC<Props> = ({
)
}
export default TimelineDefault
export default React.memo(TimelineDefault, () => true)

View File

@ -18,7 +18,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
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 * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
@ -53,14 +53,6 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => {
notification.status &&
navigation.push('Tab-Shared-Toot', {
toot: notification.status,
rootQueryKey: queryKey
})
}, [])
const main = () => {
return (
<>
@ -159,7 +151,13 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
backgroundColor: colors.backgroundDefault,
paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding
}}
onPress={onPress}
onPress={() =>
notification.status &&
navigation.push('Tab-Shared-Toot', {
toot: notification.status,
rootQueryKey: queryKey
})
}
onLongPress={() => {}}
children={main()}
/>
@ -187,4 +185,4 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
)
}
export default TimelineNotifications
export default React.memo(TimelineNotifications, () => true)

View File

@ -16,7 +16,7 @@ import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { uniqBy } from 'lodash'
import React, { useCallback, useContext, useMemo } from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import StatusContext from './Context'
@ -76,7 +76,7 @@ const TimelineActions: React.FC = () => {
})
const [accountId] = useAccountStorage.string('auth.account.id')
const onPressReply = useCallback(() => {
const onPressReply = () => {
const accts = uniqBy(
([status.account] as Mastodon.Account[] & Mastodon.Mention[])
.concat(status.mentions)
@ -89,9 +89,9 @@ const TimelineActions: React.FC = () => {
accts,
queryKey
})
}, [status.replies_count])
}
const { showActionSheetWithOptions } = useActionSheet()
const onPressReblog = useCallback(() => {
const onPressReblog = () => {
if (!status.reblogged) {
showActionSheetWithOptions(
{
@ -157,8 +157,8 @@ const TimelineActions: React.FC = () => {
}
})
}
}, [status.reblogged, status.reblogs_count])
const onPressFavourite = useCallback(() => {
}
const onPressFavourite = () => {
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
@ -172,8 +172,8 @@ const TimelineActions: React.FC = () => {
countValue: status.favourites_count
}
})
}, [status.favourited, status.favourites_count])
const onPressBookmark = useCallback(() => {
}
const onPressBookmark = () => {
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
@ -187,28 +187,25 @@ const TimelineActions: React.FC = () => {
countValue: undefined
}
})
}, [status.bookmarked])
}
const childrenReply = useMemo(
() => (
<>
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
{status.replies_count > 0 ? (
<CustomText
style={{
color: colors.secondary,
fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.XS
}}
>
{status.replies_count}
</CustomText>
) : null}
</>
),
[status.replies_count]
const childrenReply = () => (
<>
<Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} />
{status.replies_count > 0 ? (
<CustomText
style={{
color: colors.secondary,
fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.XS
}}
>
{status.replies_count}
</CustomText>
) : null}
</>
)
const childrenReblog = useMemo(() => {
const childrenReblog = () => {
const color = (state: boolean) => (state ? colors.green : colors.secondary)
const disabled =
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
@ -236,8 +233,8 @@ const TimelineActions: React.FC = () => {
) : null}
</>
)
}, [status.reblogged, status.reblogs_count])
const childrenFavourite = useMemo(() => {
}
const childrenFavourite = () => {
const color = (state: boolean) => (state ? colors.red : colors.secondary)
return (
<>
@ -256,13 +253,13 @@ const TimelineActions: React.FC = () => {
) : null}
</>
)
}, [status.favourited, status.favourites_count])
const childrenBookmark = useMemo(() => {
}
const childrenBookmark = () => {
const color = (state: boolean) => (state ? colors.yellow : colors.secondary)
return (
<Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} />
)
}, [status.bookmarked])
}
return (
<View style={{ flexDirection: 'row' }}>
@ -275,7 +272,7 @@ const TimelineActions: React.FC = () => {
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReply}
children={childrenReply}
children={childrenReply()}
/>
<Pressable
@ -289,7 +286,7 @@ const TimelineActions: React.FC = () => {
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReblog}
children={childrenReblog}
children={childrenReblog()}
disabled={
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
}
@ -306,7 +303,7 @@ const TimelineActions: React.FC = () => {
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressFavourite}
children={childrenFavourite}
children={childrenFavourite()}
/>
<Pressable
@ -320,7 +317,7 @@ const TimelineActions: React.FC = () => {
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressBookmark}
children={childrenBookmark}
children={childrenBookmark()}
/>
</View>
)

View File

@ -14,7 +14,7 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
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 { Pressable, View } from 'react-native'
import StatusContext from './Context'
@ -73,7 +73,7 @@ const TimelinePoll: React.FC = () => {
}
})
const pollButton = useMemo(() => {
const pollButton = () => {
if (!poll.expired) {
if (!ownAccount && !poll.voted) {
return (
@ -127,17 +127,14 @@ const TimelinePoll: React.FC = () => {
)
}
}
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
}
const isSelected = useCallback(
(index: number): string =>
allOptions[index]
? `Check${poll.multiple ? 'Square' : 'Circle'}`
: `${poll.multiple ? 'Square' : 'Circle'}`,
[allOptions]
)
const isSelected = (index: number): string =>
allOptions[index]
? `Check${poll.multiple ? 'Square' : 'Circle'}`
: `${poll.multiple ? 'Square' : 'Circle'}`
const pollBodyDisallow = useMemo(() => {
const pollBodyDisallow = () => {
const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count
return poll.options.map((option, index) => (
<View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}>
@ -191,8 +188,8 @@ const TimelinePoll: React.FC = () => {
/>
</View>
))
}, [theme, poll.options])
const pollBodyAllow = useMemo(() => {
}
const pollBodyAllow = () => {
return poll.options.map((option, index) => (
<Pressable
key={index}
@ -229,7 +226,7 @@ const TimelinePoll: React.FC = () => {
</View>
</Pressable>
))
}, [theme, allOptions])
}
const pollVoteCounts = () => {
if (poll.voters_count !== null) {
@ -263,7 +260,7 @@ const TimelinePoll: React.FC = () => {
return (
<View style={{ marginTop: StyleConstants.Spacing.M }}>
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
{poll.expired || poll.voted ? pollBodyDisallow() : pollBodyAllow()}
<View
style={{
flex: 1,
@ -272,7 +269,7 @@ const TimelinePoll: React.FC = () => {
marginTop: StyleConstants.Spacing.XS
}}
>
{pollButton}
{pollButton()}
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{pollVoteCounts()}
{pollExpiration()}

View File

@ -5,7 +5,7 @@ import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { useGlobalStorageListener } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
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 Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import TimelineEmpty from './Empty'
@ -56,11 +56,6 @@ const Timeline: React.FC<Props> = ({
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
const onEndReached = useCallback(
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
[isFetchingNextPage]
)
const flRef = useRef<FlatList>(null)
const scrollY = useSharedValue(0)
@ -120,7 +115,7 @@ const Timeline: React.FC<Props> = ({
data={flattenData}
initialNumToRender={6}
maxToRenderPerBatch={3}
onEndReached={onEndReached}
onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />