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:
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user