This commit is contained in:
2026-10-16 01:25:50 +05:30
commit c60ea2d370
89 changed files with 4661 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
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<InboxScreen> createState() => _InboxScreenState();
}
class _InboxScreenState extends State<InboxScreen> {
var _started = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_started) {
return;
}
_started = true;
unawaited(AppScope.of(context).inbox.refreshList(showLoading: true));
}
Future<void> _logout() async {
final confirmed = await showDialog<bool>(
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<TicketStatusFilter>(
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<String?>(
isExpanded: true,
value: inbox.category,
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('All categories'),
),
for (final entry in supportCategoryLabels.entries)
DropdownMenuItem<String?>(
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<void>(
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),
),
);
}
}