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>
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
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 { FollowListItemDTO, UserSummaryDTO } from '@musicshare/shared';
|
|
import { AmbientBackground } from '@/components/AmbientBackground';
|
|
import { Avatar } from '@/components/Avatar';
|
|
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';
|
|
|
|
/** 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 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={rows}
|
|
keyExtractor={(u) => u.id}
|
|
keyboardShouldPersistTaps="handled"
|
|
contentContainerStyle={styles.list}
|
|
onEndReached={searching ? undefined : list.loadMore}
|
|
ListHeaderComponent={
|
|
<View style={{ gap: space.md }}>
|
|
<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="Find people by name or username"
|
|
value={q}
|
|
onChangeText={setQ}
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
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={
|
|
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>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safe: { flex: 1, backgroundColor: colors.bg },
|
|
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 },
|
|
});
|