Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 274 lines 5.6 kB view raw
1import {useCallback, useEffect} from 'react' 2import {type StyleProp, View, type ViewStyle} from 'react-native' 3import Animated, { 4 Easing, 5 interpolate, 6 type SharedValue, 7 useAnimatedStyle, 8 useSharedValue, 9 withDelay, 10 withTiming, 11} from 'react-native-reanimated' 12 13import {useSession} from '#/state/session' 14import {UserAvatar} from '#/view/com/util/UserAvatar' 15import {atoms as a, useTheme} from '#/alf' 16import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' 17import type * as bsky from '#/types/bsky' 18 19type Props = { 20 animate?: boolean 21 profiles: bsky.profile.AnyProfileView[] 22 size?: 'small' | 'medium' | 'large' | number 23} 24 25export function AvatarBubbles({ 26 animate = false, 27 profiles: allProfiles, 28 size = 'large', 29}: Props) { 30 const {currentAccount} = useSession() 31 const profiles = allProfiles.filter(p => p.did !== currentAccount?.did) 32 const containerSize = 33 typeof size === 'number' 34 ? size 35 : size === 'small' 36 ? 40 37 : size === 'medium' 38 ? 56 39 : 120 40 const scale = 41 typeof size === 'number' 42 ? size / 120 43 : size === 'small' 44 ? 40 / 120 45 : size === 'medium' 46 ? 56 / 120 47 : 1 48 const marginOffset = size === 'small' || size === 'medium' ? -2 : 0 49 50 const initialValue = animate ? 0 : 1 51 const p0 = useSharedValue(initialValue) 52 const p1 = useSharedValue(initialValue) 53 const p2 = useSharedValue(initialValue) 54 const p3 = useSharedValue(initialValue) 55 56 const animateScale = (p: Animated.SharedValue<number>, index: number) => { 57 p.set(0) 58 p.set(() => 59 withDelay( 60 500 + index * 100, 61 withTiming(1, { 62 duration: 250, 63 easing: Easing.out(Easing.back(1.75)), 64 }), 65 ), 66 ) 67 } 68 69 const playScaleAnimation = useCallback(() => { 70 animateScale(p0, 0) 71 animateScale(p1, 1) 72 animateScale(p2, 2) 73 animateScale(p3, 3) 74 }, [p0, p1, p2, p3]) 75 76 useEffect(() => { 77 if (!animate) return 78 playScaleAnimation() 79 }, [animate, playScaleAnimation]) 80 81 let avatars = ( 82 <> 83 <AvatarBubble 84 profile={profiles[0] ?? allProfiles[0]} 85 scale={p0} 86 size={76} 87 x={-2} 88 y={-2} 89 style={[a.z_20]} 90 includeProfileBorder 91 /> 92 <AvatarBubble 93 profile={profiles[1]} 94 scale={p1} 95 size={76} 96 x={42} 97 y={42} 98 style={[a.z_10]} 99 includeProfileBorder 100 /> 101 </> 102 ) 103 104 if (profiles.length === 3) { 105 avatars = ( 106 <> 107 <AvatarBubble 108 profile={profiles[0]} 109 scale={p0} 110 size={68} 111 x={-2} 112 y={-2} 113 /> 114 <AvatarBubble 115 profile={profiles[1]} 116 scale={p1} 117 size={56} 118 x={38} 119 y={62} 120 /> 121 <AvatarBubble 122 profile={profiles[2]} 123 scale={p2} 124 size={46} 125 x={71} 126 y={18} 127 /> 128 </> 129 ) 130 } 131 132 if (profiles.length >= 4) { 133 avatars = ( 134 <> 135 <AvatarBubble 136 profile={profiles[0]} 137 scale={p0} 138 size={68} 139 x={-2} 140 y={-2} 141 /> 142 <AvatarBubble 143 profile={profiles[1]} 144 scale={p1} 145 size={56} 146 x={60} 147 y={49} 148 /> 149 <AvatarBubble 150 profile={profiles[2]} 151 scale={p2} 152 size={42} 153 x={14} 154 y={74} 155 /> 156 <AvatarBubble profile={profiles[3]} scale={p3} size={32} x={72} y={9} /> 157 </> 158 ) 159 } 160 161 return ( 162 <Animated.View 163 style={[ 164 a.p_2xs, 165 { 166 height: containerSize, 167 width: containerSize, 168 }, 169 ]}> 170 <View 171 style={[ 172 { 173 marginTop: marginOffset, 174 marginLeft: marginOffset, 175 transform: [{scale}], 176 transformOrigin: 'top left', 177 }, 178 ]}> 179 {avatars} 180 </View> 181 </Animated.View> 182 ) 183} 184 185function AvatarBubble({ 186 profile, 187 scale, 188 size, 189 style, 190 x, 191 y, 192 includeProfileBorder, 193}: { 194 profile?: bsky.profile.AnyProfileView 195 scale: SharedValue<number> 196 size: number 197 style?: StyleProp<ViewStyle> 198 x: number 199 y: number 200 includeProfileBorder?: boolean 201}) { 202 const t = useTheme() 203 204 const animatedStyle = useAnimatedStyle(() => ({ 205 transform: [ 206 {translateX: x}, 207 {translateY: y}, 208 {scale: interpolate(scale.get(), [0, 1], [0, 1])}, 209 ], 210 })) 211 212 return ( 213 <Animated.View 214 style={[ 215 a.absolute, 216 a.rounded_full, 217 a.flex_grow_0, 218 includeProfileBorder && { 219 borderColor: t.atoms.text_inverted.color, 220 borderWidth: 2, 221 }, 222 style, 223 animatedStyle, 224 ]}> 225 {profile ? ( 226 <Avatar profile={profile} size={size} /> 227 ) : ( 228 <AvatarPlaceholder size={size} /> 229 )} 230 </Animated.View> 231 ) 232} 233 234function Avatar({ 235 profile, 236 size = 76, 237}: { 238 profile: bsky.profile.AnyProfileView 239 size?: number 240}) { 241 return ( 242 <UserAvatar 243 avatar={profile.avatar} 244 size={size} 245 type="user" 246 hideLiveBadge 247 noBorder 248 /> 249 ) 250} 251 252function AvatarPlaceholder({size = 76}: {size?: number}) { 253 const t = useTheme() 254 255 return ( 256 <View 257 style={[ 258 a.align_center, 259 a.justify_center, 260 a.rounded_full, 261 t.atoms.bg_contrast_200, 262 { 263 width: size, 264 height: size, 265 }, 266 ]}> 267 <PersonIcon 268 width={size * 0.5} 269 height={size * 0.5} 270 fill={t.atoms.text_inverted.color} 271 /> 272 </View> 273 ) 274}