import 'dart:async'; import 'package:flutter/material.dart'; import '../app_scope.dart'; import '../config.dart'; import '../format.dart'; import '../inbox.dart'; import '../polling.dart'; import 'thread_screen.dart'; class InboxScreen extends StatefulWidget { const InboxScreen({super.key}); @override State createState() => _InboxScreenState(); } class _InboxScreenState extends State { var _started = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (_started) { return; } _started = true; unawaited(AppScope.of(context).inbox.refreshList(showLoading: true)); } Future _logout() async { final confirmed = await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Log out?'), content: const Text( 'This device will stop receiving support notifications.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.pop(context, true), child: const Text('Log out'), ), ], ); }, ); if (confirmed != true || !mounted) { return; } final session = AppScope.of(context).session; await session.signOut(); } @override Widget build(BuildContext context) { final inbox = AppScope.of(context).inbox; return ForegroundPoll( onPoll: () => inbox.refreshList(), child: Scaffold( appBar: AppBar( title: const Text('Inbox'), actions: [ IconButton( tooltip: 'Log out', onPressed: _logout, icon: const Icon(Icons.logout), ), ], ), body: Column( children: [ _Filters(inbox: inbox), const Divider(height: 1), Expanded( child: ListenableBuilder( listenable: inbox, builder: (context, _) { return RefreshIndicator( onRefresh: () => inbox.refreshList(), child: _TicketList(inbox: inbox), ); }, ), ), ], ), ), ); } } class _Filters extends StatelessWidget { const _Filters({required this.inbox}); final InboxController inbox; @override Widget build(BuildContext context) { return ListenableBuilder( listenable: inbox, builder: (context, _) { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SegmentedButton( segments: const [ ButtonSegment( value: TicketStatusFilter.open, label: Text('Open'), ), ButtonSegment( value: TicketStatusFilter.closed, label: Text('Closed'), ), ButtonSegment( value: TicketStatusFilter.all, label: Text('All'), ), ], selected: {inbox.statusFilter}, onSelectionChanged: (selection) { inbox.setStatusFilter(selection.first); }, ), const SizedBox(height: 12), DropdownButton( isExpanded: true, value: inbox.category, items: [ const DropdownMenuItem( value: null, child: Text('All categories'), ), for (final entry in supportCategoryLabels.entries) DropdownMenuItem( value: entry.key, child: Text(entry.value), ), ], onChanged: inbox.setCategory, ), ], ), ); }, ); } } class _TicketList extends StatelessWidget { const _TicketList({required this.inbox}); final InboxController inbox; @override Widget build(BuildContext context) { if (inbox.loading && inbox.tickets.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (inbox.error != null && inbox.tickets.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ Padding( padding: const EdgeInsets.all(24), child: Column( children: [ Text(inbox.error!, textAlign: TextAlign.center), const SizedBox(height: 12), OutlinedButton( onPressed: () => inbox.refreshList(showLoading: true), child: const Text('Try again'), ), ], ), ), ], ); } if (inbox.tickets.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: const [ Padding( padding: EdgeInsets.all(24), child: Text('No tickets', textAlign: TextAlign.center), ), ], ); } return ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 8), itemCount: inbox.tickets.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final ticket = inbox.tickets[index]; return ListTile( title: Text(ticket.displayCategory), subtitle: Text( '${ticket.username}\n${ticket.lastMessagePreview}', maxLines: 3, overflow: TextOverflow.ellipsis, ), isThreeLine: true, trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ _StatusChip(closed: ticket.isClosed, label: ticket.statusLabel), const SizedBox(height: 6), Text( formatTimestamp(ticket.lastMessageAt), style: Theme.of(context).textTheme.labelSmall, ), ], ), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ThreadScreen(ticketId: ticket.id), ), ); }, ); }, ); } } class _StatusChip extends StatelessWidget { const _StatusChip({required this.closed, required this.label}); final bool closed; final String label; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return DecoratedBox( decoration: BoxDecoration( color: closed ? scheme.surfaceContainerHighest : scheme.primaryContainer, borderRadius: BorderRadius.circular(999), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: Text(label, style: Theme.of(context).textTheme.labelSmall), ), ); } }