import {useEffect} from 'react' import {View} from 'react-native' import Animated, { Easing, type SharedValue, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated' import {moderateProfile} from '#/lib/moderation/subjects' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' import type * as bsky from '#/types/bsky' type Layout = { size: number x: number y: number zIndex?: number border?: boolean } export function AvatarBubbles({ animate = false, profiles: allProfiles, self = false, size = 120, count, }: { animate?: boolean profiles: bsky.profile.AnyProfileView[] /** * By default, when there are more than 2 profiles, the current user is * filtered out (so you don't see yourself among your own group's members). * Set this to `true` for cases where every passed profile should appear, * e.g. an invite preview where the owner is meaningful regardless of viewer. */ self?: boolean size?: number /** * The true number of members, used to decide how many bubbles to render when * it exceeds the number of `profiles` we have on hand (e.g. an invite preview * that only carries a few member profiles for a much larger group). Slots * without a profile render as placeholders. Defaults to `profiles.length`. */ count?: number }) { const {currentAccount} = useSession() const profiles = !self && allProfiles.length > 2 ? allProfiles.filter(p => p.did !== currentAccount?.did) : allProfiles const bubbleCount = Math.max(profiles.length, count ?? 0) const scale = size / 120 const marginOffset = size < 120 ? -2 : 0 const initialValue = animate ? 0 : 1 const p0 = useSharedValue(initialValue) const p1 = useSharedValue(initialValue) const p2 = useSharedValue(initialValue) const p3 = useSharedValue(initialValue) useEffect(() => { if (!animate) return const animateBubble = (p: SharedValue, i: number) => { p.set(0) p.set(() => withDelay( 500 + i * 100, withTiming(1, { duration: 250, easing: Easing.out(Easing.back(1.75)), }), ), ) } animateBubble(p0, 0) animateBubble(p1, 1) animateBubble(p2, 2) animateBubble(p3, 3) }, [animate, p0, p1, p2, p3]) const scales = [p0, p1, p2, p3] const layouts = getLayouts(bubbleCount) return ( {layouts.map((layout, i) => ( ))} ) } function AvatarBubble({ profile: profileUnshadowed, scale, size, x, y, zIndex, includeProfileBorder, }: { profile?: bsky.profile.AnyProfileView scale: SharedValue size: number x: number y: number zIndex?: number includeProfileBorder?: boolean }) { const t = useTheme() const animatedStyle = useAnimatedStyle(() => ({ transform: [{translateX: x}, {translateY: y}, {scale: scale.get()}], })) const profile = useMaybeProfileShadow(profileUnshadowed) const moderationOpts = useModerationOpts() return ( {profile && moderationOpts ? ( ) : ( )} ) } function AvatarPlaceholder({size}: {size: number}) { const t = useTheme() return ( ) } function getLayouts(count: number): Layout[] { if (count === 3) { return [ {size: 68, x: -2, y: -2}, {size: 56, x: 38, y: 62}, {size: 46, x: 71, y: 18}, ] } if (count >= 4) { return [ {size: 68, x: -2, y: -2}, {size: 56, x: 60, y: 49}, {size: 42, x: 14, y: 74}, {size: 32, x: 72, y: 9}, ] } return [ {size: 76, x: -2, y: -2, zIndex: 20, border: true}, {size: 76, x: 42, y: 42, zIndex: 10, border: true}, ] }