feat: adopt smolmail protocol 1.1, adaptive nav shell, and server mail retention
This commit is contained in:
parent
eaaa3f2ede
commit
c693e6fcb9
27 changed files with 1190 additions and 592 deletions
|
|
@ -4,18 +4,46 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
|||
|
||||
import "package:smol_mail/data/providers/providers.dart";
|
||||
import "package:smol_mail/presentation/routes/app_router.gr.dart";
|
||||
import "package:smol_mail/presentation/widgets/drawer/app_drawer.dart";
|
||||
import "package:smol_mail/presentation/widgets/image_banner.dart";
|
||||
import "package:smol_mail/presentation/widgets/message/message_list.dart";
|
||||
import "package:smol_mail/shared/utils/snackbar.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
|
||||
const _folders = ["inbox", "requests", "sent"];
|
||||
|
||||
/// No server push in v1 (SPEC.md §13): new mail arrives when the user taps
|
||||
/// fetch, which pulls everything and acknowledges what verifies.
|
||||
/// fetch, which pulls everything and acknowledges what verifies. One FETCH
|
||||
/// call populates both Inbox and Requests (they're tiers of the same
|
||||
/// mailbox), so both — but not the purely local Sent folder — offer fetch.
|
||||
@RoutePage()
|
||||
class InboxScreen extends ConsumerWidget {
|
||||
class InboxScreen extends ConsumerStatefulWidget {
|
||||
const InboxScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<InboxScreen> createState() => _InboxScreenState();
|
||||
}
|
||||
|
||||
class _InboxScreenState extends ConsumerState<InboxScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: _folders.length, vsync: this)
|
||||
..addListener(() {
|
||||
// Fires once settled, whether the change came from a tap or a swipe;
|
||||
// only the AppBar's fetch button depends on which tab is active.
|
||||
if (!_tabController.indexIsChanging) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetch(BuildContext context, WidgetRef ref) async {
|
||||
final client = ref.read(clientProvider);
|
||||
try {
|
||||
|
|
@ -36,47 +64,63 @@ class InboxScreen extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _folderView(BuildContext context, WidgetRef ref, String folder) {
|
||||
final messages = ref.watch(messagesForFolderProvider(folder));
|
||||
final canFetch = folder != "sent";
|
||||
final onRefresh = canFetch ? () => _fetch(context, ref) : () => Future.value();
|
||||
|
||||
if (messages.isEmpty) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: ListView(
|
||||
children: const [
|
||||
ImageBanner(
|
||||
imgSrc: "assets/images/empty.png",
|
||||
text: "Nothing here yet!",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return MessageList(folder: folder, onRefresh: onRefresh);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final messages = ref.watch(messagesProvider);
|
||||
final folder = ref.watch(folderProvider);
|
||||
Widget build(BuildContext context) {
|
||||
final unread = ref.watch(unreadProvider);
|
||||
final requestsUnread = ref.watch(requestsUnreadProvider);
|
||||
final canFetch = _folders[_tabController.index] != "sent";
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(folder == "inbox" ? "Inbox" : "Sent"),
|
||||
title: const Text("kirakira"),
|
||||
actions: [
|
||||
if (folder == "inbox")
|
||||
if (canFetch)
|
||||
IconButton(
|
||||
tooltip: "Fetch",
|
||||
onPressed: () => _fetch(context, ref),
|
||||
icon: const Icon(Icons.cloud_download),
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: unread > 0 ? "Inbox ($unread)" : "Inbox"),
|
||||
Tab(text: requestsUnread > 0 ? "Requests ($requestsUnread)" : "Requests"),
|
||||
const Tab(text: "Sent"),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => AutoRouter.of(context).push(ComposeRoute()),
|
||||
child: const Icon(Icons.edit),
|
||||
),
|
||||
body: messages.isEmpty
|
||||
? RefreshIndicator(
|
||||
onRefresh: () => folder == "inbox"
|
||||
? _fetch(context, ref)
|
||||
: Future.value(),
|
||||
child: ListView(
|
||||
children: const [
|
||||
ImageBanner(
|
||||
imgSrc: "assets/images/empty.png",
|
||||
text: "Nothing here yet!",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: MessageList(
|
||||
onRefresh: () => folder == "inbox"
|
||||
? _fetch(context, ref)
|
||||
: Future.value(),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
for (final folder in _folders) _folderView(context, ref, folder),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue