A play is recorded when a new track starts (pausing and resuming doesn't count; returning after 10+ minutes does). Profiles show the last 10, excluding the track playing now; 50 are kept per user. Recent plays are free on every plan; the paid flag is renamed to fullHistory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import { router } from 'expo-router';
|
|
import { useState } from 'react';
|
|
import { StyleSheet, Text, View } from 'react-native';
|
|
import useSWR from 'swr';
|
|
import type { ProfileDTO } from '@musicshare/shared';
|
|
import { api } from '@/lib/api';
|
|
import { showError } from '@/lib/errors';
|
|
import { useRealtime } from '@/lib/useRealtime';
|
|
import { useSession } from '@/lib/session';
|
|
import { colors, space } from '@/lib/theme';
|
|
import { Avatar } from './Avatar';
|
|
import { NowPlayingCard } from './NowPlayingCard';
|
|
import { RecentPlays } from './RecentPlays';
|
|
import { Screen } from './Screen';
|
|
import { TrackCard } from './TrackCard';
|
|
import { Body, Button, Card, Heading, Row, Title } from './ui';
|
|
|
|
export function ProfileView({ username, edges }: { username: string; edges?: ('top' | 'bottom')[] }) {
|
|
const { me } = useSession();
|
|
const { data: profile, error, mutate } = useSWR<ProfileDTO>(`/users/${username}`);
|
|
const [busy, setBusy] = useState(false);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
useRealtime(true, (msg) => {
|
|
if (msg.type === 'now_playing' && profile && msg.data.user.id === profile.user.id) {
|
|
// A new track moves the previous one into "Recently played", so refetch; otherwise patch in place.
|
|
const newTrack = msg.data.track.id !== profile.nowPlaying?.track.id;
|
|
mutate({ ...profile, nowPlaying: msg.data }, { revalidate: newTrack });
|
|
}
|
|
});
|
|
|
|
const act = async (fn: () => Promise<unknown>) => {
|
|
setBusy(true);
|
|
try {
|
|
await fn();
|
|
await mutate();
|
|
} catch (err) {
|
|
showError(err);
|
|
}
|
|
setBusy(false);
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<Screen edges={edges}>
|
|
<Body muted>User not found.</Body>
|
|
</Screen>
|
|
);
|
|
}
|
|
if (!profile) return <Screen edges={edges} />;
|
|
const { user } = profile;
|
|
|
|
return (
|
|
<Screen
|
|
edges={edges}
|
|
refreshing={refreshing}
|
|
onRefresh={async () => {
|
|
setRefreshing(true);
|
|
await mutate().catch(() => {});
|
|
setRefreshing(false);
|
|
}}>
|
|
<View style={styles.header}>
|
|
<Avatar user={user} size={72} />
|
|
<View style={{ flex: 1, gap: 2 }}>
|
|
<Title numberOfLines={1}>{user.displayName ?? user.username}</Title>
|
|
<Text style={styles.meta}>
|
|
@{user.username} · {profile.followerCount} followers · {profile.followingCount} following
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
{user.bio && <Body>{user.bio}</Body>}
|
|
|
|
<Row>
|
|
{profile.isSelf ? (
|
|
<Button small title="Edit profile" onPress={() => router.push('/settings')} />
|
|
) : (
|
|
<>
|
|
<Button
|
|
small
|
|
variant={profile.isFollowing ? 'secondary' : 'primary'}
|
|
title={profile.isFollowing ? 'Following' : 'Follow'}
|
|
disabled={busy}
|
|
onPress={() => act(() => (profile.isFollowing ? api.unfollow(username) : api.follow(username)))}
|
|
/>
|
|
{profile.isFollowing && (
|
|
<Button
|
|
small
|
|
title={profile.isPinned ? '★ Pinned' : '☆ Pin to top'}
|
|
disabled={busy}
|
|
onPress={() => act(() => (profile.isPinned ? api.unpin(username) : api.pin(username)))}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</Row>
|
|
|
|
{profile.featured && (
|
|
<Card>
|
|
<Heading>Featured song</Heading>
|
|
<TrackCard track={profile.featured.track} preferred={me?.preferredService} />
|
|
{profile.featured.comment && <Text style={styles.quote}>“{profile.featured.comment}”</Text>}
|
|
</Card>
|
|
)}
|
|
|
|
{profile.nowPlaying ? (
|
|
<NowPlayingCard np={profile.nowPlaying} preferred={me?.preferredService} showUser={false} />
|
|
) : (
|
|
<Body muted>Nothing playing right now.</Body>
|
|
)}
|
|
|
|
<RecentPlays plays={profile.recent} preferred={me?.preferredService} />
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
header: { flexDirection: 'row', alignItems: 'center', gap: space.lg },
|
|
meta: { color: colors.muted, fontSize: 13 },
|
|
quote: {
|
|
color: colors.text,
|
|
fontStyle: 'italic',
|
|
borderLeftWidth: 3,
|
|
borderLeftColor: colors.accent,
|
|
paddingLeft: space.md,
|
|
fontSize: 15,
|
|
},
|
|
});
|