From 101114e38477af6b010dac1c22b9c2c41f532a67 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 2 Apr 2025 15:27:24 -0500 Subject: [PATCH] New SP card --- src/components/AvatarStack.tsx | 14 +- .../StarterPack/StarterPackCard.tsx | 28 +- src/screens/Search/Explore.tsx | 27 +- .../Search/components/StarterPackCard.tsx | 268 +++++++++++++++--- .../queries/useSuggestedStarterPacksQuery.ts | 88 +----- 5 files changed, 286 insertions(+), 139 deletions(-) diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx index 63f5ed77a0..146f08d6dc 100644 --- a/src/components/AvatarStack.tsx +++ b/src/components/AvatarStack.tsx @@ -6,22 +6,24 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfilesQuery} from '#/state/queries/profile' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' -import * as bsky from '#/types/bsky' +import type * as bsky from '#/types/bsky' export function AvatarStack({ profiles, size = 26, numPending, backgroundColor, + spacing = 'normal', }: { profiles: bsky.profile.AnyProfileView[] size?: number numPending?: number backgroundColor?: string + spacing?: 'normal' | 'loose' }) { - const halfSize = size / 2 const t = useTheme() const moderationOpts = useModerationOpts() + const translateX = size / getSpacingValue(spacing) const isPending = (numPending && profiles.length === 0) || !moderationOpts @@ -43,7 +45,7 @@ export function AvatarStack({ a.flex_row, a.align_center, a.relative, - {width: size + (items.length - 1) * halfSize}, + {width: size + (items.length - 1) * translateX}, ]}> {items.map((item, i) => ( { + const rkey = new AtUri(view.uri).rkey + const {creator} = view + return {rkey, handleOrDid: creator.handle || creator.did} + }, [view]) + const precache = () => { + precacheResolvedUri(qc, view.creator.handle, view.creator.did) + precacheStarterPack(qc, view) + } + + return { + to: `/starter-pack/${handleOrDid}/${rkey}`, + label: AppBskyGraphStarterpack.isRecord(view.record) + ? _(msg`Navigate to ${view.record.name}`) + : _(msg`Navigate to starter pack`), + precache, + } +} + export function Link({ starterPack, children, diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 3f2a3cc9f2..d0bab40792 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -1,6 +1,10 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {View, type ViewabilityConfig, type ViewToken} from 'react-native' -import {type AppBskyActorDefs, type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' +import { + type AppBskyActorDefs, + type AppBskyFeedDefs, + type AppBskyGraphDefs, +} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -13,19 +17,20 @@ import {useActorSearchPaginated} from '#/state/queries/actor-search' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' +import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' import {useProgressGuide} from '#/state/shell/progress-guide' import {List} from '#/view/com/util/List' import { FeedFeedLoadingPlaceholder, ProfileCardFeedLoadingPlaceholder, } from '#/view/com/util/LoadingPlaceholder' +import {StarterPackCard} from '#/screens/Search/components/StarterPackCard' import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations' import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' -import {StarterPackCard} from '#/screens/Search/components/StarterPackCard' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {type Props as SVGIconProps} from '#/components/icons/common' @@ -40,7 +45,6 @@ import { SuggestedProfileCard, useLoadEnoughProfiles, } from './modules/ExploreSuggestedAccounts' -import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { const t = useTheme() @@ -156,9 +160,9 @@ type ExploreScreenItems = error: string } | { - type: 'starterPack', + type: 'starterPack' key: string - view: AppBskyGraphDefs.StarterPackViewBasic + view: AppBskyGraphDefs.StarterPackView } export function Explore({ @@ -465,7 +469,7 @@ export function Explore({ // error: cleanError(suggestedSPsError), // }) } else { - suggestedSPs.map(s => { + suggestedSPs.starterPacks.map(s => { i.push({ type: 'starterPack', key: s.uri, @@ -512,6 +516,9 @@ export function Explore({ gate, moderationOpts, hasPressedLoadMoreFeeds, + suggestedSPs, + isLoadingSuggestedSPs, + suggestedSPsError, ]) const renderItem = useCallback( @@ -593,13 +600,7 @@ export function Explore({ } case 'starterPack': { return ( - + ) diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx index bb39dc1553..0532a7b444 100644 --- a/src/screens/Search/components/StarterPackCard.tsx +++ b/src/screens/Search/components/StarterPackCard.tsx @@ -1,57 +1,253 @@ import React from 'react' import {View} from 'react-native' -import {AppBskyGraphDefs} from '@atproto/api' -import {Image} from 'expo-image' +import { + type AppBskyGraphDefs, + AppBskyGraphStarterpack, + moderateProfile, +} from '@atproto/api' +import {msg,Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import {getStarterPackOgCard} from '#/lib/strings/starter-pack' -import * as Card from '#/components/StarterPack/StarterPackCard' -import {atoms as a, useTheme, useBreakpoints} from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useSession} from '#/state/session' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {ButtonText} from '#/components/Button' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Link} from '#/components/Link' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard' +import {Text} from '#/components/Typography' +import * as bsky from '#/types/bsky' -export function StarterPackCard({view}: {view: AppBskyGraphDefs.StarterPackViewBasic}) { +export function StarterPackCard({ + view, +}: { + view: AppBskyGraphDefs.StarterPackView +}) { const t = useTheme() const {_} = useLingui() - const imageUri = getStarterPackOgCard(view) + const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() + const link = useStarterPackLink({view}) + + if ( + !bsky.dangerousIsType( + view.record, + AppBskyGraphStarterpack.isRecord, + ) + ) { + return null + } + + const profileCount = gtPhone ? 11 : 8 + const profiles = view.listItemsSample + ?.slice(0, profileCount) + .map(item => item.subject) return ( - - - - - - - - - - - + + + + + + + + + + + + {view.record.name} + + + {view.creator?.did === currentAccount?.did + ? _(msg`By you`) + : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)} + - + + + Open pack + + + + + ) +} + +export function AvatarStack({ + profiles, + numPending, + total, +}: { + profiles: bsky.profile.AnyProfileView[] + numPending: number + total?: number +}) { + const t = useTheme() + const {gtPhone} = useBreakpoints() + const moderationOpts = useModerationOpts() + const circlesCount = numPending + 1 // add total at end + const widthPerc = 100 / circlesCount + const [size, setSize] = React.useState(null) + + const isPending = (numPending && profiles.length === 0) || !moderationOpts + + const items = isPending + ? Array.from({length: numPending ?? circlesCount}).map((_, i) => ({ + key: i, + profile: null, + moderation: null, + })) + : profiles.map(item => ({ + key: item.did, + profile: item, + moderation: moderateProfile(item, moderationOpts), + })) + + return ( + + {items.map((item, i) => ( + + + setSize(e.nativeEvent.layout.width)} + style={[ + a.rounded_full, + t.atoms.bg_contrast_25, + { + paddingTop: '100%', + }, + ]}> + {size && item.profile ? ( + + ) : ( + + )} + + + + ))} + + + + + {total ? ( + + + +{total} + + + ) : ( + + )} + + + + ) } diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts index f40d511469..fab6bfe190 100644 --- a/src/state/queries/useSuggestedStarterPacksQuery.ts +++ b/src/state/queries/useSuggestedStarterPacksQuery.ts @@ -1,5 +1,4 @@ import {useQuery} from '@tanstack/react-query' -import {AppBskyGraphDefs} from '@atproto/api' import {useAgent} from '#/state/session' @@ -13,90 +12,9 @@ export function useSuggestedStarterPacksQuery() { return useQuery({ queryKey: createSuggestedStarterPacksQueryKey(), async queryFn() { - // // @ts-expect-error - // const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks() - // const {starterPacks} = data - const {starterPacks} = {starterPacks: [temp]} - return starterPacks as AppBskyGraphDefs.StarterPackViewBasic[] + // TODO interests + const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks() + return data }, }) } - -const temp = { - uri: 'at://did:plc:dpajgwmnecpdyjyqzjzm6bnb/app.bsky.graph.starterpack/3l3bhtqkjfo2m', - cid: 'bafyreifvtjkrvuq3qcyz32p6lacdxbwhcmux5amnhokfputfarlpuyqqhu', - record: { - $type: 'app.bsky.graph.starterpack', - createdAt: '2024-09-03T18:44:43.158Z', - feeds: [ - { - avatar: - 'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreieppnjpayqgjbyqnbeqzwj3pzqtiwbe5a4m6whzqnp3k7gpqs2gai@jpeg', - cid: 'bafyreigreonzn577vy6i4qh2so7aqfjztqrrj4jpssg2jczc27p6x4y6wi', - creator: { - associated: { - chat: { - allowIncoming: 'none', - }, - }, - avatar: - 'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2cmvl2jt4mgx3sppwe2it3fwolkrbtjrhcnwjk4jdijhsoze@jpeg', - createdAt: '2023-04-12T04:53:57.057Z', - description: - 'official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app', - did: 'did:plc:z72i7hdynmk6r22z27h6tvur', - displayName: 'Bluesky', - handle: 'bsky.app', - indexedAt: '2024-12-18T07:43:57.513Z', - labels: [ - { - cts: '2024-05-31T04:01:47.907Z', - src: 'did:plc:z3yk2cflhmn6vmzo3f5ixqh4', - uri: 'did:plc:z72i7hdynmk6r22z27h6tvur', - val: 'cornerstone', - ver: 1, - }, - ], - viewer: { - blockedBy: false, - muted: false, - }, - }, - description: - 'A mix of popular content from accounts you follow and content that your follows like.', - did: 'did:web:discover.bsky.app', - displayName: 'Popular With Friends', - indexedAt: '2023-05-19T23:19:21.076Z', - labels: [], - likeCount: 33616, - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', - viewer: {}, - }, - ], - list: 'at://did:plc:dpajgwmnecpdyjyqzjzm6bnb/app.bsky.graph.list/3l3bhtqgyrn2e', - name: "Tom Sawyeeeeeee's Starter Pack", - updatedAt: '2024-12-19T22:44:58.411Z', - }, - creator: { - did: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb', - handle: 'test.esb.lol', - displayName: 'e', - avatar: - 'https://cdn.bsky.app/img/avatar/plain/did:plc:dpajgwmnecpdyjyqzjzm6bnb/bafkreia6dx7fhoi6fxwfpgm7jrxijpqci7ap53wpilkpazojwvqlmgud2m@jpeg', - associated: { - chat: { - allowIncoming: 'all', - }, - }, - viewer: { - muted: false, - blockedBy: false, - }, - labels: [], - createdAt: '2023-07-17T15:57:52.014Z', - }, - joinedAllTimeCount: 1, - joinedWeekCount: 0, - labels: [], - indexedAt: '2024-12-19T22:45:06.322Z', -}