Group Clops Feature Branch (#10360)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-04-24 13:38:50 -05:00
committed by GitHub
parent e832791367
commit cdb8d4bfb8
63 changed files with 3517 additions and 1844 deletions
+87 -177
View File
@@ -1,8 +1,7 @@
import {useCallback, useEffect} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {useEffect} from 'react'
import {View} from 'react-native'
import Animated, {
Easing,
interpolate,
type SharedValue,
useAnimatedStyle,
useSharedValue,
@@ -16,44 +15,32 @@ 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
}
type Props = {
animate?: boolean
profiles: bsky.profile.AnyProfileView[]
size?: 'small' | 'medium' | 'large' | number
size?: number
}
export function AvatarBubbles({
animate = false,
profiles: allProfiles,
size = 'large',
size = 120,
}: Props) {
const {currentAccount} = useSession()
const profiles =
allProfiles.length > 2
? allProfiles.filter(p => p.did !== currentAccount?.did)
: allProfiles
const containerSize =
typeof size === 'number'
? size
: size === 'small'
? 40
: size === 'medium'
? 56
: 120
const scale =
typeof size === 'number'
? size / 120
: size === 'small'
? 40 / 120
: size === 'medium'
? 56 / 120
: 1
const marginOffset =
(typeof size === 'number' && size < 120) ||
size === 'small' ||
size === 'medium'
? -2
: 0
const scale = profiles.length <= 1 ? 1 : size / 120
const marginOffset = size < 120 ? -2 : 0
const initialValue = animate ? 0 : 1
const p0 = useSharedValue(initialValue)
@@ -61,130 +48,50 @@ export function AvatarBubbles({
const p2 = useSharedValue(initialValue)
const p3 = useSharedValue(initialValue)
const animateScale = (p: Animated.SharedValue<number>, index: number) => {
p.set(0)
p.set(() =>
withDelay(
500 + index * 100,
withTiming(1, {
duration: 250,
easing: Easing.out(Easing.back(1.75)),
}),
),
)
}
const playScaleAnimation = useCallback(() => {
animateScale(p0, 0)
animateScale(p1, 1)
animateScale(p2, 2)
animateScale(p3, 3)
}, [p0, p1, p2, p3])
useEffect(() => {
if (!animate) return
playScaleAnimation()
}, [animate, playScaleAnimation])
const animateBubble = (p: SharedValue<number>, 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])
let avatars = (
<>
<AvatarBubble
profile={profiles[0]}
scale={p0}
size={76}
x={-2}
y={-2}
style={[a.z_20]}
includeProfileBorder
/>
<AvatarBubble
profile={profiles[1]}
scale={p1}
size={76}
x={42}
y={42}
style={[a.z_10]}
includeProfileBorder
/>
</>
)
if (profiles.length === 3) {
avatars = (
<>
<AvatarBubble
profile={profiles[0]}
scale={p0}
size={68}
x={-2}
y={-2}
/>
<AvatarBubble
profile={profiles[1]}
scale={p1}
size={56}
x={38}
y={62}
/>
<AvatarBubble
profile={profiles[2]}
scale={p2}
size={46}
x={71}
y={18}
/>
</>
)
}
if (profiles.length >= 4) {
avatars = (
<>
<AvatarBubble
profile={profiles[0]}
scale={p0}
size={68}
x={-2}
y={-2}
/>
<AvatarBubble
profile={profiles[1]}
scale={p1}
size={56}
x={60}
y={49}
/>
<AvatarBubble
profile={profiles[2]}
scale={p2}
size={42}
x={14}
y={74}
/>
<AvatarBubble profile={profiles[3]} scale={p3} size={32} x={72} y={9} />
</>
)
}
const scales = [p0, p1, p2, p3]
const layouts = getLayouts(profiles.length)
return (
<Animated.View
style={[
a.p_2xs,
{
height: containerSize,
width: containerSize,
},
]}>
<Animated.View style={[a.p_2xs, {height: size, width: size}]}>
<View
style={[
{
marginTop: marginOffset,
marginLeft: marginOffset,
transform: [{scale}],
transformOrigin: 'top left',
},
]}>
{avatars}
style={{
marginTop: marginOffset,
marginLeft: marginOffset,
transform: [{scale}],
transformOrigin: 'top left',
}}>
{layouts.map((layout, i) => (
<AvatarBubble
key={i}
profile={profiles[i]}
scale={scales[i]}
size={layout.size}
x={layout.x}
y={layout.y}
zIndex={layout.zIndex}
includeProfileBorder={layout.border}
/>
))}
</View>
</Animated.View>
)
@@ -194,27 +101,23 @@ function AvatarBubble({
profile,
scale,
size,
style,
x,
y,
zIndex,
includeProfileBorder,
}: {
profile?: bsky.profile.AnyProfileView
scale: SharedValue<number>
size: number
style?: StyleProp<ViewStyle>
x: number
y: number
zIndex?: number
includeProfileBorder?: boolean
}) {
const t = useTheme()
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{translateX: x},
{translateY: y},
{scale: interpolate(scale.get(), [0, 1], [0, 1])},
],
transform: [{translateX: x}, {translateY: y}, {scale: scale.get()}],
}))
return (
@@ -227,11 +130,17 @@ function AvatarBubble({
borderColor: t.atoms.text_inverted.color,
borderWidth: 2,
},
style,
zIndex != null && {zIndex},
animatedStyle,
]}>
{profile ? (
<Avatar profile={profile} size={size} />
<UserAvatar
avatar={profile.avatar}
size={size}
type="user"
hideLiveBadge
noBorder
/>
) : (
<AvatarPlaceholder size={size} />
)}
@@ -239,25 +148,7 @@ function AvatarBubble({
)
}
function Avatar({
profile,
size = 76,
}: {
profile: bsky.profile.AnyProfileView
size?: number
}) {
return (
<UserAvatar
avatar={profile.avatar}
size={size}
type="user"
hideLiveBadge
noBorder
/>
)
}
function AvatarPlaceholder({size = 76}: {size?: number}) {
function AvatarPlaceholder({size}: {size: number}) {
const t = useTheme()
return (
@@ -267,10 +158,7 @@ function AvatarPlaceholder({size = 76}: {size?: number}) {
a.justify_center,
a.rounded_full,
t.atoms.bg_contrast_200,
{
width: size,
height: size,
},
{width: size, height: size},
]}>
<PersonIcon
width={size * 0.5}
@@ -280,3 +168,25 @@ function AvatarPlaceholder({size = 76}: {size?: number}) {
</View>
)
}
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},
]
}