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:
@@ -2,7 +2,7 @@ import { Link } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import type { FollowListDTO } from '@musicshare/shared';
|
||||
import type { FollowListDTO, FollowListItemDTO } from '@musicshare/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { useSession } from '@/lib/session';
|
||||
@@ -10,17 +10,17 @@ import { colors, space } from '@/lib/theme';
|
||||
import { Avatar } from './Avatar';
|
||||
import { Body, Button } from './ui';
|
||||
|
||||
/** Followers or following, 50 at a time. On your own followers you can remove people. */
|
||||
export function FollowList({ username, direction }: { username: string; direction: 'followers' | 'following' }) {
|
||||
const { me } = useSession();
|
||||
type Direction = 'followers' | 'following';
|
||||
|
||||
/** Followers or following, 50 at a time, with follow/unfollow/remove actions that refresh the list. */
|
||||
export function useFollowList(username: string | undefined, direction: Direction) {
|
||||
const { data, size, setSize, mutate, isLoading } = useSWRInfinite<FollowListDTO>(
|
||||
(i, prev: FollowListDTO | null) => (i > 0 && !prev?.nextCursor ? null : [username, direction, prev?.nextCursor ?? null]),
|
||||
([u, d, cursor]: [string, typeof direction, string | null]) => (d === 'followers' ? api.followers(u, cursor) : api.following(u, cursor)),
|
||||
(i, prev: FollowListDTO | null) =>
|
||||
!username || (i > 0 && !prev?.nextCursor) ? null : [username, direction, prev?.nextCursor ?? null],
|
||||
([u, d, cursor]: [string, Direction, string | null]) => (d === 'followers' ? api.followers(u, cursor) : api.following(u, cursor)),
|
||||
);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const users = data?.flatMap((p) => p.users) ?? [];
|
||||
const more = Boolean(data?.at(-1)?.nextCursor);
|
||||
const own = me?.username === username;
|
||||
|
||||
const act = async (target: string, fn: () => Promise<unknown>) => {
|
||||
setBusy(target);
|
||||
@@ -33,44 +33,81 @@ export function FollowList({ username, direction }: { username: string; directio
|
||||
setBusy(null);
|
||||
};
|
||||
|
||||
return {
|
||||
users: data?.flatMap((p) => p.users) ?? [],
|
||||
isLoading,
|
||||
loadMore: () => more && setSize(size + 1),
|
||||
refresh: () => mutate(),
|
||||
busy,
|
||||
act,
|
||||
};
|
||||
}
|
||||
|
||||
/** One person in a follow list: picture and name linking to their profile, plus Follow / Remove buttons. */
|
||||
export function PersonRow({
|
||||
user: u,
|
||||
list,
|
||||
removable,
|
||||
}: {
|
||||
user: FollowListItemDTO;
|
||||
list: ReturnType<typeof useFollowList>;
|
||||
/** On your own followers you can remove people. */
|
||||
removable?: boolean;
|
||||
}) {
|
||||
const { me } = useSession();
|
||||
return (
|
||||
<FlatList
|
||||
data={users}
|
||||
keyExtractor={(u) => u.id}
|
||||
contentContainerStyle={styles.list}
|
||||
onEndReached={() => more && setSize(size + 1)}
|
||||
ListEmptyComponent={
|
||||
isLoading ? null : <Body muted>{direction === 'followers' ? 'No followers yet.' : 'Not following anyone yet.'}</Body>
|
||||
}
|
||||
renderItem={({ item: u }) => (
|
||||
<View style={styles.row}>
|
||||
<Link href={`/u/${u.username}`} style={{ flex: 1 }}>
|
||||
<View style={styles.person}>
|
||||
<Avatar user={u} size={40} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.name} numberOfLines={1}>
|
||||
{u.displayName ?? u.username}
|
||||
</Text>
|
||||
<Text style={styles.handle}>@{u.username}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
{me && u.username !== me.username && (
|
||||
<View style={styles.actions}>
|
||||
{own && direction === 'followers' && (
|
||||
<Button small title="Remove" disabled={busy === u.username} onPress={() => act(u.username, () => api.removeFollower(u.username))} />
|
||||
)}
|
||||
<Button
|
||||
small
|
||||
variant={u.isFollowing ? 'secondary' : 'primary'}
|
||||
title={u.isFollowing ? 'Following' : 'Follow'}
|
||||
disabled={busy === u.username}
|
||||
onPress={() => act(u.username, () => (u.isFollowing ? api.unfollow(u.username) : api.follow(u.username)))}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Link href={`/u/${u.username}`} style={{ flex: 1 }}>
|
||||
<View style={styles.person}>
|
||||
<Avatar user={u} size={40} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.name} numberOfLines={1}>
|
||||
{u.displayName ?? u.username}
|
||||
</Text>
|
||||
<Text style={styles.handle}>@{u.username}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
{me && u.username !== me.username && (
|
||||
<View style={styles.actions}>
|
||||
{removable && (
|
||||
<Button
|
||||
small
|
||||
title="Remove"
|
||||
disabled={list.busy === u.username}
|
||||
onPress={() => list.act(u.username, () => api.removeFollower(u.username))}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
small
|
||||
variant={u.isFollowing ? 'secondary' : 'primary'}
|
||||
title={u.isFollowing ? 'Following' : 'Follow'}
|
||||
disabled={list.busy === u.username}
|
||||
onPress={() => list.act(u.username, () => (u.isFollowing ? api.unfollow(u.username) : api.follow(u.username)))}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export const emptyFollowText = (direction: Direction) =>
|
||||
direction === 'followers' ? 'No followers yet.' : 'Not following anyone yet.';
|
||||
|
||||
/** Followers or following, 50 at a time. On your own followers you can remove people. */
|
||||
export function FollowList({ username, direction }: { username: string; direction: Direction }) {
|
||||
const { me } = useSession();
|
||||
const list = useFollowList(username, direction);
|
||||
const removable = me?.username === username && direction === 'followers';
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={list.users}
|
||||
keyExtractor={(u) => u.id}
|
||||
contentContainerStyle={styles.list}
|
||||
onEndReached={list.loadMore}
|
||||
ListEmptyComponent={list.isLoading ? null : <Body muted>{emptyFollowText(direction)}</Body>}
|
||||
renderItem={({ item }) => <PersonRow user={item} list={list} removable={removable} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user