Re-rendering improvements for like/unlike (#2180)

* Add a few memos

* Memo PostDropdownBtn better

* More memo

* More granularity

* Extract PostContent

* Fix a usage I missed

* oops
zio/stable
dan 2023-12-12 21:50:43 +00:00 committed by GitHub
parent a5e25a7a16
commit 5c701f8e0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 87 deletions

View File

@ -328,7 +328,9 @@ let PostThreadItemLoaded = ({
</View> </View>
<PostDropdownBtn <PostDropdownBtn
testID="postDropdownBtn" testID="postDropdownBtn"
post={post} postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
record={record} record={record}
style={{ style={{
paddingVertical: 6, paddingVertical: 6,

View File

@ -102,10 +102,6 @@ let FeedItemInner = ({
}): React.ReactNode => { }): React.ReactNode => {
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES,
)
const href = useMemo(() => { const href = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
@ -134,10 +130,6 @@ let FeedItemInner = ({
}) })
}, [post, record, openComposer]) }, [post, record, openComposer])
const onPressShowMore = React.useCallback(() => {
setLimitLines(false)
}, [setLimitLines])
const outerStyles = [ const outerStyles = [
styles.outer, styles.outer,
pal.view, pal.view,
@ -286,6 +278,41 @@ let FeedItemInner = ({
</Text> </Text>
</View> </View>
)} )}
<PostContent
moderation={moderation}
richText={richText}
postEmbed={post.embed}
postAuthor={post.author}
/>
<PostCtrls post={post} record={record} onPressReply={onPressReply} />
</View>
</View>
</Link>
)
}
FeedItemInner = memo(FeedItemInner)
let PostContent = ({
moderation,
richText,
postEmbed,
postAuthor,
}: {
moderation: PostModeration
richText: RichTextAPI
postEmbed: AppBskyFeedDefs.PostView['embed']
postAuthor: AppBskyFeedDefs.PostView['author']
}): React.ReactNode => {
const pal = usePalette('default')
const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES,
)
const onPressShowMore = React.useCallback(() => {
setLimitLines(false)
}, [setLimitLines])
return (
<ContentHider <ContentHider
testID="contentHider-post" testID="contentHider-post"
moderation={moderation.content} moderation={moderation.content}
@ -312,29 +339,25 @@ let FeedItemInner = ({
href="#" href="#"
/> />
) : undefined} ) : undefined}
{post.embed ? ( {postEmbed ? (
<ContentHider <ContentHider
testID="contentHider-embed" testID="contentHider-embed"
moderation={moderation.embed} moderation={moderation.embed}
moderationDecisions={moderation.decisions} moderationDecisions={moderation.decisions}
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} ignoreMute={isEmbedByEmbedder(postEmbed, postAuthor.did)}
ignoreQuoteDecisions ignoreQuoteDecisions
style={styles.embed}> style={styles.embed}>
<PostEmbeds <PostEmbeds
embed={post.embed} embed={postEmbed}
moderation={moderation.embed} moderation={moderation.embed}
moderationDecisions={moderation.decisions} moderationDecisions={moderation.decisions}
/> />
</ContentHider> </ContentHider>
) : null} ) : null}
</ContentHider> </ContentHider>
<PostCtrls post={post} record={record} onPressReply={onPressReply} />
</View>
</View>
</Link>
) )
} }
FeedItemInner = memo(FeedItemInner) PostContent = memo(PostContent)
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {

View File

@ -1,4 +1,4 @@
import React from 'react' import React, {memo} from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import {Text} from './text/Text' import {Text} from './text/Text'
import {TextLinkOnWebOnly} from './Link' import {TextLinkOnWebOnly} from './Link'
@ -29,7 +29,7 @@ interface PostMetaOpts {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
} }
export function PostMeta(opts: PostMetaOpts) { let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const displayName = opts.author.displayName || opts.author.handle const displayName = opts.author.displayName || opts.author.handle
const handle = opts.author.handle const handle = opts.author.handle
@ -92,6 +92,8 @@ export function PostMeta(opts: PostMetaOpts) {
</View> </View>
) )
} }
PostMeta = memo(PostMeta)
export {PostMeta}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -1,4 +1,4 @@
import React, {useMemo} from 'react' import React, {memo, useMemo} from 'react'
import {Image, StyleSheet, View} from 'react-native' import {Image, StyleSheet, View} from 'react-native'
import Svg, {Circle, Rect, Path} from 'react-native-svg' import Svg, {Circle, Rect, Path} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@ -43,13 +43,13 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
const BLUR_AMOUNT = isWeb ? 5 : 100 const BLUR_AMOUNT = isWeb ? 5 : 100
export function DefaultAvatar({ let DefaultAvatar = ({
type, type,
size, size,
}: { }: {
type: UserAvatarType type: UserAvatarType
size: number size: number
}) { }): React.ReactNode => {
if (type === 'algo') { if (type === 'algo') {
// Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
return ( return (
@ -112,14 +112,16 @@ export function DefaultAvatar({
</Svg> </Svg>
) )
} }
DefaultAvatar = memo(DefaultAvatar)
export {DefaultAvatar}
export function UserAvatar({ let UserAvatar = ({
type = 'user', type = 'user',
size, size,
avatar, avatar,
moderation, moderation,
usePlainRNImage = false, usePlainRNImage = false,
}: UserAvatarProps) { }: UserAvatarProps): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const aviStyle = useMemo(() => { const aviStyle = useMemo(() => {
@ -182,13 +184,15 @@ export function UserAvatar({
</View> </View>
) )
} }
UserAvatar = memo(UserAvatar)
export {UserAvatar}
export function EditableUserAvatar({ let EditableUserAvatar = ({
type = 'user', type = 'user',
size, size,
avatar, avatar,
onSelectNewAvatar, onSelectNewAvatar,
}: EditableUserAvatarProps) { }: EditableUserAvatarProps): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission() const {requestCameraAccessIfNeeded} = useCameraPermission()
@ -323,14 +327,20 @@ export function EditableUserAvatar({
</NativeDropdown> </NativeDropdown>
) )
} }
EditableUserAvatar = memo(EditableUserAvatar)
export {EditableUserAvatar}
export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) { let PreviewableUserAvatar = (
props: PreviewableUserAvatarProps,
): React.ReactNode => {
return ( return (
<UserPreviewLink did={props.did} handle={props.handle}> <UserPreviewLink did={props.did} handle={props.handle}>
<UserAvatar {...props} /> <UserAvatar {...props} />
</UserPreviewLink> </UserPreviewLink>
) )
} }
PreviewableUserAvatar = memo(PreviewableUserAvatar)
export {PreviewableUserAvatar}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
editButtonContainer: { editButtonContainer: {

View File

@ -1,8 +1,8 @@
import React from 'react' import React, {memo} from 'react'
import {Linking, StyleProp, View, ViewStyle} from 'react-native' import {Linking, StyleProp, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard' import Clipboard from '@react-native-clipboard/clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api' import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
@ -19,23 +19,26 @@ import {usePostDeleteMutation} from '#/state/queries/post'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
export function PostDropdownBtn({ let PostDropdownBtn = ({
testID, testID,
post, postAuthor,
postCid,
postUri,
record, record,
style, style,
}: { }: {
testID: string testID: string
post: Shadow<AppBskyFeedDefs.PostView> postAuthor: AppBskyActorDefs.ProfileViewBasic
postCid: string
postUri: string
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }): React.ReactNode => {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const theme = useTheme() const theme = useTheme()
const {_} = useLingui() const {_} = useLingui()
@ -46,13 +49,13 @@ export function PostDropdownBtn({
const toggleThreadMute = useToggleThreadMute() const toggleThreadMute = useToggleThreadMute()
const postDeleteMutation = usePostDeleteMutation() const postDeleteMutation = usePostDeleteMutation()
const rootUri = record.reply?.root?.uri || post.uri const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri) const isThreadMuted = mutedThreads.includes(rootUri)
const isAuthor = post.author.did === currentAccount?.did const isAuthor = postAuthor.did === currentAccount?.did
const href = React.useMemo(() => { const href = React.useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(postUri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(postAuthor, 'post', urip.rkey)
}, [post.uri, post.author]) }, [postUri, postAuthor])
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record.text, record.text,
@ -60,7 +63,7 @@ export function PostDropdownBtn({
) )
const onDeletePost = React.useCallback(() => { const onDeletePost = React.useCallback(() => {
postDeleteMutation.mutateAsync({uri: post.uri}).then( postDeleteMutation.mutateAsync({uri: postUri}).then(
() => { () => {
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
@ -69,7 +72,7 @@ export function PostDropdownBtn({
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )
}, [post, postDeleteMutation]) }, [postUri, postDeleteMutation])
const onToggleThreadMute = React.useCallback(() => { const onToggleThreadMute = React.useCallback(() => {
try { try {
@ -163,8 +166,8 @@ export function PostDropdownBtn({
onPress() { onPress() {
openModal({ openModal({
name: 'report', name: 'report',
uri: post.uri, uri: postUri,
cid: post.cid, cid: postCid,
}) })
}, },
testID: 'postDropdownReportBtn', testID: 'postDropdownReportBtn',
@ -211,3 +214,6 @@ export function PostDropdownBtn({
</EventStopper> </EventStopper>
) )
} }
PostDropdownBtn = memo(PostDropdownBtn)
export {PostDropdownBtn}

View File

@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import React, {memo, useCallback} from 'react'
import { import {
StyleProp, StyleProp,
StyleSheet, StyleSheet,
@ -27,7 +27,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
export function PostCtrls({ let PostCtrls = ({
big, big,
post, post,
record, record,
@ -39,7 +39,7 @@ export function PostCtrls({
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
onPressReply: () => void onPressReply: () => void
}) { }): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
@ -71,7 +71,14 @@ export function PostCtrls({
likeCount: post.likeCount || 0, likeCount: post.likeCount || 0,
}) })
} }
}, [post, postLikeMutation, postUnlikeMutation]) }, [
post.viewer?.like,
post.uri,
post.cid,
post.likeCount,
postLikeMutation,
postUnlikeMutation,
])
const onRepost = useCallback(() => { const onRepost = useCallback(() => {
closeModal() closeModal()
@ -89,7 +96,15 @@ export function PostCtrls({
repostCount: post.repostCount || 0, repostCount: post.repostCount || 0,
}) })
} }
}, [post, closeModal, postRepostMutation, postUnrepostMutation]) }, [
post.uri,
post.cid,
post.viewer?.repost,
post.repostCount,
closeModal,
postRepostMutation,
postUnrepostMutation,
])
const onQuote = useCallback(() => { const onQuote = useCallback(() => {
closeModal() closeModal()
@ -103,7 +118,16 @@ export function PostCtrls({
}, },
}) })
Haptics.default() Haptics.default()
}, [post, record, openComposer, closeModal]) }, [
post.uri,
post.cid,
post.author,
post.indexedAt,
record.text,
openComposer,
closeModal,
])
return ( return (
<View style={[styles.ctrls, style]}> <View style={[styles.ctrls, style]}>
<TouchableOpacity <TouchableOpacity
@ -179,7 +203,9 @@ export function PostCtrls({
{big ? undefined : ( {big ? undefined : (
<PostDropdownBtn <PostDropdownBtn
testID="postDropdownBtn" testID="postDropdownBtn"
post={post} postAuthor={post.author}
postCid={post.cid}
postUri={post.uri}
record={record} record={record}
style={styles.ctrlPad} style={styles.ctrlPad}
/> />
@ -189,6 +215,8 @@ export function PostCtrls({
</View> </View>
) )
} }
PostCtrls = memo(PostCtrls)
export {PostCtrls}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
ctrls: { ctrls: {

View File

@ -1,4 +1,4 @@
import React, {useCallback} from 'react' import React, {memo, useCallback} from 'react'
import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native'
import {RepostIcon} from 'lib/icons' import {RepostIcon} from 'lib/icons'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
@ -17,13 +17,13 @@ interface Props {
onQuote: () => void onQuote: () => void
} }
export const RepostButton = ({ let RepostButton = ({
isReposted, isReposted,
repostCount, repostCount,
big, big,
onRepost, onRepost,
onQuote, onQuote,
}: Props) => { }: Props): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
@ -80,6 +80,8 @@ export const RepostButton = ({
</TouchableOpacity> </TouchableOpacity>
) )
} }
RepostButton = memo(RepostButton)
export {RepostButton}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
control: { control: {