Inspired by a dashboard design the user shared. - App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue gradient card for your own song (text and controls switch to white on it), flat cards for friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab. - Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel, and a right column with your profile, recent activity and recent plays. Phones keep the top bar and bottom tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
import { Image } from 'expo-image';
|
|
import { router, useFocusEffect } from 'expo-router';
|
|
import { useCallback } from 'react';
|
|
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
import { useSWRConfig } from 'swr';
|
|
import useSWRInfinite from 'swr/infinite';
|
|
import { notificationPath, notificationText, timeAgo, type NotificationDTO, type NotificationListDTO } from '@musicshare/shared';
|
|
import { Avatar } from '@/components/Avatar';
|
|
import { Body } from '@/components/ui';
|
|
import { api } from '@/lib/api';
|
|
import { colors, fonts, space } from '@/lib/theme';
|
|
|
|
/** Reactions, new followers and songs sent to you, newest first. Opening it marks them read. */
|
|
export default function NotificationsScreen() {
|
|
const { mutate: mutateGlobal } = useSWRConfig();
|
|
const { data, size, setSize, isLoading, mutate } = useSWRInfinite<NotificationListDTO>(
|
|
(i, prev: NotificationListDTO | null) => (i > 0 && !prev?.nextCursor ? null : ['notifications', prev?.nextCursor ?? null]),
|
|
([, cursor]: [string, string | null]) => api.notifications(cursor),
|
|
);
|
|
const items = data?.flatMap((p) => p.notifications) ?? [];
|
|
const more = Boolean(data?.at(-1)?.nextCursor);
|
|
|
|
// Seen once you've looked; the bell clears (the list keeps showing which were new until you leave).
|
|
const unread = data?.[0]?.unread ?? 0;
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if (!unread) return;
|
|
api
|
|
.markNotificationsRead()
|
|
.then(() => mutateGlobal('/notifications'))
|
|
.catch(() => {});
|
|
return () => {
|
|
mutate();
|
|
};
|
|
}, [unread, mutate, mutateGlobal]),
|
|
);
|
|
|
|
return (
|
|
<FlatList
|
|
data={items}
|
|
keyExtractor={(n) => n.id}
|
|
contentContainerStyle={styles.list}
|
|
style={{ backgroundColor: colors.bg }}
|
|
onEndReached={() => more && setSize(size + 1)}
|
|
ListEmptyComponent={
|
|
isLoading ? null : <Body muted>Nothing yet. Reactions, new followers and songs sent to you show up here.</Body>
|
|
}
|
|
renderItem={({ item }) => <Row n={item} />}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function Row({ n }: { n: NotificationDTO }) {
|
|
const { title, body } = notificationText(n);
|
|
return (
|
|
<Pressable
|
|
onPress={() => router.push(notificationPath(n) as never)}
|
|
style={({ pressed }) => [styles.row, !n.read && styles.unread, pressed && { opacity: 0.7 }]}
|
|
accessibilityRole="link">
|
|
<Avatar user={n.actor} size={40} />
|
|
<View style={styles.text}>
|
|
<Text style={styles.title} numberOfLines={2}>
|
|
{title}
|
|
</Text>
|
|
<Text style={styles.body} numberOfLines={3}>
|
|
{body}
|
|
</Text>
|
|
<Text style={styles.time}>{timeAgo(n.createdAt)}</Text>
|
|
</View>
|
|
{n.track?.artworkUrl && <Image source={n.track.artworkUrl} style={styles.art} contentFit="cover" />}
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
list: { padding: space.lg, gap: space.sm },
|
|
row: { flexDirection: 'row', gap: space.md, alignItems: 'center', padding: space.md, borderRadius: 14 },
|
|
unread: { backgroundColor: colors.surface2 },
|
|
text: { flex: 1, minWidth: 0, gap: 2 },
|
|
title: { color: colors.text, fontFamily: fonts.display, fontSize: 15 },
|
|
body: { color: colors.text, opacity: 0.85, fontSize: 14, fontFamily: fonts.body },
|
|
time: { color: colors.muted, fontSize: 12 },
|
|
art: { width: 44, height: 44, borderRadius: 8 },
|
|
});
|