Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

dan and committed by
GitHub
5c701f8e a5e25a7a

+160 -87
+3 -1
src/view/com/post-thread/PostThreadItem.tsx
··· 328 328 </View> 329 329 <PostDropdownBtn 330 330 testID="postDropdownBtn" 331 - post={post} 331 + postAuthor={post.author} 332 + postCid={post.cid} 333 + postUri={post.uri} 332 334 record={record} 333 335 style={{ 334 336 paddingVertical: 6,
+73 -50
src/view/com/posts/FeedItem.tsx
··· 102 102 }): React.ReactNode => { 103 103 const {openComposer} = useComposerControls() 104 104 const pal = usePalette('default') 105 - const [limitLines, setLimitLines] = useState( 106 - () => countLines(richText.text) >= MAX_POST_LINES, 107 - ) 108 - 109 105 const href = useMemo(() => { 110 106 const urip = new AtUri(post.uri) 111 107 return makeProfileLink(post.author, 'post', urip.rkey) ··· 133 129 }, 134 130 }) 135 131 }, [post, record, openComposer]) 136 - 137 - const onPressShowMore = React.useCallback(() => { 138 - setLimitLines(false) 139 - }, [setLimitLines]) 140 132 141 133 const outerStyles = [ 142 134 styles.outer, ··· 286 278 </Text> 287 279 </View> 288 280 )} 289 - <ContentHider 290 - testID="contentHider-post" 291 - moderation={moderation.content} 292 - ignoreMute 293 - childContainerStyle={styles.contentHiderChild}> 294 - <PostAlerts moderation={moderation.content} style={styles.alert} /> 295 - {richText.text ? ( 296 - <View style={styles.postTextContainer}> 297 - <RichText 298 - testID="postText" 299 - type="post-text" 300 - richText={richText} 301 - lineHeight={1.3} 302 - numberOfLines={limitLines ? MAX_POST_LINES : undefined} 303 - style={s.flex1} 304 - /> 305 - </View> 306 - ) : undefined} 307 - {limitLines ? ( 308 - <TextLink 309 - text="Show More" 310 - style={pal.link} 311 - onPress={onPressShowMore} 312 - href="#" 313 - /> 314 - ) : undefined} 315 - {post.embed ? ( 316 - <ContentHider 317 - testID="contentHider-embed" 318 - moderation={moderation.embed} 319 - moderationDecisions={moderation.decisions} 320 - ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} 321 - ignoreQuoteDecisions 322 - style={styles.embed}> 323 - <PostEmbeds 324 - embed={post.embed} 325 - moderation={moderation.embed} 326 - moderationDecisions={moderation.decisions} 327 - /> 328 - </ContentHider> 329 - ) : null} 330 - </ContentHider> 281 + <PostContent 282 + moderation={moderation} 283 + richText={richText} 284 + postEmbed={post.embed} 285 + postAuthor={post.author} 286 + /> 331 287 <PostCtrls post={post} record={record} onPressReply={onPressReply} /> 332 288 </View> 333 289 </View> ··· 335 291 ) 336 292 } 337 293 FeedItemInner = memo(FeedItemInner) 294 + 295 + let PostContent = ({ 296 + moderation, 297 + richText, 298 + postEmbed, 299 + postAuthor, 300 + }: { 301 + moderation: PostModeration 302 + richText: RichTextAPI 303 + postEmbed: AppBskyFeedDefs.PostView['embed'] 304 + postAuthor: AppBskyFeedDefs.PostView['author'] 305 + }): React.ReactNode => { 306 + const pal = usePalette('default') 307 + const [limitLines, setLimitLines] = useState( 308 + () => countLines(richText.text) >= MAX_POST_LINES, 309 + ) 310 + 311 + const onPressShowMore = React.useCallback(() => { 312 + setLimitLines(false) 313 + }, [setLimitLines]) 314 + 315 + return ( 316 + <ContentHider 317 + testID="contentHider-post" 318 + moderation={moderation.content} 319 + ignoreMute 320 + childContainerStyle={styles.contentHiderChild}> 321 + <PostAlerts moderation={moderation.content} style={styles.alert} /> 322 + {richText.text ? ( 323 + <View style={styles.postTextContainer}> 324 + <RichText 325 + testID="postText" 326 + type="post-text" 327 + richText={richText} 328 + lineHeight={1.3} 329 + numberOfLines={limitLines ? MAX_POST_LINES : undefined} 330 + style={s.flex1} 331 + /> 332 + </View> 333 + ) : undefined} 334 + {limitLines ? ( 335 + <TextLink 336 + text="Show More" 337 + style={pal.link} 338 + onPress={onPressShowMore} 339 + href="#" 340 + /> 341 + ) : undefined} 342 + {postEmbed ? ( 343 + <ContentHider 344 + testID="contentHider-embed" 345 + moderation={moderation.embed} 346 + moderationDecisions={moderation.decisions} 347 + ignoreMute={isEmbedByEmbedder(postEmbed, postAuthor.did)} 348 + ignoreQuoteDecisions 349 + style={styles.embed}> 350 + <PostEmbeds 351 + embed={postEmbed} 352 + moderation={moderation.embed} 353 + moderationDecisions={moderation.decisions} 354 + /> 355 + </ContentHider> 356 + ) : null} 357 + </ContentHider> 358 + ) 359 + } 360 + PostContent = memo(PostContent) 338 361 339 362 const styles = StyleSheet.create({ 340 363 outer: {
+4 -2
src/view/com/util/PostMeta.tsx
··· 1 - import React from 'react' 1 + import React, {memo} from 'react' 2 2 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 3 import {Text} from './text/Text' 4 4 import {TextLinkOnWebOnly} from './Link' ··· 29 29 style?: StyleProp<ViewStyle> 30 30 } 31 31 32 - export function PostMeta(opts: PostMetaOpts) { 32 + let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 33 33 const pal = usePalette('default') 34 34 const displayName = opts.author.displayName || opts.author.handle 35 35 const handle = opts.author.handle ··· 92 92 </View> 93 93 ) 94 94 } 95 + PostMeta = memo(PostMeta) 96 + export {PostMeta} 95 97 96 98 const styles = StyleSheet.create({ 97 99 container: {
+18 -8
src/view/com/util/UserAvatar.tsx
··· 1 - import React, {useMemo} from 'react' 1 + import React, {memo, useMemo} from 'react' 2 2 import {Image, StyleSheet, View} from 'react-native' 3 3 import Svg, {Circle, Rect, Path} from 'react-native-svg' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' ··· 43 43 44 44 const BLUR_AMOUNT = isWeb ? 5 : 100 45 45 46 - export function DefaultAvatar({ 46 + let DefaultAvatar = ({ 47 47 type, 48 48 size, 49 49 }: { 50 50 type: UserAvatarType 51 51 size: number 52 - }) { 52 + }): React.ReactNode => { 53 53 if (type === 'algo') { 54 54 // Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. 55 55 return ( ··· 112 112 </Svg> 113 113 ) 114 114 } 115 + DefaultAvatar = memo(DefaultAvatar) 116 + export {DefaultAvatar} 115 117 116 - export function UserAvatar({ 118 + let UserAvatar = ({ 117 119 type = 'user', 118 120 size, 119 121 avatar, 120 122 moderation, 121 123 usePlainRNImage = false, 122 - }: UserAvatarProps) { 124 + }: UserAvatarProps): React.ReactNode => { 123 125 const pal = usePalette('default') 124 126 125 127 const aviStyle = useMemo(() => { ··· 182 184 </View> 183 185 ) 184 186 } 187 + UserAvatar = memo(UserAvatar) 188 + export {UserAvatar} 185 189 186 - export function EditableUserAvatar({ 190 + let EditableUserAvatar = ({ 187 191 type = 'user', 188 192 size, 189 193 avatar, 190 194 onSelectNewAvatar, 191 - }: EditableUserAvatarProps) { 195 + }: EditableUserAvatarProps): React.ReactNode => { 192 196 const pal = usePalette('default') 193 197 const {_} = useLingui() 194 198 const {requestCameraAccessIfNeeded} = useCameraPermission() ··· 323 327 </NativeDropdown> 324 328 ) 325 329 } 330 + EditableUserAvatar = memo(EditableUserAvatar) 331 + export {EditableUserAvatar} 326 332 327 - export function PreviewableUserAvatar(props: PreviewableUserAvatarProps) { 333 + let PreviewableUserAvatar = ( 334 + props: PreviewableUserAvatarProps, 335 + ): React.ReactNode => { 328 336 return ( 329 337 <UserPreviewLink did={props.did} handle={props.handle}> 330 338 <UserAvatar {...props} /> 331 339 </UserPreviewLink> 332 340 ) 333 341 } 342 + PreviewableUserAvatar = memo(PreviewableUserAvatar) 343 + export {PreviewableUserAvatar} 334 344 335 345 const styles = StyleSheet.create({ 336 346 editButtonContainer: {
+22 -16
src/view/com/util/forms/PostDropdownBtn.tsx
··· 1 - import React from 'react' 1 + import React, {memo} from 'react' 2 2 import {Linking, StyleProp, View, ViewStyle} from 'react-native' 3 3 import Clipboard from '@react-native-clipboard/clipboard' 4 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 - import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api' 5 + import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api' 6 6 import {toShareUrl} from 'lib/strings/url-helpers' 7 7 import {useTheme} from 'lib/ThemeContext' 8 8 import {shareUrl} from 'lib/sharing' ··· 19 19 import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' 20 20 import {useLanguagePrefs} from '#/state/preferences' 21 21 import {logger} from '#/logger' 22 - import {Shadow} from '#/state/cache/types' 23 22 import {msg} from '@lingui/macro' 24 23 import {useLingui} from '@lingui/react' 25 24 import {useSession} from '#/state/session' 26 25 import {isWeb} from '#/platform/detection' 27 26 28 - export function PostDropdownBtn({ 27 + let PostDropdownBtn = ({ 29 28 testID, 30 - post, 29 + postAuthor, 30 + postCid, 31 + postUri, 31 32 record, 32 33 style, 33 34 }: { 34 35 testID: string 35 - post: Shadow<AppBskyFeedDefs.PostView> 36 + postAuthor: AppBskyActorDefs.ProfileViewBasic 37 + postCid: string 38 + postUri: string 36 39 record: AppBskyFeedPost.Record 37 40 style?: StyleProp<ViewStyle> 38 - }) { 41 + }): React.ReactNode => { 39 42 const {hasSession, currentAccount} = useSession() 40 43 const theme = useTheme() 41 44 const {_} = useLingui() ··· 46 49 const toggleThreadMute = useToggleThreadMute() 47 50 const postDeleteMutation = usePostDeleteMutation() 48 51 49 - const rootUri = record.reply?.root?.uri || post.uri 52 + const rootUri = record.reply?.root?.uri || postUri 50 53 const isThreadMuted = mutedThreads.includes(rootUri) 51 - const isAuthor = post.author.did === currentAccount?.did 54 + const isAuthor = postAuthor.did === currentAccount?.did 52 55 const href = React.useMemo(() => { 53 - const urip = new AtUri(post.uri) 54 - return makeProfileLink(post.author, 'post', urip.rkey) 55 - }, [post.uri, post.author]) 56 + const urip = new AtUri(postUri) 57 + return makeProfileLink(postAuthor, 'post', urip.rkey) 58 + }, [postUri, postAuthor]) 56 59 57 60 const translatorUrl = getTranslatorLink( 58 61 record.text, ··· 60 63 ) 61 64 62 65 const onDeletePost = React.useCallback(() => { 63 - postDeleteMutation.mutateAsync({uri: post.uri}).then( 66 + postDeleteMutation.mutateAsync({uri: postUri}).then( 64 67 () => { 65 68 Toast.show('Post deleted') 66 69 }, ··· 69 72 Toast.show('Failed to delete post, please try again') 70 73 }, 71 74 ) 72 - }, [post, postDeleteMutation]) 75 + }, [postUri, postDeleteMutation]) 73 76 74 77 const onToggleThreadMute = React.useCallback(() => { 75 78 try { ··· 163 166 onPress() { 164 167 openModal({ 165 168 name: 'report', 166 - uri: post.uri, 167 - cid: post.cid, 169 + uri: postUri, 170 + cid: postCid, 168 171 }) 169 172 }, 170 173 testID: 'postDropdownReportBtn', ··· 211 214 </EventStopper> 212 215 ) 213 216 } 217 + 218 + PostDropdownBtn = memo(PostDropdownBtn) 219 + export {PostDropdownBtn}
+35 -7
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 1 - import React, {useCallback} from 'react' 1 + import React, {memo, useCallback} from 'react' 2 2 import { 3 3 StyleProp, 4 4 StyleSheet, ··· 27 27 import {Shadow} from '#/state/cache/types' 28 28 import {useRequireAuth} from '#/state/session' 29 29 30 - export function PostCtrls({ 30 + let PostCtrls = ({ 31 31 big, 32 32 post, 33 33 record, ··· 39 39 record: AppBskyFeedPost.Record 40 40 style?: StyleProp<ViewStyle> 41 41 onPressReply: () => void 42 - }) { 42 + }): React.ReactNode => { 43 43 const theme = useTheme() 44 44 const {openComposer} = useComposerControls() 45 45 const {closeModal} = useModalControls() ··· 71 71 likeCount: post.likeCount || 0, 72 72 }) 73 73 } 74 - }, [post, postLikeMutation, postUnlikeMutation]) 74 + }, [ 75 + post.viewer?.like, 76 + post.uri, 77 + post.cid, 78 + post.likeCount, 79 + postLikeMutation, 80 + postUnlikeMutation, 81 + ]) 75 82 76 83 const onRepost = useCallback(() => { 77 84 closeModal() ··· 89 96 repostCount: post.repostCount || 0, 90 97 }) 91 98 } 92 - }, [post, closeModal, postRepostMutation, postUnrepostMutation]) 99 + }, [ 100 + post.uri, 101 + post.cid, 102 + post.viewer?.repost, 103 + post.repostCount, 104 + closeModal, 105 + postRepostMutation, 106 + postUnrepostMutation, 107 + ]) 93 108 94 109 const onQuote = useCallback(() => { 95 110 closeModal() ··· 103 118 }, 104 119 }) 105 120 Haptics.default() 106 - }, [post, record, openComposer, closeModal]) 121 + }, [ 122 + post.uri, 123 + post.cid, 124 + post.author, 125 + post.indexedAt, 126 + record.text, 127 + openComposer, 128 + closeModal, 129 + ]) 130 + 107 131 return ( 108 132 <View style={[styles.ctrls, style]}> 109 133 <TouchableOpacity ··· 179 203 {big ? undefined : ( 180 204 <PostDropdownBtn 181 205 testID="postDropdownBtn" 182 - post={post} 206 + postAuthor={post.author} 207 + postCid={post.cid} 208 + postUri={post.uri} 183 209 record={record} 184 210 style={styles.ctrlPad} 185 211 /> ··· 189 215 </View> 190 216 ) 191 217 } 218 + PostCtrls = memo(PostCtrls) 219 + export {PostCtrls} 192 220 193 221 const styles = StyleSheet.create({ 194 222 ctrls: {
+5 -3
src/view/com/util/post-ctrls/RepostButton.tsx
··· 1 - import React, {useCallback} from 'react' 1 + import React, {memo, useCallback} from 'react' 2 2 import {StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native' 3 3 import {RepostIcon} from 'lib/icons' 4 4 import {s, colors} from 'lib/styles' ··· 17 17 onQuote: () => void 18 18 } 19 19 20 - export const RepostButton = ({ 20 + let RepostButton = ({ 21 21 isReposted, 22 22 repostCount, 23 23 big, 24 24 onRepost, 25 25 onQuote, 26 - }: Props) => { 26 + }: Props): React.ReactNode => { 27 27 const theme = useTheme() 28 28 const {openModal} = useModalControls() 29 29 const requireAuth = useRequireAuth() ··· 80 80 </TouchableOpacity> 81 81 ) 82 82 } 83 + RepostButton = memo(RepostButton) 84 + export {RepostButton} 83 85 84 86 const styles = StyleSheet.create({ 85 87 control: {