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