Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Shadow refactoring and improvements (#1959)

* Make shadow a type-only concept

* Prevent unnecessary init state recalc

* Use derived state instead of effects

* Batch emitter updates

* Use object first seen time instead of dataUpdatedAt

* Stop threading dataUpdatedAt through

* Use same value consistently

authored by danabra.mov and committed by

GitHub 4c4ba553 f18b9b32

+115 -203
+1
src/lib/batchedUpdates.ts
··· 1 + export {unstable_batchedUpdates as batchedUpdates} from 'react-native'
+2
src/lib/batchedUpdates.web.ts
··· 1 + // @ts-ignore 2 + export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
+37 -29
src/state/cache/post-shadow.ts
··· 1 - import {useEffect, useState, useMemo, useCallback, useRef} from 'react' 1 + import {useEffect, useState, useMemo, useCallback} from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 import {AppBskyFeedDefs} from '@atproto/api' 4 - import {Shadow} from './types' 4 + import {batchedUpdates} from '#/lib/batchedUpdates' 5 + import {Shadow, castAsShadow} from './types' 5 6 export type {Shadow} from './types' 6 7 7 8 const emitter = new EventEmitter() ··· 21 22 value: PostShadow 22 23 } 23 24 25 + const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>() 26 + function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number { 27 + let timeStamp = firstSeenMap.get(post) 28 + if (timeStamp !== undefined) { 29 + return timeStamp 30 + } 31 + timeStamp = Date.now() 32 + firstSeenMap.set(post, timeStamp) 33 + return timeStamp 34 + } 35 + 24 36 export function usePostShadow( 25 37 post: AppBskyFeedDefs.PostView, 26 - ifAfterTS: number, 27 38 ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 28 - const [state, setState] = useState<CacheEntry>({ 29 - ts: Date.now(), 39 + const postSeenTS = getFirstSeenTS(post) 40 + const [state, setState] = useState<CacheEntry>(() => ({ 41 + ts: postSeenTS, 30 42 value: fromPost(post), 31 - }) 32 - const firstRun = useRef(true) 43 + })) 44 + 45 + const [prevPost, setPrevPost] = useState(post) 46 + if (post !== prevPost) { 47 + // if we got a new prop, assume it's fresher 48 + // than whatever shadow state we accumulated 49 + setPrevPost(post) 50 + setState({ 51 + ts: postSeenTS, 52 + value: fromPost(post), 53 + }) 54 + } 33 55 34 56 const onUpdate = useCallback( 35 57 (value: Partial<PostShadow>) => { ··· 46 68 } 47 69 }, [post.uri, onUpdate]) 48 70 49 - // react to post updates 50 - useEffect(() => { 51 - // dont fire on first run to avoid needless re-renders 52 - if (!firstRun.current) { 53 - setState({ts: Date.now(), value: fromPost(post)}) 54 - } 55 - firstRun.current = false 56 - }, [post]) 57 - 58 71 return useMemo(() => { 59 - return state.ts > ifAfterTS 72 + return state.ts > postSeenTS 60 73 ? mergeShadow(post, state.value) 61 - : {...post, isShadowed: true} 62 - }, [post, state, ifAfterTS]) 74 + : castAsShadow(post) 75 + }, [post, state, postSeenTS]) 63 76 } 64 77 65 78 export function updatePostShadow(uri: string, value: Partial<PostShadow>) { 66 - emitter.emit(uri, value) 67 - } 68 - 69 - export function isPostShadowed( 70 - v: AppBskyFeedDefs.PostView | Shadow<AppBskyFeedDefs.PostView>, 71 - ): v is Shadow<AppBskyFeedDefs.PostView> { 72 - return 'isShadowed' in v && !!v.isShadowed 79 + batchedUpdates(() => { 80 + emitter.emit(uri, value) 81 + }) 73 82 } 74 83 75 84 function fromPost(post: AppBskyFeedDefs.PostView): PostShadow { ··· 89 98 if (shadow.isDeleted) { 90 99 return POST_TOMBSTONE 91 100 } 92 - return { 101 + return castAsShadow({ 93 102 ...post, 94 103 likeCount: shadow.likeCount, 95 104 repostCount: shadow.repostCount, ··· 98 107 like: shadow.likeUri, 99 108 repost: shadow.repostUri, 100 109 }, 101 - isShadowed: true, 102 - } 110 + }) 103 111 }
+38 -32
src/state/cache/profile-shadow.ts
··· 1 - import {useEffect, useState, useMemo, useCallback, useRef} from 'react' 1 + import {useEffect, useState, useMemo, useCallback} from 'react' 2 2 import EventEmitter from 'eventemitter3' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 - import {Shadow} from './types' 4 + import {batchedUpdates} from '#/lib/batchedUpdates' 5 + import {Shadow, castAsShadow} from './types' 5 6 export type {Shadow} from './types' 6 7 7 8 const emitter = new EventEmitter() ··· 22 23 | AppBskyActorDefs.ProfileViewBasic 23 24 | AppBskyActorDefs.ProfileViewDetailed 24 25 25 - export function useProfileShadow( 26 - profile: ProfileView, 27 - ifAfterTS: number, 28 - ): Shadow<ProfileView> { 29 - const [state, setState] = useState<CacheEntry>({ 30 - ts: Date.now(), 26 + const firstSeenMap = new WeakMap<ProfileView, number>() 27 + function getFirstSeenTS(profile: ProfileView): number { 28 + let timeStamp = firstSeenMap.get(profile) 29 + if (timeStamp !== undefined) { 30 + return timeStamp 31 + } 32 + timeStamp = Date.now() 33 + firstSeenMap.set(profile, timeStamp) 34 + return timeStamp 35 + } 36 + 37 + export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> { 38 + const profileSeenTS = getFirstSeenTS(profile) 39 + const [state, setState] = useState<CacheEntry>(() => ({ 40 + ts: profileSeenTS, 31 41 value: fromProfile(profile), 32 - }) 33 - const firstRun = useRef(true) 42 + })) 43 + 44 + const [prevProfile, setPrevProfile] = useState(profile) 45 + if (profile !== prevProfile) { 46 + // if we got a new prop, assume it's fresher 47 + // than whatever shadow state we accumulated 48 + setPrevProfile(profile) 49 + setState({ 50 + ts: profileSeenTS, 51 + value: fromProfile(profile), 52 + }) 53 + } 34 54 35 55 const onUpdate = useCallback( 36 56 (value: Partial<ProfileShadow>) => { ··· 47 67 } 48 68 }, [profile.did, onUpdate]) 49 69 50 - // react to profile updates 51 - useEffect(() => { 52 - // dont fire on first run to avoid needless re-renders 53 - if (!firstRun.current) { 54 - setState({ts: Date.now(), value: fromProfile(profile)}) 55 - } 56 - firstRun.current = false 57 - }, [profile]) 58 - 59 70 return useMemo(() => { 60 - return state.ts > ifAfterTS 71 + return state.ts > profileSeenTS 61 72 ? mergeShadow(profile, state.value) 62 - : {...profile, isShadowed: true} 63 - }, [profile, state, ifAfterTS]) 73 + : castAsShadow(profile) 74 + }, [profile, state, profileSeenTS]) 64 75 } 65 76 66 77 export function updateProfileShadow( 67 78 uri: string, 68 79 value: Partial<ProfileShadow>, 69 80 ) { 70 - emitter.emit(uri, value) 71 - } 72 - 73 - export function isProfileShadowed<T extends ProfileView>( 74 - v: T | Shadow<T>, 75 - ): v is Shadow<T> { 76 - return 'isShadowed' in v && !!v.isShadowed 81 + batchedUpdates(() => { 82 + emitter.emit(uri, value) 83 + }) 77 84 } 78 85 79 86 function fromProfile(profile: ProfileView): ProfileShadow { ··· 88 95 profile: ProfileView, 89 96 shadow: ProfileShadow, 90 97 ): Shadow<ProfileView> { 91 - return { 98 + return castAsShadow({ 92 99 ...profile, 93 100 viewer: { 94 101 ...(profile.viewer || {}), ··· 96 103 muted: shadow.muted, 97 104 blocking: shadow.blockingUri, 98 105 }, 99 - isShadowed: true, 100 - } 106 + }) 101 107 }
+7 -1
src/state/cache/types.ts
··· 1 - export type Shadow<T> = T & {isShadowed: true} 1 + // This isn't a real property, but it prevents T being compatible with Shadow<T>. 2 + declare const shadowTag: unique symbol 3 + export type Shadow<T> = T & {[shadowTag]: true} 4 + 5 + export function castAsShadow<T>(value: T): Shadow<T> { 6 + return value as any as Shadow<T> 7 + }
+1 -3
src/view/com/auth/onboarding/RecommendedFollows.tsx
··· 24 24 const pal = usePalette('default') 25 25 const {_} = useLingui() 26 26 const {isTabletOrMobile} = useWebMediaQueries() 27 - const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery() 27 + const {data: suggestedFollows} = useSuggestedFollowsQuery() 28 28 const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor() 29 29 const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{ 30 30 [did: string]: AppBskyActorDefs.ProfileView[] ··· 162 162 renderItem={({item}) => ( 163 163 <RecommendedFollowsItem 164 164 profile={item} 165 - dataUpdatedAt={dataUpdatedAt} 166 165 onFollowStateChange={onFollowStateChange} 167 166 moderation={moderateProfile(item, moderationOpts)} 168 167 /> ··· 197 196 renderItem={({item}) => ( 198 197 <RecommendedFollowsItem 199 198 profile={item} 200 - dataUpdatedAt={dataUpdatedAt} 201 199 onFollowStateChange={onFollowStateChange} 202 200 moderation={moderateProfile(item, moderationOpts)} 203 201 />
+1 -3
src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
··· 18 18 19 19 type Props = { 20 20 profile: AppBskyActorDefs.ProfileViewBasic 21 - dataUpdatedAt: number 22 21 moderation: ProfileModeration 23 22 onFollowStateChange: (props: { 24 23 did: string ··· 28 27 29 28 export function RecommendedFollowsItem({ 30 29 profile, 31 - dataUpdatedAt, 32 30 moderation, 33 31 onFollowStateChange, 34 32 }: React.PropsWithChildren<Props>) { 35 33 const pal = usePalette('default') 36 34 const {isMobile} = useWebMediaQueries() 37 - const shadowedProfile = useProfileShadow(profile, dataUpdatedAt) 35 + const shadowedProfile = useProfileShadow(profile) 38 36 39 37 return ( 40 38 <Animated.View
-3
src/view/com/lists/ListMembers.tsx
··· 64 64 65 65 const { 66 66 data, 67 - dataUpdatedAt, 68 67 isFetching, 69 68 isFetched, 70 69 isError, ··· 185 184 (item as AppBskyGraphDefs.ListItemView).subject.handle 186 185 }`} 187 186 profile={(item as AppBskyGraphDefs.ListItemView).subject} 188 - dataUpdatedAt={dataUpdatedAt} 189 187 renderButton={renderMemberButton} 190 188 style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} 191 189 /> ··· 198 196 onPressTryAgain, 199 197 onPressRetryLoadMore, 200 198 isMobile, 201 - dataUpdatedAt, 202 199 ], 203 200 ) 204 201
+2 -11
src/view/com/modals/ProfilePreview.tsx
··· 22 22 const moderationOpts = useModerationOpts() 23 23 const { 24 24 data: profile, 25 - dataUpdatedAt, 26 25 error: profileError, 27 26 refetch: refetchProfile, 28 27 isFetching: isFetchingProfile, ··· 51 50 ) 52 51 } 53 52 if (profile && moderationOpts) { 54 - return ( 55 - <ComponentLoaded 56 - profile={profile} 57 - dataUpdatedAt={dataUpdatedAt} 58 - moderationOpts={moderationOpts} 59 - /> 60 - ) 53 + return <ComponentLoaded profile={profile} moderationOpts={moderationOpts} /> 61 54 } 62 55 // should never happen 63 56 return ( ··· 71 64 72 65 function ComponentLoaded({ 73 66 profile: profileUnshadowed, 74 - dataUpdatedAt, 75 67 moderationOpts, 76 68 }: { 77 69 profile: AppBskyActorDefs.ProfileViewDetailed 78 - dataUpdatedAt: number 79 70 moderationOpts: ModerationOpts 80 71 }) { 81 72 const pal = usePalette('default') 82 - const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt) 73 + const profile = useProfileShadow(profileUnshadowed) 83 74 const {screen} = useAnalytics() 84 75 const moderation = React.useMemo( 85 76 () => moderateProfile(profile, moderationOpts),
+2 -9
src/view/com/notifications/Feed.tsx
··· 38 38 const {markAllRead} = useUnreadNotificationsApi() 39 39 const { 40 40 data, 41 - dataUpdatedAt, 42 41 isLoading, 43 42 isFetching, 44 43 isFetched, ··· 132 131 } else if (item === LOADING_ITEM) { 133 132 return <NotificationFeedLoadingPlaceholder /> 134 133 } 135 - return ( 136 - <FeedItem 137 - item={item} 138 - dataUpdatedAt={dataUpdatedAt} 139 - moderationOpts={moderationOpts!} 140 - /> 141 - ) 134 + return <FeedItem item={item} moderationOpts={moderationOpts!} /> 142 135 }, 143 - [onPressRetryLoadMore, dataUpdatedAt, moderationOpts], 136 + [onPressRetryLoadMore, moderationOpts], 144 137 ) 145 138 146 139 const showHeaderSpinner = !isPTRing && isFetching && !isLoading
-3
src/view/com/notifications/FeedItem.tsx
··· 58 58 59 59 let FeedItem = ({ 60 60 item, 61 - dataUpdatedAt, 62 61 moderationOpts, 63 62 }: { 64 63 item: FeedNotification 65 - dataUpdatedAt: number 66 64 moderationOpts: ModerationOpts 67 65 }): React.ReactNode => { 68 66 const pal = usePalette('default') ··· 135 133 accessible={false}> 136 134 <Post 137 135 post={item.subject} 138 - dataUpdatedAt={dataUpdatedAt} 139 136 style={ 140 137 item.notification.isRead 141 138 ? undefined
+5 -13
src/view/com/post-thread/PostLikedBy.tsx
··· 20 20 } = useResolveUriQuery(uri) 21 21 const { 22 22 data, 23 - dataUpdatedAt, 24 23 isFetching, 25 24 isFetched, 26 25 isFetchingNextPage, ··· 55 54 } 56 55 }, [isFetching, hasNextPage, isError, fetchNextPage]) 57 56 58 - const renderItem = useCallback( 59 - ({item}: {item: GetLikes.Like}) => { 60 - return ( 61 - <ProfileCardWithFollowBtn 62 - key={item.actor.did} 63 - profile={item.actor} 64 - dataUpdatedAt={dataUpdatedAt} 65 - /> 66 - ) 67 - }, 68 - [dataUpdatedAt], 69 - ) 57 + const renderItem = useCallback(({item}: {item: GetLikes.Like}) => { 58 + return ( 59 + <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} /> 60 + ) 61 + }, []) 70 62 71 63 if (isFetchingResolvedUri || !isFetched) { 72 64 return (
+2 -9
src/view/com/post-thread/PostRepostedBy.tsx
··· 20 20 } = useResolveUriQuery(uri) 21 21 const { 22 22 data, 23 - dataUpdatedAt, 24 23 isFetching, 25 24 isFetched, 26 25 isFetchingNextPage, ··· 57 56 58 57 const renderItem = useCallback( 59 58 ({item}: {item: ActorDefs.ProfileViewBasic}) => { 60 - return ( 61 - <ProfileCardWithFollowBtn 62 - key={item.did} 63 - profile={item} 64 - dataUpdatedAt={dataUpdatedAt} 65 - /> 66 - ) 59 + return <ProfileCardWithFollowBtn key={item.did} profile={item} /> 67 60 }, 68 - [dataUpdatedAt], 61 + [], 69 62 ) 70 63 71 64 if (isFetchingResolvedUri || !isFetched) {
-6
src/view/com/post-thread/PostThread.tsx
··· 73 73 refetch, 74 74 isRefetching, 75 75 data: thread, 76 - dataUpdatedAt, 77 76 } = usePostThreadQuery(uri) 78 77 const {data: preferences} = usePreferencesQuery() 79 78 const rootPost = thread?.type === 'post' ? thread.post : undefined ··· 111 110 <PostThreadLoaded 112 111 thread={thread} 113 112 isRefetching={isRefetching} 114 - dataUpdatedAt={dataUpdatedAt} 115 113 threadViewPrefs={preferences.threadViewPrefs} 116 114 onRefresh={refetch} 117 115 onPressReply={onPressReply} ··· 122 120 function PostThreadLoaded({ 123 121 thread, 124 122 isRefetching, 125 - dataUpdatedAt, 126 123 threadViewPrefs, 127 124 onRefresh, 128 125 onPressReply, 129 126 }: { 130 127 thread: ThreadNode 131 128 isRefetching: boolean 132 - dataUpdatedAt: number 133 129 threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs'] 134 130 onRefresh: () => void 135 131 onPressReply: () => void ··· 295 291 <PostThreadItem 296 292 post={item.post} 297 293 record={item.record} 298 - dataUpdatedAt={dataUpdatedAt} 299 294 treeView={threadViewPrefs.lab_treeViewEnabled || false} 300 295 depth={item.ctx.depth} 301 296 isHighlightedPost={item.ctx.isHighlightedPost} ··· 322 317 posts, 323 318 onRefresh, 324 319 threadViewPrefs.lab_treeViewEnabled, 325 - dataUpdatedAt, 326 320 _, 327 321 ], 328 322 )
+1 -3
src/view/com/post-thread/PostThreadItem.tsx
··· 45 45 export function PostThreadItem({ 46 46 post, 47 47 record, 48 - dataUpdatedAt, 49 48 treeView, 50 49 depth, 51 50 isHighlightedPost, ··· 57 56 }: { 58 57 post: AppBskyFeedDefs.PostView 59 58 record: AppBskyFeedPost.Record 60 - dataUpdatedAt: number 61 59 treeView: boolean 62 60 depth: number 63 61 isHighlightedPost?: boolean ··· 68 66 onPostReply: () => void 69 67 }) { 70 68 const moderationOpts = useModerationOpts() 71 - const postShadowed = usePostShadow(post, dataUpdatedAt) 69 + const postShadowed = usePostShadow(post) 72 70 const richText = useMemo( 73 71 () => 74 72 new RichTextAPI({
+1 -3
src/view/com/post/Post.tsx
··· 30 30 31 31 export function Post({ 32 32 post, 33 - dataUpdatedAt, 34 33 showReplyLine, 35 34 style, 36 35 }: { 37 36 post: AppBskyFeedDefs.PostView 38 - dataUpdatedAt: number 39 37 showReplyLine?: boolean 40 38 style?: StyleProp<ViewStyle> 41 39 }) { ··· 48 46 : undefined, 49 47 [post], 50 48 ) 51 - const postShadowed = usePostShadow(post, dataUpdatedAt) 49 + const postShadowed = usePostShadow(post) 52 50 const richText = useMemo( 53 51 () => 54 52 record
-3
src/view/com/posts/Feed.tsx
··· 76 76 const opts = React.useMemo(() => ({enabled}), [enabled]) 77 77 const { 78 78 data, 79 - dataUpdatedAt, 80 79 isFetching, 81 80 isFetched, 82 81 isError, ··· 200 199 return ( 201 200 <FeedSlice 202 201 slice={item} 203 - dataUpdatedAt={dataUpdatedAt} 204 202 // we check for this before creating the feedItems array 205 203 moderationOpts={moderationOpts!} 206 204 /> ··· 208 206 }, 209 207 [ 210 208 feed, 211 - dataUpdatedAt, 212 209 error, 213 210 onPressTryAgain, 214 211 onPressRetryLoadMore,
+1 -3
src/view/com/posts/FeedItem.tsx
··· 40 40 record, 41 41 reason, 42 42 moderation, 43 - dataUpdatedAt, 44 43 isThreadChild, 45 44 isThreadLastChild, 46 45 isThreadParent, ··· 49 48 record: AppBskyFeedPost.Record 50 49 reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined 51 50 moderation: PostModeration 52 - dataUpdatedAt: number 53 51 isThreadChild?: boolean 54 52 isThreadLastChild?: boolean 55 53 isThreadParent?: boolean 56 54 }) { 57 - const postShadowed = usePostShadow(post, dataUpdatedAt) 55 + const postShadowed = usePostShadow(post) 58 56 const richText = useMemo( 59 57 () => 60 58 new RichTextAPI({
-6
src/view/com/posts/FeedSlice.tsx
··· 11 11 12 12 let FeedSlice = ({ 13 13 slice, 14 - dataUpdatedAt, 15 14 ignoreFilterFor, 16 15 moderationOpts, 17 16 }: { 18 17 slice: FeedPostSlice 19 - dataUpdatedAt: number 20 18 ignoreFilterFor?: string 21 19 moderationOpts: ModerationOpts 22 20 }): React.ReactNode => { ··· 44 42 record={slice.items[0].record} 45 43 reason={slice.items[0].reason} 46 44 moderation={moderations[0]} 47 - dataUpdatedAt={dataUpdatedAt} 48 45 isThreadParent={isThreadParentAt(slice.items, 0)} 49 46 isThreadChild={isThreadChildAt(slice.items, 0)} 50 47 /> ··· 54 51 record={slice.items[1].record} 55 52 reason={slice.items[1].reason} 56 53 moderation={moderations[1]} 57 - dataUpdatedAt={dataUpdatedAt} 58 54 isThreadParent={isThreadParentAt(slice.items, 1)} 59 55 isThreadChild={isThreadChildAt(slice.items, 1)} 60 56 /> ··· 65 61 record={slice.items[last].record} 66 62 reason={slice.items[last].reason} 67 63 moderation={moderations[last]} 68 - dataUpdatedAt={dataUpdatedAt} 69 64 isThreadParent={isThreadParentAt(slice.items, last)} 70 65 isThreadChild={isThreadChildAt(slice.items, last)} 71 66 isThreadLastChild ··· 83 78 record={slice.items[i].record} 84 79 reason={slice.items[i].reason} 85 80 moderation={moderations[i]} 86 - dataUpdatedAt={dataUpdatedAt} 87 81 isThreadParent={isThreadParentAt(slice.items, i)} 88 82 isThreadChild={isThreadChildAt(slice.items, i)} 89 83 isThreadLastChild={
+1 -6
src/view/com/profile/ProfileCard.tsx
··· 27 27 export function ProfileCard({ 28 28 testID, 29 29 profile: profileUnshadowed, 30 - dataUpdatedAt, 31 30 noBg, 32 31 noBorder, 33 32 followers, ··· 36 35 }: { 37 36 testID?: string 38 37 profile: AppBskyActorDefs.ProfileViewBasic 39 - dataUpdatedAt: number 40 38 noBg?: boolean 41 39 noBorder?: boolean 42 40 followers?: AppBskyActorDefs.ProfileView[] | undefined ··· 46 44 style?: StyleProp<ViewStyle> 47 45 }) { 48 46 const pal = usePalette('default') 49 - const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt) 47 + const profile = useProfileShadow(profileUnshadowed) 50 48 const moderationOpts = useModerationOpts() 51 49 if (!moderationOpts) { 52 50 return null ··· 202 200 noBg, 203 201 noBorder, 204 202 followers, 205 - dataUpdatedAt, 206 203 }: { 207 204 profile: AppBskyActorDefs.ProfileViewBasic 208 205 noBg?: boolean 209 206 noBorder?: boolean 210 207 followers?: AppBskyActorDefs.ProfileView[] | undefined 211 - dataUpdatedAt: number 212 208 }) { 213 209 const {currentAccount} = useSession() 214 210 const isMe = profile.did === currentAccount?.did ··· 224 220 ? undefined 225 221 : profileShadow => <FollowButton profile={profileShadow} /> 226 222 } 227 - dataUpdatedAt={dataUpdatedAt} 228 223 /> 229 224 ) 230 225 }
+2 -7
src/view/com/profile/ProfileFollowers.tsx
··· 20 20 } = useResolveDidQuery(name) 21 21 const { 22 22 data, 23 - dataUpdatedAt, 24 23 isFetching, 25 24 isFetched, 26 25 isFetchingNextPage, ··· 58 57 59 58 const renderItem = React.useCallback( 60 59 ({item}: {item: ActorDefs.ProfileViewBasic}) => ( 61 - <ProfileCardWithFollowBtn 62 - key={item.did} 63 - profile={item} 64 - dataUpdatedAt={dataUpdatedAt} 65 - /> 60 + <ProfileCardWithFollowBtn key={item.did} profile={item} /> 66 61 ), 67 - [dataUpdatedAt], 62 + [], 68 63 ) 69 64 70 65 if (isFetchingDid || !isFetched) {
+2 -7
src/view/com/profile/ProfileFollows.tsx
··· 20 20 } = useResolveDidQuery(name) 21 21 const { 22 22 data, 23 - dataUpdatedAt, 24 23 isFetching, 25 24 isFetched, 26 25 isFetchingNextPage, ··· 58 57 59 58 const renderItem = React.useCallback( 60 59 ({item}: {item: ActorDefs.ProfileViewBasic}) => ( 61 - <ProfileCardWithFollowBtn 62 - key={item.did} 63 - profile={item} 64 - dataUpdatedAt={dataUpdatedAt} 65 - /> 60 + <ProfileCardWithFollowBtn key={item.did} profile={item} /> 66 61 ), 67 - [dataUpdatedAt], 62 + [], 68 63 ) 69 64 70 65 if (isFetchingDid || !isFetched) {
+3 -9
src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
··· 65 65 } 66 66 }, [active, animatedHeight, track]) 67 67 68 - const {isLoading, data, dataUpdatedAt} = useSuggestedFollowsByActorQuery({ 68 + const {isLoading, data} = useSuggestedFollowsByActorQuery({ 69 69 did: actorDid, 70 70 }) 71 71 ··· 127 127 </> 128 128 ) : data ? ( 129 129 data.suggestions.map(profile => ( 130 - <SuggestedFollow 131 - key={profile.did} 132 - profile={profile} 133 - dataUpdatedAt={dataUpdatedAt} 134 - /> 130 + <SuggestedFollow key={profile.did} profile={profile} /> 135 131 )) 136 132 ) : ( 137 133 <View /> ··· 196 192 197 193 function SuggestedFollow({ 198 194 profile: profileUnshadowed, 199 - dataUpdatedAt, 200 195 }: { 201 196 profile: AppBskyActorDefs.ProfileView 202 - dataUpdatedAt: number 203 197 }) { 204 198 const {track} = useAnalytics() 205 199 const pal = usePalette('default') 206 200 const moderationOpts = useModerationOpts() 207 - const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt) 201 + const profile = useProfileShadow(profileUnshadowed) 208 202 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 209 203 210 204 const onPressFollow = React.useCallback(async () => {
-2
src/view/screens/ModerationBlockedAccounts.tsx
··· 40 40 const [isPTRing, setIsPTRing] = React.useState(false) 41 41 const { 42 42 data, 43 - dataUpdatedAt, 44 43 isFetching, 45 44 isError, 46 45 error, ··· 95 94 testID={`blockedAccount-${index}`} 96 95 key={item.did} 97 96 profile={item} 98 - dataUpdatedAt={dataUpdatedAt} 99 97 /> 100 98 ) 101 99 return (
-2
src/view/screens/ModerationMutedAccounts.tsx
··· 40 40 const [isPTRing, setIsPTRing] = React.useState(false) 41 41 const { 42 42 data, 43 - dataUpdatedAt, 44 43 isFetching, 45 44 isError, 46 45 error, ··· 95 94 testID={`mutedAccount-${index}`} 96 95 key={item.did} 97 96 profile={item} 98 - dataUpdatedAt={dataUpdatedAt} 99 97 /> 100 98 ) 101 99 return (
+1 -5
src/view/screens/Profile.tsx
··· 57 57 } = useResolveDidQuery(name) 58 58 const { 59 59 data: profile, 60 - dataUpdatedAt, 61 60 error: profileError, 62 61 refetch: refetchProfile, 63 62 isFetching: isFetchingProfile, ··· 100 99 return ( 101 100 <ProfileScreenLoaded 102 101 profile={profile} 103 - dataUpdatedAt={dataUpdatedAt} 104 102 moderationOpts={moderationOpts} 105 103 hideBackButton={!!route.params.hideBackButton} 106 104 /> ··· 125 123 126 124 function ProfileScreenLoaded({ 127 125 profile: profileUnshadowed, 128 - dataUpdatedAt, 129 126 moderationOpts, 130 127 hideBackButton, 131 128 }: { 132 129 profile: AppBskyActorDefs.ProfileViewDetailed 133 - dataUpdatedAt: number 134 130 moderationOpts: ModerationOpts 135 131 hideBackButton: boolean 136 132 }) { 137 - const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt) 133 + const profile = useProfileShadow(profileUnshadowed) 138 134 const {currentAccount} = useSession() 139 135 const setMinimalShellMode = useSetMinimalShellMode() 140 136 const {openComposer} = useComposerControls()
+5 -25
src/view/screens/Search/Search.tsx
··· 111 111 function SearchScreenSuggestedFollows() { 112 112 const pal = usePalette('default') 113 113 const {currentAccount} = useSession() 114 - const [dataUpdatedAt, setDataUpdatedAt] = React.useState(0) 115 114 const [suggestions, setSuggestions] = React.useState< 116 115 AppBskyActorDefs.ProfileViewBasic[] 117 116 >([]) ··· 141 140 ) 142 141 143 142 setSuggestions(Array.from(friendsOfFriends.values())) 144 - setDataUpdatedAt(Date.now()) 145 143 } 146 144 147 145 try { ··· 151 149 error: e, 152 150 }) 153 151 } 154 - }, [ 155 - currentAccount, 156 - setSuggestions, 157 - setDataUpdatedAt, 158 - getSuggestedFollowsByActor, 159 - ]) 152 + }, [currentAccount, setSuggestions, getSuggestedFollowsByActor]) 160 153 161 154 return suggestions.length ? ( 162 155 <FlatList 163 156 data={suggestions} 164 - renderItem={({item}) => ( 165 - <ProfileCardWithFollowBtn 166 - profile={item} 167 - noBg 168 - dataUpdatedAt={dataUpdatedAt} 169 - /> 170 - )} 157 + renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />} 171 158 keyExtractor={item => item.did} 172 159 // @ts-ignore web only -prf 173 160 desktopFixedHeight ··· 205 192 fetchNextPage, 206 193 isFetchingNextPage, 207 194 hasNextPage, 208 - dataUpdatedAt, 209 195 } = useSearchPostsQuery({query}) 210 196 211 197 const onPullToRefresh = React.useCallback(async () => { ··· 258 244 data={items} 259 245 renderItem={({item}) => { 260 246 if (item.type === 'post') { 261 - return <Post post={item.post} dataUpdatedAt={dataUpdatedAt} /> 247 + return <Post post={item.post} /> 262 248 } else { 263 249 return <Loader /> 264 250 } ··· 291 277 function SearchScreenUserResults({query}: {query: string}) { 292 278 const {_} = useLingui() 293 279 const [isFetched, setIsFetched] = React.useState(false) 294 - const [dataUpdatedAt, setDataUpdatedAt] = React.useState(0) 295 280 const [results, setResults] = React.useState< 296 281 AppBskyActorDefs.ProfileViewBasic[] 297 282 >([]) ··· 302 287 const searchResults = await search({query, limit: 30}) 303 288 304 289 if (searchResults) { 305 - setDataUpdatedAt(Date.now()) 306 290 setResults(results) 307 291 setIsFetched(true) 308 292 } ··· 314 298 setResults([]) 315 299 setIsFetched(false) 316 300 } 317 - }, [query, setDataUpdatedAt, search, results]) 301 + }, [query, search, results]) 318 302 319 303 return isFetched ? ( 320 304 <> ··· 322 306 <FlatList 323 307 data={results} 324 308 renderItem={({item}) => ( 325 - <ProfileCardWithFollowBtn 326 - profile={item} 327 - noBg 328 - dataUpdatedAt={dataUpdatedAt} 329 - /> 309 + <ProfileCardWithFollowBtn profile={item} noBg /> 330 310 )} 331 311 keyExtractor={item => item.did} 332 312 // @ts-ignore web only -prf