goon/mobile/src/components/WhatsNewModal.tsx
goon-foss 576058497a fix(ui): grupuj changelog po miesiacu, zdejmij naglowek z Sites
Changelog: 17 sierpniowych wpisow drukowalo "AUGUST 2026" 17 razy, bo naglowek szedl
przy KAZDYM wpisie. Teraz wpisy sa laczone w grupy miesieczne (sa juz newest-first,
wiec wystarczy sklejac kolejne z tym samym `date`), naglowek leci raz na grupe.

Sites: usuniety tytul "SITES" + licznik i podpowiedz "tap a tube -> newest scenes".
Byl jedynym ekranem-zakladka z wlasnym naglowkiem, przez co jego tresc zaczynala sie
nizej niz na pozostalych. Martwe style (headerRow/headerLabel/headerCount/hint) wyciete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:23:04 +02:00

136 lines
4.4 KiB
TypeScript

/**
* "What's new" popup, pojawia się raz po dociągnięciu OTA (bundle ma nowszy changelog
* niż ostatnio widziany). Self-contained: czyta CHANGELOG (bundlowany) + lastSeen ze
* SecureStore. Po zamknięciu zapisuje najnowszy id, więc nie pokazuje się ponownie aż
* do kolejnej aktualizacji. Montowany raz, nad nawigacją (App.tsx).
*/
import React from 'react';
import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import {
NEWEST_CHANGELOG_ID,
unseenEntries,
type ChangelogEntry,
} from '../changelog';
import { getLastSeenChangelog, getOnboardingSeen, setLastSeenChangelog } from '../storage';
import { theme } from '../theme';
export function WhatsNewModal() {
const [entries, setEntries] = React.useState<ChangelogEntry[]>([]);
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
let cancelled = false;
(async () => {
try {
// Nowy user (jeszcze nie przeszedł tutoriala) → tutorial ma pierwszeństwo,
// nie wyskakuj changelogiem. Onboarding po sobie oznaczy changelog widziany.
const onboarded = await getOnboardingSeen();
if (!onboarded) return;
const lastSeen = await getLastSeenChangelog();
const unseen = unseenEntries(lastSeen);
if (!cancelled && unseen.length > 0) {
setEntries(unseen);
setVisible(true);
}
} catch {
// brak SecureStore / błąd, po prostu nie pokazujemy
}
})();
return () => {
cancelled = true;
};
}, []);
const dismiss = React.useCallback(() => {
setVisible(false);
if (NEWEST_CHANGELOG_ID) setLastSeenChangelog(NEWEST_CHANGELOG_ID).catch(() => {});
}, []);
// Nagłówek miesiąca RAZ na grupę, nie przy każdym wpisie. Wcześniej 17 sierpniowych
// wpisów drukowało „AUGUST 2026" 17 razy (zgłoszenie 2026-08-18). Wpisy są już
// newest-first, więc kolejne z tym samym `date` po prostu doklejamy do bieżącej grupy.
const months = React.useMemo(() => {
const out: { date: string; items: string[] }[] = [];
for (const e of entries) {
const last = out[out.length - 1];
if (last && last.date === e.date) last.items.push(...e.items);
else out.push({ date: e.date, items: [...e.items] });
}
return out;
}, [entries]);
if (!visible || entries.length === 0) return null;
return (
<Modal visible transparent animationType="fade" onRequestClose={dismiss}>
<View style={styles.backdrop}>
<View style={styles.card}>
<Text style={styles.title}>What's new</Text>
<ScrollView style={styles.scroll} contentContainerStyle={{ paddingBottom: 4 }}>
{months.map((m) => (
<View key={m.date} style={styles.month}>
<Text style={styles.entryDate}>{m.date}</Text>
{m.items.map((it, i) => (
<View key={i} style={styles.itemRow}>
<Text style={styles.bullet}>•</Text>
<Text style={styles.itemText}>{it}</Text>
</View>
))}
</View>
))}
</ScrollView>
<Pressable style={styles.btn} onPress={dismiss}>
<Text style={styles.btnText}>Got it</Text>
</Pressable>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.7)',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
},
card: {
width: '100%',
maxHeight: '80%',
backgroundColor: theme.bgElevated,
borderRadius: 16,
borderColor: theme.border,
borderWidth: 1,
padding: 20,
},
title: {
color: theme.fg,
fontSize: 20,
fontWeight: '800',
marginBottom: 14,
},
scroll: { flexGrow: 0 },
month: { marginBottom: 16 },
entryDate: {
color: theme.accentText,
fontSize: 12,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
},
itemRow: { flexDirection: 'row', gap: 8, marginBottom: 8 },
bullet: { color: theme.accentText, fontSize: 15, lineHeight: 21 },
itemText: { color: theme.fg, fontSize: 15, lineHeight: 21, flex: 1 },
btn: {
marginTop: 8,
backgroundColor: theme.accent,
borderRadius: 10,
paddingVertical: 12,
alignItems: 'center',
},
btnText: { color: theme.fg, fontSize: 15, fontWeight: '700' },
});