The feed only listed people who had played something, so following someone who hadn't connected a service yet looked like it did nothing. Under the feed, a "Following" row now shows everyone else you follow, linking to their profiles, and the empty message says why it's empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { RefreshControl, StyleSheet, Text, View } from 'react-native';
|
import { Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
import Animated, { FadeInDown, LinearTransition } from 'react-native-reanimated';
|
import Animated, { FadeInDown, LinearTransition } from 'react-native-reanimated';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { sortFeed, type ConnectionDTO, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
|
import { sortFeed, type ConnectionDTO, type FeedItemDTO, type FollowListDTO, type ProfileDTO } from '@musicshare/shared';
|
||||||
import { AmbientBackground } from '@/components/AmbientBackground';
|
import { AmbientBackground } from '@/components/AmbientBackground';
|
||||||
|
import { Avatar } from '@/components/Avatar';
|
||||||
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
||||||
import { RelinkReminder } from '@/components/RelinkReminder';
|
import { RelinkReminder } from '@/components/RelinkReminder';
|
||||||
import { SharingControls } from '@/components/SharingControls';
|
import { SharingControls } from '@/components/SharingControls';
|
||||||
@@ -24,6 +25,10 @@ export default function FeedScreen() {
|
|||||||
const { data: feed, mutate, isLoading } = useSWR<FeedItemDTO[]>('/feed');
|
const { data: feed, mutate, isLoading } = useSWR<FeedItemDTO[]>('/feed');
|
||||||
// Your own profile, for the "You" card. Loading it also marks you as watched, so your updates arrive quickly.
|
// Your own profile, for the "You" card. Loading it also marks you as watched, so your updates arrive quickly.
|
||||||
const { data: mine, mutate: mutateMine } = useSWR<ProfileDTO>(me ? `/users/${me.username}` : null);
|
const { data: mine, mutate: mutateMine } = useSWR<ProfileDTO>(me ? `/users/${me.username}` : null);
|
||||||
|
// Everyone you follow; the feed only has the ones who've played something.
|
||||||
|
const { data: following, mutate: mutateFollowing } = useSWR<FollowListDTO>(me ? `/users/${me.username}/following` : null);
|
||||||
|
const inFeed = new Set(feed?.map((n) => n.user.id));
|
||||||
|
const quiet = following?.users.filter((u) => !inFeed.has(u.id)) ?? [];
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const reader = useReader();
|
const reader = useReader();
|
||||||
@@ -67,7 +72,7 @@ export default function FeedScreen() {
|
|||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
await Promise.all([mutate(), mutateMine()]).catch(() => {});
|
await Promise.all([mutate(), mutateMine(), mutateFollowing()]).catch(() => {});
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,11 +120,37 @@ export default function FeedScreen() {
|
|||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
isLoading ? null : (
|
isLoading ? null : (
|
||||||
<Card>
|
<Card>
|
||||||
<Body muted>Nothing yet. Follow some friends and their music will show up here live.</Body>
|
<Body muted>
|
||||||
|
{following?.users.length
|
||||||
|
? 'Nobody you follow has played anything yet. Their music will show up here live.'
|
||||||
|
: 'Nothing yet. Follow some friends and their music will show up here live.'}
|
||||||
|
</Body>
|
||||||
<Button small variant="primary" title="Find friends" onPress={() => router.push('/search')} style={{ alignSelf: 'flex-start' }} />
|
<Button small variant="primary" title="Find friends" onPress={() => router.push('/search')} style={{ alignSelf: 'flex-start' }} />
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ListFooterComponent={
|
||||||
|
quiet.length > 0 ? (
|
||||||
|
<View style={{ gap: space.sm, marginTop: space.md }}>
|
||||||
|
<Text style={styles.sectionLabel}>FOLLOWING · NOTHING PLAYED YET</Text>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.quiet}>
|
||||||
|
{quiet.map((u) => (
|
||||||
|
<Pressable key={u.id} onPress={() => router.push(`/u/${u.username}`)} style={styles.quietPerson} accessibilityLabel={u.displayName ?? u.username}>
|
||||||
|
<Avatar user={u} size={52} />
|
||||||
|
<Text style={styles.quietName} numberOfLines={1}>
|
||||||
|
{u.displayName ?? u.username}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
{following?.nextCursor && me && (
|
||||||
|
<Pressable onPress={() => router.push(`/u/${me.username}/following`)} style={[styles.quietPerson, { justifyContent: 'center' }]}>
|
||||||
|
<Text style={[styles.quietName, { color: colors.muted }]}>See all</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<Animated.View entering={FadeInDown.delay(Math.min(index, 8) * 60).springify().damping(16)}>
|
<Animated.View entering={FadeInDown.delay(Math.min(index, 8) * 60).springify().damping(16)}>
|
||||||
<NowPlayingCard
|
<NowPlayingCard
|
||||||
@@ -140,4 +171,7 @@ const styles = StyleSheet.create({
|
|||||||
list: { padding: space.lg, gap: space.md, paddingBottom: space.xl * 3 },
|
list: { padding: space.lg, gap: space.md, paddingBottom: space.xl * 3 },
|
||||||
header: { gap: space.md, marginBottom: space.xs },
|
header: { gap: space.md, marginBottom: space.xs },
|
||||||
sectionLabel: { color: colors.muted, fontSize: 12, fontWeight: '700', letterSpacing: 0.8 },
|
sectionLabel: { color: colors.muted, fontSize: 12, fontWeight: '700', letterSpacing: 0.8 },
|
||||||
|
quiet: { gap: space.lg, paddingVertical: space.xs },
|
||||||
|
quietPerson: { width: 64, alignItems: 'center', gap: 4 },
|
||||||
|
quietName: { color: colors.text, fontSize: 12, textAlign: 'center' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ input:focus, textarea:focus, select:focus {
|
|||||||
.list-row.danger .list-label { color: var(--danger); }
|
.list-row.danger .list-label { color: var(--danger); }
|
||||||
.list-panel { padding: 0.25rem 0.4rem 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
.list-panel { padding: 0.25rem 0.4rem 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||||
.list-message { margin: 0.5rem 0 0; }
|
.list-message { margin: 0.5rem 0 0; }
|
||||||
|
.quiet-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr)); gap: 0.75rem; }
|
||||||
|
.quiet-person { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; text-decoration: none; font-size: 0.82rem; text-align: center; min-width: 0; }
|
||||||
|
.quiet-person span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.quiet-person:hover span { text-decoration: underline; }
|
||||||
|
.quiet-more { justify-content: center; color: var(--muted); }
|
||||||
.avatar-picker { display: flex; gap: 1rem; align-items: center; }
|
.avatar-picker { display: flex; gap: 1rem; align-items: center; }
|
||||||
.avatar-picker-actions { display: flex; flex-direction: column; gap: 0.4rem; }
|
.avatar-picker-actions { display: flex; flex-direction: column; gap: 0.4rem; }
|
||||||
.grow { flex: 1; min-width: 0; }
|
.grow { flex: 1; min-width: 0; }
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { AnimatePresence, motion } from 'motion/react';
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
|
import { sortFeed, type FeedItemDTO, type FollowListDTO, type ProfileDTO } from '@musicshare/shared';
|
||||||
|
import { Avatar } from '@/components/Avatar';
|
||||||
import { Equalizer } from '@/components/Equalizer';
|
import { Equalizer } from '@/components/Equalizer';
|
||||||
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
||||||
import { SharingControls } from '@/components/SharingControls';
|
import { SharingControls } from '@/components/SharingControls';
|
||||||
@@ -23,6 +24,10 @@ function Feed() {
|
|||||||
const { data: feed, mutate } = useSWR<FeedItemDTO[]>('/feed', fetcher);
|
const { data: feed, mutate } = useSWR<FeedItemDTO[]>('/feed', fetcher);
|
||||||
// Your own profile, for the "You" card. Loading it also marks you as watched, so your updates arrive quickly.
|
// Your own profile, for the "You" card. Loading it also marks you as watched, so your updates arrive quickly.
|
||||||
const { data: mine, mutate: mutateMine } = useSWR<ProfileDTO>(me ? `/users/${me.username}` : null, fetcher);
|
const { data: mine, mutate: mutateMine } = useSWR<ProfileDTO>(me ? `/users/${me.username}` : null, fetcher);
|
||||||
|
// Everyone you follow; the feed only has the ones who've played something.
|
||||||
|
const { data: following } = useSWR<FollowListDTO>(me ? `/users/${me.username}/following` : null, fetcher);
|
||||||
|
const inFeed = new Set(feed?.map((n) => n.user.id));
|
||||||
|
const quiet = following?.users.filter((u) => !inFeed.has(u.id)) ?? [];
|
||||||
|
|
||||||
useRealtime(true, (msg) => {
|
useRealtime(true, (msg) => {
|
||||||
if (msg.type !== 'now_playing') return;
|
if (msg.type !== 'now_playing') return;
|
||||||
@@ -69,9 +74,12 @@ function Feed() {
|
|||||||
Pick your streaming service in <Link href="/settings">Settings</Link> so songs open in the right app.
|
Pick your streaming service in <Link href="/settings">Settings</Link> so songs open in the right app.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{feed?.length === 0 && (
|
{feed?.length === 0 &&
|
||||||
<p className="muted">Nothing yet. Follow some friends and their music will show up here live.</p>
|
(following?.users.length ? (
|
||||||
)}
|
<p className="muted">Nobody you follow has played anything yet. Their music will show up here live.</p>
|
||||||
|
) : (
|
||||||
|
<p className="muted">Nothing yet. Find friends above, and their music will show up here live.</p>
|
||||||
|
))}
|
||||||
{/* Cards rise in on load and glide to new positions when someone pins or starts playing. */}
|
{/* Cards rise in on load and glide to new positions when someone pins or starts playing. */}
|
||||||
<AnimatePresence initial={true}>
|
<AnimatePresence initial={true}>
|
||||||
{feed?.map((np, i) => (
|
{feed?.map((np, i) => (
|
||||||
@@ -92,6 +100,25 @@ function Feed() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{quiet.length > 0 && (
|
||||||
|
<section className="stack">
|
||||||
|
<h2 className="section-label">Following · nothing played yet</h2>
|
||||||
|
<div className="card quiet-list">
|
||||||
|
{quiet.map((u) => (
|
||||||
|
<Link key={u.id} href={`/u/${u.username}`} className="quiet-person" title={`@${u.username}`}>
|
||||||
|
<Avatar user={u} size={44} />
|
||||||
|
<span>{u.displayName ?? u.username}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{following?.nextCursor && me && (
|
||||||
|
<Link href={`/u/${me.username}/following`} className="quiet-person quiet-more">
|
||||||
|
See all
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user