diff --git a/apps/mobile/src/app/(tabs)/search.tsx b/apps/mobile/src/app/(tabs)/search.tsx index 6b0c2ac..7f7e0a3 100644 --- a/apps/mobile/src/app/(tabs)/search.tsx +++ b/apps/mobile/src/app/(tabs)/search.tsx @@ -1,37 +1,56 @@ -import { router } from 'expo-router'; -import { useState } from 'react'; +import { router, useFocusEffect } from 'expo-router'; +import { useCallback, useState } from 'react'; import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import useSWR from 'swr'; -import type { UserSummaryDTO } from '@musicshare/shared'; +import type { FollowListItemDTO, UserSummaryDTO } from '@musicshare/shared'; import { AmbientBackground } from '@/components/AmbientBackground'; import { Avatar } from '@/components/Avatar'; -import { Body, Button, Field, Row, Title } from '@/components/ui'; +import { PersonRow, emptyFollowText, useFollowList } from '@/components/FollowList'; +import { Body, Button, Chip, Field, Row, Title } from '@/components/ui'; +import { useSession } from '@/lib/session'; import { useDebounced } from '@/lib/useDebounced'; import { colors, space } from '@/lib/theme'; -export default function SearchScreen() { +/** Friends: who you follow and who follows you, plus search and QR codes to find new people. */ +export default function FriendsScreen() { + const { me } = useSession(); const [q, setQ] = useState(''); + const [tab, setTab] = useState<'following' | 'followers'>('following'); const query = useDebounced(q.trim(), 250); - const { data } = useSWR(query.length >= 2 ? `/users/search?q=${encodeURIComponent(query)}` : null); + const searching = query.length >= 2; + const { data: found } = useSWR(searching ? `/users/search?q=${encodeURIComponent(query)}` : null); + const list = useFollowList(me?.username, tab); + + // Follows change on profiles and elsewhere; catch up when coming back to this tab. + const { refresh } = list; + useFocusEffect( + useCallback(() => { + refresh(); + }, [refresh]), + ); + + // One list for both, so the search field keeps focus while results swap in. + const rows: (UserSummaryDTO | FollowListItemDTO)[] = searching ? (found ?? []) : list.users; return ( = 2 ? (data ?? []) : []} + data={rows} keyExtractor={(u) => u.id} keyboardShouldPersistTaps="handled" contentContainerStyle={styles.list} + onEndReached={searching ? undefined : list.loadMore} ListHeaderComponent={ - Find friends + Friends + ))} + + + + ); +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index e78b650..bf88425 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -8,7 +8,6 @@ import { Avatar } from '@/components/Avatar'; import { Equalizer } from '@/components/Equalizer'; import { NowPlayingCard } from '@/components/NowPlayingCard'; import { SharingControls } from '@/components/SharingControls'; -import { UserSearch } from '@/components/UserSearch'; import { api, fetcher, useMe } from '@/lib/api'; import { useRealtime } from '@/lib/useRealtime'; @@ -60,7 +59,6 @@ function Feed() { return (
- {mine?.nowPlaying && (
@@ -78,7 +76,7 @@ function Feed() { (following?.users.length ? (

Nobody you follow has played anything yet. Their music will show up here live.

) : ( -

Nothing yet. Find friends above, and their music will show up here live.

+

Nothing yet. Find friends, and their music will show up here live.

))} {/* Cards rise in on load and glide to new positions when someone pins or starts playing. */} diff --git a/apps/web/src/components/Nav.tsx b/apps/web/src/components/Nav.tsx index b93c8f2..b80eaee 100644 --- a/apps/web/src/components/Nav.tsx +++ b/apps/web/src/components/Nav.tsx @@ -5,13 +5,14 @@ import { usePathname } from 'next/navigation'; import { useState } from 'react'; import { client, useMe } from '@/lib/api'; -/** Top bar: logo and the three main places. On phones those move to a bottom tab bar. */ +/** Top bar: logo and the four main places. On phones those move to a bottom tab bar. */ export function Nav() { const { me } = useMe(); const path = usePathname(); const tabs = me ? [ { href: '/', label: 'Feed', icon: , active: path === '/' }, + { href: '/friends', label: 'Friends', icon: , active: path.startsWith('/friends') }, { href: `/u/${me.username}`, label: 'Profile', icon: , active: path.startsWith(`/u/${me.username}`) }, { href: '/settings', label: 'Settings', icon: , active: path.startsWith('/settings') || path === '/admin' }, ] @@ -89,6 +90,14 @@ const FeedIcon = () => ( ); +const FriendsIcon = () => ( + + + + + + +); const ProfileIcon = () => (