diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-04-03 03:21:15 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-02 17:21:15 -0700 |
commit | 87da619aaa92e0ec762e68c13b24e58a25da10a8 (patch) | |
tree | 4da902d3ca43a226f6da8e5c090ab33c2df3297a /src/screens/Search/components/StarterPackCard.tsx | |
parent | 8d1f97b5ffac5d86762f1d4e9384ff3097acbc52 (diff) | |
download | voidsky-87da619aaa92e0ec762e68c13b24e58a25da10a8.tar.zst |
[Explore] Base (#8053)
* migrate to #/screens * rm unneeded import * block drawer gesture on recent profiles * rm recommendations (#8056) * [Explore] Disable Trending videos (#8054) * remove giant header * disable * [Explore] Dynamic module ordering (#8066) * Dynamic module ordering * [Explore] New headers, metrics (#8067) * new sticky headers * improve spacing between modules * view metric on modules * update metrics names * [Explore] Suggested accounts module (#8072) * use modern profile card, update load more * add tab bar * tabbed suggested accounts * [Explore] Discover feeds module (#8073) * cap number of feeds to 3 * change feed pin button * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * restore statsig to log events * filter out followed profiles, make suer enough are loaded (#8090) * [Explore] Trending topics (#8055) * redesigned trending topics * rm borders on web * get post count / age / ranking from api * spacing tweaks * fetch more topics then slice * use api data for avis/category * rm top border * Integrate new SDK, part out components * Clean up * Use status field * Bump SDK * Send up interests and langs --------- Co-authored-by: Eric Bailey <git@esb.lol> * Clean up module spacing and borders (cherry picked from commit 63d19b6c2d67e226e0e14709b1047a1f88b3ce1c) (cherry picked from commit 62d7d394ab1dc31b40b9c2cf59075adbf94737a1) * Switch back border ordering (cherry picked from commit 34e3789f8b410132c1390df3c2bb8257630ebdd9) * [Explore] Starter Packs (#8095) * Temp WIP (cherry picked from commit 43b5d7b1e64b3adb1ed162262d0310e0bf026c18) * New SP card * Load state * Revert change * Cleanup * Interests and caching * Count total * Format * Caching * [Explore] Feed previews module (#8075) * wip new hook * get fetching working, maybe * get feed previews rendering! * fix header height * working pin button * extract out FeedLink * add loader * only make preview:header sticky * Fix headers * Header tweaks * Fix moderation filter * Fix threading --------- Co-authored-by: Eric Bailey <git@esb.lol> * Space it out * Fix query key * Mock new endpoint, filter saved feeds * Make sure we're pinning, lower cache time * add news category * Remove log * Improve suggested accounts load state * Integrate new app view endpoint * fragment * Update src/screens/Search/modules/ExploreTrendingTopics.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Search/modules/ExploreTrendingTopics.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * lint * maybe fix this --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/screens/Search/components/StarterPackCard.tsx')
-rw-r--r-- | src/screens/Search/components/StarterPackCard.tsx | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx new file mode 100644 index 000000000..9520dd5a7 --- /dev/null +++ b/src/screens/Search/components/StarterPackCard.tsx @@ -0,0 +1,296 @@ +import React from 'react' +import {View} from 'react-native' +import { + type AppBskyGraphDefs, + AppBskyGraphStarterpack, + moderateProfile, +} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {sanitizeHandle} from '#/lib/strings/handles' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useSession} from '#/state/session' +import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {ButtonText} from '#/components/Button' +import {PlusSmall_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.StarterPackView +}) { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const {gtPhone} = useBreakpoints() + const link = useStarterPackLink({view}) + + if ( + !bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( + view.record, + AppBskyGraphStarterpack.isRecord, + ) + ) { + return null + } + + const profileCount = gtPhone ? 11 : 8 + const profiles = view.listItemsSample + ?.slice(0, profileCount) + .map(item => item.subject) + + return ( + <View + style={[ + a.w_full, + a.p_lg, + a.gap_md, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ]}> + <View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}> + <Link + to={link.to} + label={link.label} + style={[a.absolute, a.inset_0]} + onHoverIn={link.precache} + onPress={link.precache}> + <View /> + </Link> + </View> + + <AvatarStack + profiles={profiles ?? []} + numPending={profileCount} + total={view.list?.listItemCount} + /> + + <View + style={[ + a.w_full, + a.flex_row, + a.align_start, + a.gap_lg, + web({ + position: 'static', + zIndex: 'unset', + }), + ]}> + <View style={[a.flex_1]}> + <Text + emoji + style={[a.text_md, a.font_bold, a.leading_snug]} + numberOfLines={1}> + {view.record.name} + </Text> + <Text + emoji + style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]} + numberOfLines={1}> + {view.creator?.did === currentAccount?.did + ? _(msg`By you`) + : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)} + </Text> + </View> + <Link + to={link.to} + label={link.label} + onHoverIn={link.precache} + onPress={link.precache} + variant="solid" + color="secondary" + size="small" + style={[a.z_50]}> + <ButtonText> + <Trans>Open pack</Trans> + </ButtonText> + </Link> + </View> + </View> + ) +} + +export function AvatarStack({ + profiles, + numPending, + total, +}: { + profiles: bsky.profile.AnyProfileView[] + numPending: number + total?: number +}) { + const t = useTheme() + const {gtPhone} = useBreakpoints() + const moderationOpts = useModerationOpts() + const computedTotal = (total ?? numPending) - numPending + const circlesCount = numPending + 1 // add total at end + const widthPerc = 100 / circlesCount + const [size, setSize] = React.useState<number | null>(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 ( + <View + style={[ + a.w_full, + a.flex_row, + a.align_center, + a.relative, + {width: `${100 - widthPerc * 0.2}%`}, + ]}> + {items.map((item, i) => ( + <View + key={item.key} + style={[ + { + width: `${widthPerc}%`, + zIndex: 100 - i, + }, + ]}> + <View + style={[ + a.relative, + { + width: '120%', + }, + ]}> + <View + onLayout={e => setSize(e.nativeEvent.layout.width)} + style={[ + a.rounded_full, + t.atoms.bg_contrast_25, + { + paddingTop: '100%', + }, + ]}> + {size && item.profile ? ( + <UserAvatar + size={size} + avatar={item.profile.avatar} + type={item.profile.associated?.labeler ? 'labeler' : 'user'} + moderation={item.moderation.ui('avatar')} + style={[a.absolute, a.inset_0]} + /> + ) : ( + <MediaInsetBorder style={[a.rounded_full]} /> + )} + </View> + </View> + </View> + ))} + <View + style={[ + { + width: `${widthPerc}%`, + zIndex: 1, + }, + ]}> + <View + style={[ + a.relative, + { + width: '120%', + }, + ]}> + <View + style={[ + { + paddingTop: '100%', + }, + ]}> + <View + style={[ + a.absolute, + a.inset_0, + a.rounded_full, + a.align_center, + a.justify_center, + { + backgroundColor: t.atoms.text_contrast_low.color, + }, + ]}> + {computedTotal > 0 ? ( + <Text + style={[ + gtPhone ? a.text_md : a.text_sm, + a.font_bold, + a.leading_snug, + {color: 'white'}, + ]}> + <Trans comment="Indicates the number of additional profiles are in the Starter Pack e.g. +12"> + +{computedTotal} + </Trans> + </Text> + ) : ( + <Plus fill="white" /> + )} + </View> + </View> + </View> + </View> + </View> + ) +} + +export function StarterPackCardSkeleton() { + const t = useTheme() + const {gtPhone} = useBreakpoints() + + const profileCount = gtPhone ? 11 : 8 + + return ( + <View + style={[ + a.w_full, + a.p_lg, + a.gap_md, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ]}> + <AvatarStack profiles={[]} numPending={profileCount} /> + + <View + style={[ + a.w_full, + a.flex_row, + a.align_start, + a.gap_lg, + web({ + position: 'static', + zIndex: 'unset', + }), + ]}> + <View style={[a.flex_1, a.gap_xs]}> + <LoadingPlaceholder width={180} height={18} /> + <LoadingPlaceholder width={120} height={14} /> + </View> + + <LoadingPlaceholder width={100} height={33} /> + </View> + </View> + ) +} |