Files
musicshare/apps/mobile/src/components/Avatar.tsx
T
chrisandClaude Opus 5.5 3568b71e90 Mobile: redesign, onboarding, QR codes, logo, Spotify setup
- Redesign matching the web: album-art backdrops, Reanimated equalizer,
  spinning live ring, drifting ambient background, gradient buttons with
  spring press feedback and haptics, Space Grotesk headings, animated feed
  (enter + layout transitions); fix invisible tab labels
- Four-step onboarding for new accounts: which app you listen with (sets
  where songs open), the matching share setup, find friends (search,
  scan, invite), and a featured song. Skippable; stored server-side
- QR codes: My code (share link) and an in-app scanner (camera only, no
  microphone permission)
- Spotify setup via Last.fm or ListenBrainz with guided linking; Android
  shares Spotify automatically through the phone reader
- New logo as app icon, adaptive/monochrome icon and splash
- app.config.ts allows cleartext only for local http:// test builds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-05 19:16:40 -04:00

49 lines
2.2 KiB
TypeScript

import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
import type { UserSummaryDTO } from '@musicshare/shared';
import { colors, fonts } from '@/lib/theme';
/** `live` wraps the avatar in a spinning gradient ring (someone is listening right now). */
export function Avatar({ user, size = 40, live }: { user: UserSummaryDTO; size?: number; live?: boolean }) {
const rotation = useSharedValue(0);
useEffect(() => {
if (live) rotation.set(withRepeat(withTiming(360, { duration: 3500, easing: Easing.linear }), -1, false));
else cancelAnimation(rotation);
return () => cancelAnimation(rotation);
}, [live, rotation]);
const spin = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotation.value}deg` }] }));
const shape = { width: size, height: size, borderRadius: size / 2 };
const face = user.avatarUrl ? (
<Image source={user.avatarUrl} style={shape} contentFit="cover" />
) : (
<LinearGradient colors={['#4a1278', '#1872cf']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={[styles.empty, shape]}>
<Text style={[styles.letter, { fontSize: size * 0.45 }]}>{(user.displayName ?? user.username).slice(0, 1).toUpperCase()}</Text>
</LinearGradient>
);
if (!live) return face;
const ring = size + 6;
return (
<View style={{ width: ring, height: ring, alignItems: 'center', justifyContent: 'center' }}>
<Animated.View style={[StyleSheet.absoluteFill, spin]}>
<LinearGradient
colors={['#7b2ff7', '#2f8cff', colors.live, '#7b2ff7']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{ width: ring, height: ring, borderRadius: ring / 2 }}
/>
</Animated.View>
<View style={{ borderRadius: (size + 2) / 2, borderWidth: 2, borderColor: colors.bg }}>{face}</View>
</View>
);
}
const styles = StyleSheet.create({
empty: { alignItems: 'center', justifyContent: 'center' },
letter: { color: '#fff', fontFamily: fonts.display },
});