import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/auth/auth_notifier.dart'; import '../../core/availability/mitra_availability_notifier.dart'; import '../../core/chat/active_session_notifier.dart'; import '../chat/widgets/topic_selection_bottom_sheet.dart'; /// Home screen. /// /// 1. The "Mulai Curhat" CTA is gated on real-time mitra availability /// (polling owned by the [mitraAvailabilityProvider]). Polling is paused /// on background and resumed on foreground via [WidgetsBindingObserver]. /// 2. Tapping the enabled CTA pushes `/payment` so the customer must confirm /// a payment session before any blast fires. class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override ConsumerState createState() => _HomeScreenState(); } class _HomeScreenState extends ConsumerState with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Kick the availability poll on once the first frame settles. Doing it // here (rather than in build) avoids re-firing on every rebuild. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ref.read(mitraAvailabilityProvider.notifier).setActive(true); }); } @override void dispose() { // Stop polling when leaving home. ref.read(mitraAvailabilityProvider.notifier).setActive(false); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final notifier = ref.read(mitraAvailabilityProvider.notifier); if (state == AppLifecycleState.resumed) { // Re-fetch in case a session ended/started while backgrounded. ref.read(activeSessionProvider.notifier).refresh(); notifier.setActive(true); } else if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { notifier.setActive(false); } } Future _onStartChatPressed(BuildContext context) async { final topic = await TopicSelectionBottomSheet.show(context); if (topic == null || !context.mounted) return; context.push('/payment', extra: {'topicSensitivity': topic}); } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); final authData = authState.valueOrNull; final activeSessionAsync = ref.watch(activeSessionProvider); final availabilityAsync = ref.watch(mitraAvailabilityProvider); final displayName = switch (authData) { AuthAuthenticatedData d => d.profile['display_name'] as String? ?? '', AuthAnonymousData d => d.displayName, _ => '', }; // Poll-failure / loading both default to "no bestie available" (greyed-out). // Never optimistically enable. final mitraAvailable = availabilityAsync.valueOrNull ?? false; return Scaffold( appBar: AppBar( title: const Text('Halo Bestie'), actions: [ IconButton( icon: const Icon(Icons.history), onPressed: () => context.push('/chat/history'), ), IconButton( icon: const Icon(Icons.logout), onPressed: () => ref.read(authProvider.notifier).logout(), ), ], ), body: RefreshIndicator( onRefresh: () async { // Pull-to-refresh kicks both the active-session and availability polls. await Future.wait([ ref.read(activeSessionProvider.notifier).refresh(), ref.read(mitraAvailabilityProvider.notifier).refresh(), ]); }, child: ListView( // Force-scroll so RefreshIndicator can fire even on a short body. physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(32), children: [ const SizedBox(height: 32), Center(child: Text('Halo, $displayName!', style: const TextStyle(fontSize: 24))), const SizedBox(height: 32), Center( child: activeSessionAsync.when( loading: () => const CircularProgressIndicator(), error: (_, __) => _StartChatButton( enabled: mitraAvailable, onPressed: () => _onStartChatPressed(context), ), data: (snapshot) { // Hide the "Sesi Aktif" CTA when the session is in `closing` // — the conversation is over, only the goodbye composer // remains. Backend auto-completes such sessions after a // grace period; until then the user shouldn't be invited // back into them from home. final status = snapshot.session?['status'] as String?; final isCurhatable = snapshot.hasSession && status != 'closing'; if (isCurhatable) { return _ActiveSessionCard( mitraName: snapshot.mitraName, unreadCount: snapshot.unreadCount, onTap: () { final sessionId = snapshot.sessionId; if (sessionId == null) return; context.push('/chat/session/$sessionId', extra: snapshot.mitraName); }, ); } return _StartChatButton( enabled: mitraAvailable, onPressed: () => _onStartChatPressed(context), ); }, ), ), ], ), ), ); } } class _StartChatButton extends StatelessWidget { final bool enabled; final VoidCallback onPressed; const _StartChatButton({required this.enabled, required this.onPressed}); @override Widget build(BuildContext context) { return Column( children: [ const SizedBox(height: 16), ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 16), ), onPressed: enabled ? onPressed : null, child: const Text('Mulai Curhat', style: TextStyle(fontSize: 18)), ), const SizedBox(height: 12), if (!enabled) Text( 'Belum ada bestie tersedia', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), ], ); } } class _ActiveSessionCard extends StatelessWidget { final String mitraName; final int unreadCount; final VoidCallback onTap; const _ActiveSessionCard({ required this.mitraName, required this.unreadCount, required this.onTap, }); @override Widget build(BuildContext context) { return Card( elevation: 2, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(20), child: Row( children: [ Badge( isLabelVisible: unreadCount > 0, label: Text('$unreadCount'), child: const CircleAvatar( backgroundColor: Colors.green, child: Icon(Icons.chat, color: Colors.white), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Sesi Aktif', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( 'Sedang curhat dengan $mitraName', style: const TextStyle(fontSize: 14, color: Colors.grey), ), ], ), ), const Icon(Icons.chevron_right), ], ), ), ), ); } }