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>
136 lines
4.4 KiB
TypeScript
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' },
|
|
});
|