Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Change size (#4957)

authored by

Hailey and committed by
GitHub
61f0be70 6616a646

+167 -87
+21 -10
src/components/StarterPack/Main/ProfilesList.tsx
··· 9 9 import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' 10 10 11 11 import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' 12 + import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 12 13 import {isBlockedOrBlocking} from 'lib/moderation/blocked-and-muted' 13 14 import {isNative, isWeb} from 'platform/detection' 14 - import {useListMembersQuery} from 'state/queries/list-members' 15 + import {useAllListMembersQuery} from 'state/queries/list-members' 15 16 import {useSession} from 'state/session' 16 17 import {List, ListRef} from 'view/com/util/List' 17 18 import {SectionRef} from '#/screens/Profile/Sections/types' 18 19 import {atoms as a, useTheme} from '#/alf' 19 - import {ListMaybePlaceholder} from '#/components/Lists' 20 + import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 20 21 import {Default as ProfileCard} from '#/components/ProfileCard' 21 22 22 23 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { ··· 39 40 ref, 40 41 ) { 41 42 const t = useTheme() 42 - const [initialHeaderHeight] = React.useState(headerHeight) 43 - const bottomBarOffset = useBottomBarOffset(20) 43 + const bottomBarOffset = useBottomBarOffset(200) 44 + const initialNumToRender = useInitialNumToRender() 44 45 const {currentAccount} = useSession() 45 - const {data, refetch, isError} = useListMembersQuery(listUri, 50) 46 + const {data, refetch, isError} = useAllListMembersQuery(listUri) 46 47 47 48 const [isPTRing, setIsPTRing] = React.useState(false) 48 49 49 50 // The server returns these sorted by descending creation date, so we want to invert 50 - const profiles = data?.pages 51 - .flatMap(p => p.items.map(i => i.subject)) 52 - .filter(p => !isBlockedOrBlocking(p) && !p.associated?.labeler) 51 + 52 + const profiles = data 53 + ?.filter( 54 + p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler, 55 + ) 56 + .map(p => p.subject) 53 57 .reverse() 54 58 const isOwn = new AtUri(listUri).host === currentAccount?.did 55 59 ··· 99 103 100 104 if (!data) { 101 105 return ( 102 - <View style={{marginTop: headerHeight, marginBottom: bottomBarOffset}}> 106 + <View 107 + style={[ 108 + a.h_full_vh, 109 + {marginTop: headerHeight, marginBottom: bottomBarOffset}, 110 + ]}> 103 111 <ListMaybePlaceholder 104 112 isLoading={true} 105 113 isError={isError} ··· 118 126 ref={scrollElRef} 119 127 headerOffset={headerHeight} 120 128 ListFooterComponent={ 121 - <View style={[{height: initialHeaderHeight + bottomBarOffset}]} /> 129 + <ListFooter 130 + style={{paddingBottom: bottomBarOffset, borderTopWidth: 0}} 131 + /> 122 132 } 123 133 showsVerticalScrollIndicator={false} 124 134 desktopFixedHeight 135 + initialNumToRender={initialNumToRender} 125 136 refreshing={isPTRing} 126 137 onRefresh={async () => { 127 138 setIsPTRing(true)
+3
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
··· 7 7 import {msg, Trans} from '@lingui/macro' 8 8 import {useLingui} from '@lingui/react' 9 9 10 + import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' 10 11 import {isWeb} from 'platform/detection' 11 12 import {useSession} from 'state/session' 12 13 import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' ··· 42 43 const {_} = useLingui() 43 44 const t = useTheme() 44 45 const {currentAccount} = useSession() 46 + const initialNumToRender = useInitialNumToRender() 45 47 46 48 const listRef = useRef<BottomSheetFlatListMethods>(null) 47 49 ··· 148 150 webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} 149 151 keyboardDismissMode="on-drag" 150 152 removeClippedSubviews={true} 153 + initialNumToRender={initialNumToRender} 151 154 /> 152 155 </Dialog.Outer> 153 156 )
+3 -2
src/components/StarterPack/Wizard/WizardListCard.tsx
··· 12 12 import {msg, Trans} from '@lingui/macro' 13 13 import {useLingui} from '@lingui/react' 14 14 15 - import {DISCOVER_FEED_URI} from 'lib/constants' 15 + import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants' 16 16 import {sanitizeDisplayName} from 'lib/strings/display-names' 17 17 import {sanitizeHandle} from 'lib/strings/handles' 18 18 import {useSession} from 'state/session' ··· 130 130 131 131 const isMe = profile.did === currentAccount?.did 132 132 const included = isMe || state.profiles.some(p => p.did === profile.did) 133 - const disabled = isMe || (!included && state.profiles.length >= 49) 133 + const disabled = 134 + isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1) 134 135 const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') 135 136 const displayName = profile.displayName 136 137 ? sanitizeDisplayName(profile.displayName)
+1
src/lib/constants.ts
··· 12 12 export const EMBED_SERVICE = 'https://embed.bsky.app' 13 13 export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` 14 14 export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' 15 + export const STARTER_PACK_MAX_SIZE = 150 15 16 16 17 // HACK 17 18 // Yes, this is exactly what it looks like. It's a hard-coded constant
+11 -8
src/screens/Onboarding/StepFinished.tsx
··· 23 23 import {uploadBlob} from 'lib/api' 24 24 import {useRequestNotificationsPermission} from 'lib/notifications/notifications' 25 25 import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' 26 + import {getAllListMembers} from 'state/queries/list-members' 26 27 import { 27 28 useActiveStarterPack, 28 29 useSetActiveStarterPack, ··· 73 74 starterPack: activeStarterPack.uri, 74 75 }) 75 76 starterPack = spRes.data.starterPack 76 - 77 - if (starterPack.list) { 78 - const listRes = await agent.app.bsky.graph.getList({ 79 - list: starterPack.list.uri, 80 - limit: 50, 81 - }) 82 - listItems = listRes.data.items 83 - } 84 77 } catch (e) { 85 78 logger.error('Failed to fetch starter pack', {safeMessage: e}) 79 + // don't tell the user, just get them through onboarding. 80 + } 81 + try { 82 + if (starterPack?.list) { 83 + listItems = await getAllListMembers(agent, starterPack.list.uri) 84 + } 85 + } catch (e) { 86 + logger.error('Failed to fetch starter pack list items', { 87 + safeMessage: e, 88 + }) 86 89 // don't tell the user, just get them through onboarding. 87 90 } 88 91 }
+8 -4
src/screens/Onboarding/util.ts
··· 4 4 BskyAgent, 5 5 } from '@atproto/api' 6 6 import {TID} from '@atproto/common-web' 7 + import chunk from 'lodash.chunk' 7 8 8 9 import {until} from '#/lib/async/until' 9 10 ··· 29 30 value: r, 30 31 })) 31 32 32 - await agent.com.atproto.repo.applyWrites({ 33 - repo: session.did, 34 - writes: followWrites, 35 - }) 33 + const chunks = chunk(followWrites, 50) 34 + for (const chunk of chunks) { 35 + await agent.com.atproto.repo.applyWrites({ 36 + repo: session.did, 37 + writes: chunk, 38 + }) 39 + } 36 40 await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length) 37 41 38 42 const followUris = new Map()
+40 -29
src/screens/StarterPack/StarterPackScreen.tsx
··· 32 32 import {isWeb} from 'platform/detection' 33 33 import {updateProfileShadow} from 'state/cache/profile-shadow' 34 34 import {useModerationOpts} from 'state/preferences/moderation-opts' 35 + import {getAllListMembers} from 'state/queries/list-members' 35 36 import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link' 36 37 import {useResolveDidQuery} from 'state/queries/resolve-uri' 37 38 import {useShortenLink} from 'state/queries/shorten-link' ··· 327 328 328 329 setIsProcessing(true) 329 330 331 + let listItems: AppBskyGraphDefs.ListItemView[] = [] 330 332 try { 331 - const list = await agent.app.bsky.graph.getList({ 332 - list: starterPack.list.uri, 333 + listItems = await getAllListMembers(agent, starterPack.list.uri) 334 + } catch (e) { 335 + setIsProcessing(false) 336 + Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark') 337 + logger.error('Failed to get list members for starter pack', { 338 + safeMessage: e, 333 339 }) 334 - const dids = list.data.items 335 - .filter( 336 - li => 337 - li.subject.did !== currentAccount?.did && 338 - !isBlockedOrBlocking(li.subject) && 339 - !isMuted(li.subject) && 340 - !li.subject.viewer?.following, 341 - ) 342 - .map(li => li.subject.did) 340 + return 341 + } 343 342 344 - const followUris = await bulkWriteFollows(agent, dids) 345 - 346 - batchedUpdates(() => { 347 - for (let did of dids) { 348 - updateProfileShadow(queryClient, did, { 349 - followingUri: followUris.get(did), 350 - }) 351 - } 352 - }) 343 + const dids = listItems 344 + .filter( 345 + li => 346 + li.subject.did !== currentAccount?.did && 347 + !isBlockedOrBlocking(li.subject) && 348 + !isMuted(li.subject) && 349 + !li.subject.viewer?.following, 350 + ) 351 + .map(li => li.subject.did) 353 352 354 - logEvent('starterPack:followAll', { 355 - logContext: 'StarterPackProfilesList', 356 - starterPack: starterPack.uri, 357 - count: dids.length, 358 - }) 359 - captureAction(ProgressGuideAction.Follow, dids.length) 360 - Toast.show(_(msg`All accounts have been followed!`)) 353 + let followUris: Map<string, string> 354 + try { 355 + followUris = await bulkWriteFollows(agent, dids) 361 356 } catch (e) { 362 - Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark') 363 - } finally { 364 357 setIsProcessing(false) 358 + Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark') 359 + logger.error('Failed to follow all accounts', {safeMessage: e}) 365 360 } 361 + 362 + setIsProcessing(false) 363 + batchedUpdates(() => { 364 + for (let did of dids) { 365 + updateProfileShadow(queryClient, did, { 366 + followingUri: followUris.get(did), 367 + }) 368 + } 369 + }) 370 + Toast.show(_(msg`All accounts have been followed!`)) 371 + captureAction(ProgressGuideAction.Follow, dids.length) 372 + logEvent('starterPack:followAll', { 373 + logContext: 'StarterPackProfilesList', 374 + starterPack: starterPack.uri, 375 + count: dids.length, 376 + }) 366 377 } 367 378 368 379 if (!AppBskyGraphStarterpack.isRecord(record)) {
+6 -4
src/screens/StarterPack/Wizard/State.tsx
··· 7 7 import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' 8 8 import {msg} from '@lingui/macro' 9 9 10 + import {STARTER_PACK_MAX_SIZE} from 'lib/constants' 10 11 import {useSession} from 'state/session' 11 12 import * as Toast from '#/view/com/util/Toast' 12 13 ··· 73 74 updatedState = {...state, description: action.description} 74 75 break 75 76 case 'AddProfile': 76 - if (state.profiles.length >= 51) { 77 + if (state.profiles.length > STARTER_PACK_MAX_SIZE) { 77 78 Toast.show( 78 - msg`You may only add up to 50 profiles`.message ?? '', 79 + msg`You may only add up to ${STARTER_PACK_MAX_SIZE} profiles` 80 + .message ?? '', 79 81 'info', 80 82 ) 81 83 } else { ··· 91 93 } 92 94 break 93 95 case 'AddFeed': 94 - if (state.feeds.length >= 50) { 95 - Toast.show(msg`You may only add up to 50 feeds`.message ?? '', 'info') 96 + if (state.feeds.length >= 3) { 97 + Toast.show(msg`You may only add up to 3 feeds`.message ?? '', 'info') 96 98 } else { 97 99 updatedState = {...state, feeds: [...state.feeds, action.feed]} 98 100 }
+6 -6
src/screens/StarterPack/Wizard/index.tsx
··· 20 20 import {NativeStackScreenProps} from '@react-navigation/native-stack' 21 21 22 22 import {logger} from '#/logger' 23 - import {HITSLOP_10} from 'lib/constants' 23 + import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from 'lib/constants' 24 24 import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name' 25 25 import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' 26 26 import {logEvent} from 'lib/statsig/statsig' ··· 33 33 } from 'lib/strings/starter-pack' 34 34 import {isAndroid, isNative, isWeb} from 'platform/detection' 35 35 import {useModerationOpts} from 'state/preferences/moderation-opts' 36 - import {useListMembersQuery} from 'state/queries/list-members' 36 + import {useAllListMembersQuery} from 'state/queries/list-members' 37 37 import {useProfileQuery} from 'state/queries/profile' 38 38 import { 39 39 useCreateStarterPackMutation, ··· 78 78 const listUri = starterPack?.list?.uri 79 79 80 80 const { 81 - data: profilesData, 81 + data: listItems, 82 82 isLoading: isLoadingProfiles, 83 83 isError: isErrorProfiles, 84 - } = useListMembersQuery(listUri, 50) 85 - const listItems = profilesData?.pages.flatMap(p => p.items) 84 + } = useAllListMembersQuery(listUri) 86 85 87 86 const { 88 87 data: profile, ··· 428 427 {items.length > minimumItems && ( 429 428 <View style={[a.absolute, {right: 14, top: 31}]}> 430 429 <Text style={[a.font_bold]}> 431 - {items.length}/{state.currentStep === 'Profiles' ? 50 : 3} 430 + {items.length}/ 431 + {state.currentStep === 'Profiles' ? STARTER_PACK_MAX_SIZE : 3} 432 432 </Text> 433 433 </View> 434 434 )}
+40 -1
src/state/queries/list-members.ts
··· 1 - import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api' 1 + import { 2 + AppBskyActorDefs, 3 + AppBskyGraphDefs, 4 + AppBskyGraphGetList, 5 + BskyAgent, 6 + } from '@atproto/api' 2 7 import { 3 8 InfiniteData, 4 9 QueryClient, 5 10 QueryKey, 6 11 useInfiniteQuery, 12 + useQuery, 7 13 } from '@tanstack/react-query' 8 14 9 15 import {STALE} from '#/state/queries' ··· 14 20 15 21 const RQKEY_ROOT = 'list-members' 16 22 export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] 23 + export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT, uri, 'all'] 17 24 18 25 export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { 19 26 const agent = useAgent() ··· 38 45 getNextPageParam: lastPage => lastPage.cursor, 39 46 enabled: Boolean(uri), 40 47 }) 48 + } 49 + 50 + export function useAllListMembersQuery(uri?: string) { 51 + const agent = useAgent() 52 + return useQuery({ 53 + staleTime: STALE.MINUTES.ONE, 54 + queryKey: RQKEY_ALL(uri ?? ''), 55 + queryFn: async () => { 56 + return getAllListMembers(agent, uri!) 57 + }, 58 + enabled: Boolean(uri), 59 + }) 60 + } 61 + 62 + export async function getAllListMembers(agent: BskyAgent, uri: string) { 63 + let hasMore = true 64 + let cursor: string | undefined 65 + const listItems: AppBskyGraphDefs.ListItemView[] = [] 66 + // We want to cap this at 6 pages, just for anything weird happening with the api 67 + let i = 0 68 + while (hasMore && i < 6) { 69 + const res = await agent.app.bsky.graph.getList({ 70 + list: uri, 71 + limit: 50, 72 + cursor, 73 + }) 74 + listItems.push(...res.data.items) 75 + hasMore = Boolean(res.data.cursor) 76 + cursor = res.data.cursor 77 + } 78 + i++ 79 + return listItems 41 80 } 42 81 43 82 export async function invalidateListMembersQuery({
+28 -23
src/state/queries/starter-packs.ts
··· 16 16 useQuery, 17 17 useQueryClient, 18 18 } from '@tanstack/react-query' 19 + import chunk from 'lodash.chunk' 19 20 20 21 import {until} from 'lib/async/until' 21 22 import {createStarterPackList} from 'lib/generate-starterpack' ··· 200 201 i.subject.did !== agent.session?.did && 201 202 !profiles.find(p => p.did === i.subject.did && p.did), 202 203 ) 203 - 204 204 if (removedItems.length !== 0) { 205 - await agent.com.atproto.repo.applyWrites({ 206 - repo: agent.session!.did, 207 - writes: removedItems.map(i => ({ 208 - $type: 'com.atproto.repo.applyWrites#delete', 209 - collection: 'app.bsky.graph.listitem', 210 - rkey: new AtUri(i.uri).rkey, 211 - })), 212 - }) 205 + const chunks = chunk(removedItems, 50) 206 + for (const chunk of chunks) { 207 + await agent.com.atproto.repo.applyWrites({ 208 + repo: agent.session!.did, 209 + writes: chunk.map(i => ({ 210 + $type: 'com.atproto.repo.applyWrites#delete', 211 + collection: 'app.bsky.graph.listitem', 212 + rkey: new AtUri(i.uri).rkey, 213 + })), 214 + }) 215 + } 213 216 } 214 217 215 218 const addedProfiles = profiles.filter( 216 219 p => !currentListItems.find(i => i.subject.did === p.did), 217 220 ) 218 - 219 221 if (addedProfiles.length > 0) { 220 - await agent.com.atproto.repo.applyWrites({ 221 - repo: agent.session!.did, 222 - writes: addedProfiles.map(p => ({ 223 - $type: 'com.atproto.repo.applyWrites#create', 224 - collection: 'app.bsky.graph.listitem', 225 - value: { 226 - $type: 'app.bsky.graph.listitem', 227 - subject: p.did, 228 - list: currentStarterPack.list?.uri, 229 - createdAt: new Date().toISOString(), 230 - }, 231 - })), 232 - }) 222 + const chunks = chunk(addedProfiles, 50) 223 + for (const chunk of chunks) { 224 + await agent.com.atproto.repo.applyWrites({ 225 + repo: agent.session!.did, 226 + writes: chunk.map(p => ({ 227 + $type: 'com.atproto.repo.applyWrites#create', 228 + collection: 'app.bsky.graph.listitem', 229 + value: { 230 + $type: 'app.bsky.graph.listitem', 231 + subject: p.did, 232 + list: currentStarterPack.list?.uri, 233 + createdAt: new Date().toISOString(), 234 + }, 235 + })), 236 + }) 237 + } 233 238 } 234 239 235 240 const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey