Friends tab shows who you follow and who follows you

The app's Friends tab was only a search box. It now lists Following and
Followers (with Follow / Remove buttons) until you type a search. The
website gets the same Friends page and tab; search moves there from the
top of the feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 20:43:03 -04:00
co-authored by Claude Opus 5.5
parent a4dad724ad
commit 08631589e3
5 changed files with 174 additions and 66 deletions
+55 -20
View File
@@ -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<UserSummaryDTO[]>(query.length >= 2 ? `/users/search?q=${encodeURIComponent(query)}` : null);
const searching = query.length >= 2;
const { data: found } = useSWR<UserSummaryDTO[]>(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 (
<SafeAreaView style={styles.safe} edges={['top']}>
<AmbientBackground />
<FlatList
data={query.length >= 2 ? (data ?? []) : []}
data={rows}
keyExtractor={(u) => u.id}
keyboardShouldPersistTaps="handled"
contentContainerStyle={styles.list}
onEndReached={searching ? undefined : list.loadMore}
ListHeaderComponent={
<View style={{ gap: space.md }}>
<Title>Find friends</Title>
<Title>Friends</Title>
<Row>
<Button small variant="primary" title="Scan a code" onPress={() => router.push('/scan')} />
<Button small title="My code" onPress={() => router.push('/qr')} />
</Row>
<Field
placeholder="Search by name or username"
placeholder="Find people by name or username"
value={q}
onChangeText={setQ}
autoCapitalize="none"
@@ -39,18 +58,34 @@ export default function SearchScreen() {
returnKeyType="search"
clearButtonMode="while-editing"
/>
{!searching && (
<Row>
<Chip label="Following" selected={tab === 'following'} onPress={() => setTab('following')} />
<Chip label="Followers" selected={tab === 'followers'} onPress={() => setTab('followers')} />
</Row>
)}
</View>
}
ListEmptyComponent={query.length >= 2 && data ? <Body muted>No one found.</Body> : null}
renderItem={({ item }) => (
<Pressable style={styles.row} onPress={() => router.push(`/u/${item.username}`)} accessibilityRole="link">
<Avatar user={item} size={40} />
<View>
<Text style={styles.name}>{item.displayName ?? item.username}</Text>
<Text style={styles.handle}>@{item.username}</Text>
</View>
</Pressable>
)}
ListEmptyComponent={
searching ? (
found ? <Body muted>No one found.</Body> : null
) : list.isLoading ? null : (
<Body muted>{emptyFollowText(tab)}</Body>
)
}
renderItem={({ item }) =>
'isFollowing' in item ? (
<PersonRow user={item} list={list} removable={tab === 'followers'} />
) : (
<Pressable style={styles.row} onPress={() => router.push(`/u/${item.username}`)} accessibilityRole="link">
<Avatar user={item} size={40} />
<View>
<Text style={styles.name}>{item.displayName ?? item.username}</Text>
<Text style={styles.handle}>@{item.username}</Text>
</View>
</Pressable>
)
}
/>
</SafeAreaView>
);
@@ -58,7 +93,7 @@ export default function SearchScreen() {
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.bg },
list: { padding: space.lg, gap: space.sm },
list: { padding: space.lg, gap: space.md },
row: { flexDirection: 'row', alignItems: 'center', gap: space.md, paddingVertical: space.sm },
name: { color: colors.text, fontWeight: '700', fontSize: 15 },
handle: { color: colors.muted, fontSize: 13 },