Safety screens: followers, blocking, reporting, admin
CI / check (push) Canceled after 0s

Web and mobile:
- Follower/following lists (tap the counts), with Follow buttons and
  "Remove" on your own followers.
- Profile ⋯ menu: block (with confirmation) / unblock, and report with a
  reason and optional note. "Follows you" label, "Follow back", and a
  banner on profiles you've blocked.
- Blocked accounts list in Settings, with unblock.
Web only: /admin for the admin account: stats, open/resolved reports,
resolve, suspend/unsuspend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 12:53:28 -04:00
co-authored by Claude Opus 5.5
parent 13df568348
commit 45a7bd190a
21 changed files with 741 additions and 21 deletions
+49
View File
@@ -0,0 +1,49 @@
import { Link } from 'expo-router';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import useSWR from 'swr';
import type { UserSummaryDTO } from '@musicshare/shared';
import { Avatar } from '@/components/Avatar';
import { Body, Button } from '@/components/ui';
import { api } from '@/lib/api';
import { showError } from '@/lib/errors';
import { colors, space } from '@/lib/theme';
export default function BlockedScreen() {
const { data: blocked = [], mutate, isLoading } = useSWR<UserSummaryDTO[]>('/me/blocks');
const unblock = async (username: string) => {
try {
await api.unblock(username);
await mutate();
} catch (err) {
showError(err);
}
};
return (
<FlatList
data={blocked}
keyExtractor={(u) => u.id}
contentContainerStyle={styles.list}
ListEmptyComponent={isLoading ? null : <Body muted>You haven&apos;t blocked anyone. Block someone from the ⋯ menu on their profile.</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} />
<Text style={styles.name} numberOfLines={1}>
{u.displayName ?? u.username}
</Text>
</View>
</Link>
<Button small title="Unblock" onPress={() => unblock(u.username)} />
</View>
)}
/>
);
}
const styles = StyleSheet.create({
list: { padding: space.lg, gap: space.md },
row: { flexDirection: 'row', alignItems: 'center', gap: space.md },
person: { flexDirection: 'row', alignItems: 'center', gap: space.md },
name: { color: colors.text, fontSize: 15, fontWeight: '600', flexShrink: 1 },
});