From 5c701f8e0bced2fe26544acc04732bdc0fcd8c40 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 12 Dec 2023 21:50:43 +0000 Subject: [PATCH] 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 --- src/view/com/post-thread/PostThreadItem.tsx | 4 +- src/view/com/posts/FeedItem.tsx | 123 +++++++++++------- src/view/com/util/PostMeta.tsx | 6 +- src/view/com/util/UserAvatar.tsx | 26 ++-- src/view/com/util/forms/PostDropdownBtn.tsx | 38 +++--- src/view/com/util/post-ctrls/PostCtrls.tsx | 42 +++++- src/view/com/util/post-ctrls/RepostButton.tsx | 8 +- 7 files changed, 160 insertions(+), 87 deletions(-) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 2636fdfbdf..8d12117071 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -328,7 +328,9 @@ let PostThreadItemLoaded = ({ { const {openComposer} = useComposerControls() const pal = usePalette('default') - const [limitLines, setLimitLines] = useState( - () => countLines(richText.text) >= MAX_POST_LINES, - ) - const href = useMemo(() => { const urip = new AtUri(post.uri) return makeProfileLink(post.author, 'post', urip.rkey) @@ -134,10 +130,6 @@ let FeedItemInner = ({ }) }, [post, record, openComposer]) - const onPressShowMore = React.useCallback(() => { - setLimitLines(false) - }, [setLimitLines]) - const outerStyles = [ styles.outer, pal.view, @@ -286,48 +278,12 @@ let FeedItemInner = ({ )} - - - {richText.text ? ( - - - - ) : undefined} - {limitLines ? ( - - ) : undefined} - {post.embed ? ( - - - - ) : null} - + @@ -336,6 +292,73 @@ let FeedItemInner = ({ } 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 ( + + + {richText.text ? ( + + + + ) : undefined} + {limitLines ? ( + + ) : undefined} + {postEmbed ? ( + + + + ) : null} + + ) +} +PostContent = memo(PostContent) + const styles = StyleSheet.create({ outer: { borderTopWidth: 1, diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index fa5f12f6b2..eef7094cd7 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {memo} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {Text} from './text/Text' import {TextLinkOnWebOnly} from './Link' @@ -29,7 +29,7 @@ interface PostMetaOpts { style?: StyleProp } -export function PostMeta(opts: PostMetaOpts) { +let PostMeta = (opts: PostMetaOpts): React.ReactNode => { const pal = usePalette('default') const displayName = opts.author.displayName || opts.author.handle const handle = opts.author.handle @@ -92,6 +92,8 @@ export function PostMeta(opts: PostMetaOpts) { ) } +PostMeta = memo(PostMeta) +export {PostMeta} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 395e9eb3ae..00ff7e1ec8 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react' +import React, {memo, useMemo} from 'react' import {Image, StyleSheet, View} from 'react-native' import Svg, {Circle, Rect, Path} from 'react-native-svg' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' @@ -43,13 +43,13 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps { const BLUR_AMOUNT = isWeb ? 5 : 100 -export function DefaultAvatar({ +let DefaultAvatar = ({ type, size, }: { type: UserAvatarType size: number -}) { +}): React.ReactNode => { 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. return ( @@ -112,14 +112,16 @@ export function DefaultAvatar({ ) } +DefaultAvatar = memo(DefaultAvatar) +export {DefaultAvatar} -export function UserAvatar({ +let UserAvatar = ({ type = 'user', size, avatar, moderation, usePlainRNImage = false, -}: UserAvatarProps) { +}: UserAvatarProps): React.ReactNode => { const pal = usePalette('default') const aviStyle = useMemo(() => { @@ -182,13 +184,15 @@ export function UserAvatar({ ) } +UserAvatar = memo(UserAvatar) +export {UserAvatar} -export function EditableUserAvatar({ +let EditableUserAvatar = ({ type = 'user', size, avatar, onSelectNewAvatar, -}: EditableUserAvatarProps) { +}: EditableUserAvatarProps): React.ReactNode => { const pal = usePalette('default') const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() @@ -323,14 +327,20 @@ export function EditableUserAvatar({ ) } +EditableUserAvatar = memo(EditableUserAvatar) +export {EditableUserAvatar} -export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) { +let PreviewableUserAvatar = ( + props: PreviewableUserAvatarProps, +): React.ReactNode => { return ( ) } +PreviewableUserAvatar = memo(PreviewableUserAvatar) +export {PreviewableUserAvatar} const styles = StyleSheet.create({ editButtonContainer: { diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 63590e92d0..8c4b03dd93 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,8 +1,8 @@ -import React from 'react' +import React, {memo} from 'react' import {Linking, StyleProp, View, ViewStyle} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' 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 {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' @@ -19,23 +19,26 @@ import {usePostDeleteMutation} from '#/state/queries/post' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {logger} from '#/logger' -import {Shadow} from '#/state/cache/types' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSession} from '#/state/session' import {isWeb} from '#/platform/detection' -export function PostDropdownBtn({ +let PostDropdownBtn = ({ testID, - post, + postAuthor, + postCid, + postUri, record, style, }: { testID: string - post: Shadow + postAuthor: AppBskyActorDefs.ProfileViewBasic + postCid: string + postUri: string record: AppBskyFeedPost.Record style?: StyleProp -}) { +}): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() const {_} = useLingui() @@ -46,13 +49,13 @@ export function PostDropdownBtn({ const toggleThreadMute = useToggleThreadMute() const postDeleteMutation = usePostDeleteMutation() - const rootUri = record.reply?.root?.uri || post.uri + const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) - const isAuthor = post.author.did === currentAccount?.did + const isAuthor = postAuthor.did === currentAccount?.did const href = React.useMemo(() => { - const urip = new AtUri(post.uri) - return makeProfileLink(post.author, 'post', urip.rkey) - }, [post.uri, post.author]) + const urip = new AtUri(postUri) + return makeProfileLink(postAuthor, 'post', urip.rkey) + }, [postUri, postAuthor]) const translatorUrl = getTranslatorLink( record.text, @@ -60,7 +63,7 @@ export function PostDropdownBtn({ ) const onDeletePost = React.useCallback(() => { - postDeleteMutation.mutateAsync({uri: post.uri}).then( + postDeleteMutation.mutateAsync({uri: postUri}).then( () => { Toast.show('Post deleted') }, @@ -69,7 +72,7 @@ export function PostDropdownBtn({ Toast.show('Failed to delete post, please try again') }, ) - }, [post, postDeleteMutation]) + }, [postUri, postDeleteMutation]) const onToggleThreadMute = React.useCallback(() => { try { @@ -163,8 +166,8 @@ export function PostDropdownBtn({ onPress() { openModal({ name: 'report', - uri: post.uri, - cid: post.cid, + uri: postUri, + cid: postCid, }) }, testID: 'postDropdownReportBtn', @@ -211,3 +214,6 @@ export function PostDropdownBtn({ ) } + +PostDropdownBtn = memo(PostDropdownBtn) +export {PostDropdownBtn} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index c0c5d470eb..83ea3e8c9f 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import React, {memo, useCallback} from 'react' import { StyleProp, StyleSheet, @@ -27,7 +27,7 @@ import {useComposerControls} from '#/state/shell/composer' import {Shadow} from '#/state/cache/types' import {useRequireAuth} from '#/state/session' -export function PostCtrls({ +let PostCtrls = ({ big, post, record, @@ -39,7 +39,7 @@ export function PostCtrls({ record: AppBskyFeedPost.Record style?: StyleProp onPressReply: () => void -}) { +}): React.ReactNode => { const theme = useTheme() const {openComposer} = useComposerControls() const {closeModal} = useModalControls() @@ -71,7 +71,14 @@ export function PostCtrls({ likeCount: post.likeCount || 0, }) } - }, [post, postLikeMutation, postUnlikeMutation]) + }, [ + post.viewer?.like, + post.uri, + post.cid, + post.likeCount, + postLikeMutation, + postUnlikeMutation, + ]) const onRepost = useCallback(() => { closeModal() @@ -89,7 +96,15 @@ export function PostCtrls({ repostCount: post.repostCount || 0, }) } - }, [post, closeModal, postRepostMutation, postUnrepostMutation]) + }, [ + post.uri, + post.cid, + post.viewer?.repost, + post.repostCount, + closeModal, + postRepostMutation, + postUnrepostMutation, + ]) const onQuote = useCallback(() => { closeModal() @@ -103,7 +118,16 @@ export function PostCtrls({ }, }) Haptics.default() - }, [post, record, openComposer, closeModal]) + }, [ + post.uri, + post.cid, + post.author, + post.indexedAt, + record.text, + openComposer, + closeModal, + ]) + return ( @@ -189,6 +215,8 @@ export function PostCtrls({ ) } +PostCtrls = memo(PostCtrls) +export {PostCtrls} const styles = StyleSheet.create({ ctrls: { diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 1d34a88abc..620852d8e4 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import React, {memo, useCallback} from 'react' import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native' import {RepostIcon} from 'lib/icons' import {s, colors} from 'lib/styles' @@ -17,13 +17,13 @@ interface Props { onQuote: () => void } -export const RepostButton = ({ +let RepostButton = ({ isReposted, repostCount, big, onRepost, onQuote, -}: Props) => { +}: Props): React.ReactNode => { const theme = useTheme() const {openModal} = useModalControls() const requireAuth = useRequireAuth() @@ -80,6 +80,8 @@ export const RepostButton = ({ ) } +RepostButton = memo(RepostButton) +export {RepostButton} const styles = StyleSheet.create({ control: {