diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index a4c9d0b..77f3dc6 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -1,9 +1,9 @@ import { router } from 'expo-router'; import { useState } from 'react'; -import { FlatList, RefreshControl, StyleSheet, View } from 'react-native'; +import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import useSWR from 'swr'; -import { sortFeed, type FeedItemDTO } from '@musicshare/shared'; +import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared'; import { NowPlayingCard } from '@/components/NowPlayingCard'; import { Body, Button, Card, Title } from '@/components/ui'; import { api } from '@/lib/api'; @@ -15,10 +15,16 @@ import { colors, space } from '@/lib/theme'; export default function FeedScreen() { const { me } = useSession(); const { data: feed, mutate, isLoading } = useSWR('/feed'); + // 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(me ? `/users/${me.username}` : null); const [refreshing, setRefreshing] = useState(false); useRealtime(true, (msg) => { - if (msg.type !== 'now_playing' || msg.data.user.id === me?.id) return; + if (msg.type !== 'now_playing') return; + if (msg.data.user.id === me?.id) { + if (mine) mutateMine({ ...mine, nowPlaying: msg.data }, { revalidate: false }); + return; + } mutate( (current = []) => { const existing = current.find((n) => n.user.id === msg.data.user.id); @@ -42,7 +48,7 @@ export default function FeedScreen() { const refresh = async () => { setRefreshing(true); - await mutate().catch(() => {}); + await Promise.all([mutate(), mutateMine()]).catch(() => {}); setRefreshing(false); }; @@ -55,6 +61,12 @@ export default function FeedScreen() { refreshControl={} ListHeaderComponent={ + {mine?.nowPlaying && ( + + YOU + + + )} Friends listening {!me?.preferredService && ( @@ -89,4 +101,5 @@ const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: colors.bg }, list: { padding: space.lg, gap: space.md, paddingBottom: space.xl * 3 }, header: { gap: space.md, marginBottom: space.xs }, + sectionLabel: { color: colors.muted, fontSize: 12, fontWeight: '700', letterSpacing: 0.8 }, }); diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 6ff83e6..b310c0a 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -41,6 +41,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent); ou .muted { color: var(--muted); } .error { color: var(--danger); margin: 0; } .notice { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 0.75rem 1rem; margin: 0; } +.section-label { margin: 0; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); } .narrow { max-width: 420px; margin: 2rem auto; } .nav { @@ -109,6 +110,15 @@ input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent); ou .search-results li { padding: 0.35rem 0.4rem; } .result { display: flex; gap: 0.6rem; align-items: center; background: none; border: 0; color: inherit; font: inherit; cursor: pointer; width: 100%; text-align: left; padding: 0; } .result:hover { color: var(--accent); } +.recent { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; } +.recent-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.45rem 0; text-decoration: none; border-radius: 8px; } +a.recent-row:hover .recent-title { color: var(--accent); } +.recent li + li { border-top: 1px solid var(--border); } +.recent-text { display: flex; flex-direction: column; min-width: 0; flex: 1; } +.recent-text > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.recent-title { font-weight: 600; } +.recent-time { font-size: 0.8rem; white-space: nowrap; } +.artwork-sm.artwork-empty { display: grid; place-items: center; background: var(--surface-2); color: var(--muted); } .picked { display: flex; gap: 0.75rem; align-items: center; } .connections { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem; } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 687d377..d507eec 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import useSWR from 'swr'; -import { sortFeed, type FeedItemDTO } from '@musicshare/shared'; +import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared'; import { NowPlayingCard } from '@/components/NowPlayingCard'; import { UserSearch } from '@/components/UserSearch'; import { api, fetcher, useMe } from '@/lib/api'; @@ -18,9 +18,15 @@ export default function Home() { function Feed() { const { me } = useMe(); const { data: feed, mutate } = useSWR('/feed', fetcher); + // 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(me ? `/users/${me.username}` : null, fetcher); useRealtime(true, (msg) => { - if (msg.type !== 'now_playing' || msg.data.user.id === me?.id) return; + if (msg.type !== 'now_playing') return; + if (msg.data.user.id === me?.id) { + if (mine) mutateMine({ ...mine, nowPlaying: msg.data }, { revalidate: false }); + return; + } mutate( (current = []) => { const existing = current.find((n) => n.user.id === msg.data.user.id); @@ -47,6 +53,12 @@ function Feed() { return (
+ {mine?.nowPlaying && ( +
+

You

+ +
+ )}

Friends listening

{!me?.preferredService && (