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:
@@ -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},
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user