Change size (#4957)

zio/stable
Hailey 2024-08-21 19:35:34 -07:00 committed by GitHub
parent 6616a6467e
commit 61f0be705d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 170 additions and 90 deletions

View File

@ -9,14 +9,15 @@ import {
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {isBlockedOrBlocking} from 'lib/moderation/blocked-and-muted' import {isBlockedOrBlocking} from 'lib/moderation/blocked-and-muted'
import {isNative, isWeb} from 'platform/detection' import {isNative, isWeb} from 'platform/detection'
import {useListMembersQuery} from 'state/queries/list-members' import {useAllListMembersQuery} from 'state/queries/list-members'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import {List, ListRef} from 'view/com/util/List' import {List, ListRef} from 'view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types' import {SectionRef} from '#/screens/Profile/Sections/types'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {ListMaybePlaceholder} from '#/components/Lists' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {Default as ProfileCard} from '#/components/ProfileCard' import {Default as ProfileCard} from '#/components/ProfileCard'
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) {
@ -39,17 +40,20 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
ref, ref,
) { ) {
const t = useTheme() const t = useTheme()
const [initialHeaderHeight] = React.useState(headerHeight) const bottomBarOffset = useBottomBarOffset(200)
const bottomBarOffset = useBottomBarOffset(20) const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data, refetch, isError} = useListMembersQuery(listUri, 50) const {data, refetch, isError} = useAllListMembersQuery(listUri)
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
// The server returns these sorted by descending creation date, so we want to invert // The server returns these sorted by descending creation date, so we want to invert
const profiles = data?.pages
.flatMap(p => p.items.map(i => i.subject)) const profiles = data
.filter(p => !isBlockedOrBlocking(p) && !p.associated?.labeler) ?.filter(
p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler,
)
.map(p => p.subject)
.reverse() .reverse()
const isOwn = new AtUri(listUri).host === currentAccount?.did const isOwn = new AtUri(listUri).host === currentAccount?.did
@ -99,7 +103,11 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
if (!data) { if (!data) {
return ( return (
<View style={{marginTop: headerHeight, marginBottom: bottomBarOffset}}> <View
style={[
a.h_full_vh,
{marginTop: headerHeight, marginBottom: bottomBarOffset},
]}>
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={true} isLoading={true}
isError={isError} isError={isError}
@ -118,10 +126,13 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
ref={scrollElRef} ref={scrollElRef}
headerOffset={headerHeight} headerOffset={headerHeight}
ListFooterComponent={ ListFooterComponent={
<View style={[{height: initialHeaderHeight + bottomBarOffset}]} /> <ListFooter
style={{paddingBottom: bottomBarOffset, borderTopWidth: 0}}
/>
} }
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
desktopFixedHeight desktopFixedHeight
initialNumToRender={initialNumToRender}
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={async () => { onRefresh={async () => {
setIsPTRing(true) setIsPTRing(true)

View File

@ -7,6 +7,7 @@ import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
@ -42,6 +43,7 @@ export function WizardEditListDialog({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender()
const listRef = useRef<BottomSheetFlatListMethods>(null) const listRef = useRef<BottomSheetFlatListMethods>(null)
@ -148,6 +150,7 @@ export function WizardEditListDialog({
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag" keyboardDismissMode="on-drag"
removeClippedSubviews={true} removeClippedSubviews={true}
initialNumToRender={initialNumToRender}
/> />
</Dialog.Outer> </Dialog.Outer>
) )

View File

@ -12,7 +12,7 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {DISCOVER_FEED_URI} from 'lib/constants' import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {useSession} from 'state/session' import {useSession} from 'state/session'
@ -130,7 +130,8 @@ export function WizardProfileCard({
const isMe = profile.did === currentAccount?.did const isMe = profile.did === currentAccount?.did
const included = isMe || state.profiles.some(p => p.did === profile.did) const included = isMe || state.profiles.some(p => p.did === profile.did)
const disabled = isMe || (!included && state.profiles.length >= 49) const disabled =
isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName) ? sanitizeDisplayName(profile.displayName)

View File

@ -12,6 +12,7 @@ export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG
export const EMBED_SERVICE = 'https://embed.bsky.app' export const EMBED_SERVICE = 'https://embed.bsky.app'
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
export const STARTER_PACK_MAX_SIZE = 150
// HACK // HACK
// Yes, this is exactly what it looks like. It's a hard-coded constant // Yes, this is exactly what it looks like. It's a hard-coded constant

View File

@ -23,6 +23,7 @@ import {useProgressGuideControls} from '#/state/shell/progress-guide'
import {uploadBlob} from 'lib/api' import {uploadBlob} from 'lib/api'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {getAllListMembers} from 'state/queries/list-members'
import { import {
useActiveStarterPack, useActiveStarterPack,
useSetActiveStarterPack, useSetActiveStarterPack,
@ -73,18 +74,20 @@ export function StepFinished() {
starterPack: activeStarterPack.uri, starterPack: activeStarterPack.uri,
}) })
starterPack = spRes.data.starterPack starterPack = spRes.data.starterPack
if (starterPack.list) {
const listRes = await agent.app.bsky.graph.getList({
list: starterPack.list.uri,
limit: 50,
})
listItems = listRes.data.items
}
} catch (e) { } catch (e) {
logger.error('Failed to fetch starter pack', {safeMessage: e}) logger.error('Failed to fetch starter pack', {safeMessage: e})
// don't tell the user, just get them through onboarding. // don't tell the user, just get them through onboarding.
} }
try {
if (starterPack?.list) {
listItems = await getAllListMembers(agent, starterPack.list.uri)
}
} catch (e) {
logger.error('Failed to fetch starter pack list items', {
safeMessage: e,
})
// don't tell the user, just get them through onboarding.
}
} }
try { try {

View File

@ -4,6 +4,7 @@ import {
BskyAgent, BskyAgent,
} from '@atproto/api' } from '@atproto/api'
import {TID} from '@atproto/common-web' import {TID} from '@atproto/common-web'
import chunk from 'lodash.chunk'
import {until} from '#/lib/async/until' import {until} from '#/lib/async/until'
@ -29,10 +30,13 @@ export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
value: r, value: r,
})) }))
await agent.com.atproto.repo.applyWrites({ const chunks = chunk(followWrites, 50)
repo: session.did, for (const chunk of chunks) {
writes: followWrites, await agent.com.atproto.repo.applyWrites({
}) repo: session.did,
writes: chunk,
})
}
await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length) await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length)
const followUris = new Map() const followUris = new Map()

View File

@ -32,6 +32,7 @@ import {getStarterPackOgCard} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {updateProfileShadow} from 'state/cache/profile-shadow' import {updateProfileShadow} from 'state/cache/profile-shadow'
import {useModerationOpts} from 'state/preferences/moderation-opts' import {useModerationOpts} from 'state/preferences/moderation-opts'
import {getAllListMembers} from 'state/queries/list-members'
import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link' import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
import {useResolveDidQuery} from 'state/queries/resolve-uri' import {useResolveDidQuery} from 'state/queries/resolve-uri'
import {useShortenLink} from 'state/queries/shorten-link' import {useShortenLink} from 'state/queries/shorten-link'
@ -327,42 +328,52 @@ function Header({
setIsProcessing(true) setIsProcessing(true)
let listItems: AppBskyGraphDefs.ListItemView[] = []
try { try {
const list = await agent.app.bsky.graph.getList({ listItems = await getAllListMembers(agent, starterPack.list.uri)
list: starterPack.list.uri,
})
const dids = list.data.items
.filter(
li =>
li.subject.did !== currentAccount?.did &&
!isBlockedOrBlocking(li.subject) &&
!isMuted(li.subject) &&
!li.subject.viewer?.following,
)
.map(li => li.subject.did)
const followUris = await bulkWriteFollows(agent, dids)
batchedUpdates(() => {
for (let did of dids) {
updateProfileShadow(queryClient, did, {
followingUri: followUris.get(did),
})
}
})
logEvent('starterPack:followAll', {
logContext: 'StarterPackProfilesList',
starterPack: starterPack.uri,
count: dids.length,
})
captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`))
} catch (e) { } catch (e) {
Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
} finally {
setIsProcessing(false) setIsProcessing(false)
Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
logger.error('Failed to get list members for starter pack', {
safeMessage: e,
})
return
} }
const dids = listItems
.filter(
li =>
li.subject.did !== currentAccount?.did &&
!isBlockedOrBlocking(li.subject) &&
!isMuted(li.subject) &&
!li.subject.viewer?.following,
)
.map(li => li.subject.did)
let followUris: Map<string, string>
try {
followUris = await bulkWriteFollows(agent, dids)
} catch (e) {
setIsProcessing(false)
Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
logger.error('Failed to follow all accounts', {safeMessage: e})
}
setIsProcessing(false)
batchedUpdates(() => {
for (let did of dids) {
updateProfileShadow(queryClient, did, {
followingUri: followUris.get(did),
})
}
})
Toast.show(_(msg`All accounts have been followed!`))
captureAction(ProgressGuideAction.Follow, dids.length)
logEvent('starterPack:followAll', {
logContext: 'StarterPackProfilesList',
starterPack: starterPack.uri,
count: dids.length,
})
} }
if (!AppBskyGraphStarterpack.isRecord(record)) { if (!AppBskyGraphStarterpack.isRecord(record)) {

View File

@ -7,6 +7,7 @@ import {
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {STARTER_PACK_MAX_SIZE} from 'lib/constants'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@ -73,9 +74,10 @@ function reducer(state: State, action: Action): State {
updatedState = {...state, description: action.description} updatedState = {...state, description: action.description}
break break
case 'AddProfile': case 'AddProfile':
if (state.profiles.length >= 51) { if (state.profiles.length > STARTER_PACK_MAX_SIZE) {
Toast.show( Toast.show(
msg`You may only add up to 50 profiles`.message ?? '', msg`You may only add up to ${STARTER_PACK_MAX_SIZE} profiles`
.message ?? '',
'info', 'info',
) )
} else { } else {
@ -91,8 +93,8 @@ function reducer(state: State, action: Action): State {
} }
break break
case 'AddFeed': case 'AddFeed':
if (state.feeds.length >= 50) { if (state.feeds.length >= 3) {
Toast.show(msg`You may only add up to 50 feeds`.message ?? '', 'info') Toast.show(msg`You may only add up to 3 feeds`.message ?? '', 'info')
} else { } else {
updatedState = {...state, feeds: [...state.feeds, action.feed]} updatedState = {...state, feeds: [...state.feeds, action.feed]}
} }

View File

@ -20,7 +20,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {logger} from '#/logger' import {logger} from '#/logger'
import {HITSLOP_10} from 'lib/constants' import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from 'lib/constants'
import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name'
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
@ -33,7 +33,7 @@ import {
} from 'lib/strings/starter-pack' } from 'lib/strings/starter-pack'
import {isAndroid, isNative, isWeb} from 'platform/detection' import {isAndroid, isNative, isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts' import {useModerationOpts} from 'state/preferences/moderation-opts'
import {useListMembersQuery} from 'state/queries/list-members' import {useAllListMembersQuery} from 'state/queries/list-members'
import {useProfileQuery} from 'state/queries/profile' import {useProfileQuery} from 'state/queries/profile'
import { import {
useCreateStarterPackMutation, useCreateStarterPackMutation,
@ -78,11 +78,10 @@ export function Wizard({
const listUri = starterPack?.list?.uri const listUri = starterPack?.list?.uri
const { const {
data: profilesData, data: listItems,
isLoading: isLoadingProfiles, isLoading: isLoadingProfiles,
isError: isErrorProfiles, isError: isErrorProfiles,
} = useListMembersQuery(listUri, 50) } = useAllListMembersQuery(listUri)
const listItems = profilesData?.pages.flatMap(p => p.items)
const { const {
data: profile, data: profile,
@ -428,7 +427,8 @@ function Footer({
{items.length > minimumItems && ( {items.length > minimumItems && (
<View style={[a.absolute, {right: 14, top: 31}]}> <View style={[a.absolute, {right: 14, top: 31}]}>
<Text style={[a.font_bold]}> <Text style={[a.font_bold]}>
{items.length}/{state.currentStep === 'Profiles' ? 50 : 3} {items.length}/
{state.currentStep === 'Profiles' ? STARTER_PACK_MAX_SIZE : 3}
</Text> </Text>
</View> </View>
)} )}

View File

@ -1,9 +1,15 @@
import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api' import {
AppBskyActorDefs,
AppBskyGraphDefs,
AppBskyGraphGetList,
BskyAgent,
} from '@atproto/api'
import { import {
InfiniteData, InfiniteData,
QueryClient, QueryClient,
QueryKey, QueryKey,
useInfiniteQuery, useInfiniteQuery,
useQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
@ -14,6 +20,7 @@ type RQPageParam = string | undefined
const RQKEY_ROOT = 'list-members' const RQKEY_ROOT = 'list-members'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT, uri, 'all']
export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
const agent = useAgent() const agent = useAgent()
@ -40,6 +47,38 @@ export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) {
}) })
} }
export function useAllListMembersQuery(uri?: string) {
const agent = useAgent()
return useQuery({
staleTime: STALE.MINUTES.ONE,
queryKey: RQKEY_ALL(uri ?? ''),
queryFn: async () => {
return getAllListMembers(agent, uri!)
},
enabled: Boolean(uri),
})
}
export async function getAllListMembers(agent: BskyAgent, uri: string) {
let hasMore = true
let cursor: string | undefined
const listItems: AppBskyGraphDefs.ListItemView[] = []
// We want to cap this at 6 pages, just for anything weird happening with the api
let i = 0
while (hasMore && i < 6) {
const res = await agent.app.bsky.graph.getList({
list: uri,
limit: 50,
cursor,
})
listItems.push(...res.data.items)
hasMore = Boolean(res.data.cursor)
cursor = res.data.cursor
}
i++
return listItems
}
export async function invalidateListMembersQuery({ export async function invalidateListMembersQuery({
queryClient, queryClient,
uri, uri,

View File

@ -16,6 +16,7 @@ import {
useQuery, useQuery,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {until} from 'lib/async/until' import {until} from 'lib/async/until'
import {createStarterPackList} from 'lib/generate-starterpack' import {createStarterPackList} from 'lib/generate-starterpack'
@ -200,36 +201,40 @@ export function useEditStarterPackMutation({
i.subject.did !== agent.session?.did && i.subject.did !== agent.session?.did &&
!profiles.find(p => p.did === i.subject.did && p.did), !profiles.find(p => p.did === i.subject.did && p.did),
) )
if (removedItems.length !== 0) { if (removedItems.length !== 0) {
await agent.com.atproto.repo.applyWrites({ const chunks = chunk(removedItems, 50)
repo: agent.session!.did, for (const chunk of chunks) {
writes: removedItems.map(i => ({ await agent.com.atproto.repo.applyWrites({
$type: 'com.atproto.repo.applyWrites#delete', repo: agent.session!.did,
collection: 'app.bsky.graph.listitem', writes: chunk.map(i => ({
rkey: new AtUri(i.uri).rkey, $type: 'com.atproto.repo.applyWrites#delete',
})), collection: 'app.bsky.graph.listitem',
}) rkey: new AtUri(i.uri).rkey,
})),
})
}
} }
const addedProfiles = profiles.filter( const addedProfiles = profiles.filter(
p => !currentListItems.find(i => i.subject.did === p.did), p => !currentListItems.find(i => i.subject.did === p.did),
) )
if (addedProfiles.length > 0) { if (addedProfiles.length > 0) {
await agent.com.atproto.repo.applyWrites({ const chunks = chunk(addedProfiles, 50)
repo: agent.session!.did, for (const chunk of chunks) {
writes: addedProfiles.map(p => ({ await agent.com.atproto.repo.applyWrites({
$type: 'com.atproto.repo.applyWrites#create', repo: agent.session!.did,
collection: 'app.bsky.graph.listitem', writes: chunk.map(p => ({
value: { $type: 'com.atproto.repo.applyWrites#create',
$type: 'app.bsky.graph.listitem', collection: 'app.bsky.graph.listitem',
subject: p.did, value: {
list: currentStarterPack.list?.uri, $type: 'app.bsky.graph.listitem',
createdAt: new Date().toISOString(), subject: p.did,
}, list: currentStarterPack.list?.uri,
})), createdAt: new Date().toISOString(),
}) },
})),
})
}
} }
const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey