feat: replace mail protocol with smolmail and rebuild the UI

This commit is contained in:
randogoth 2026-09-26 22:45:38 +03:00
parent cb24b68069
commit e94e4158cc
93 changed files with 6387 additions and 3479 deletions

View file

@ -1,69 +1,128 @@
import "dart:typed_data";
import "package:auto_route/auto_route.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:flash_mail/data/providers/providers.dart";
import "package:flash_mail/presentation/widgets/image_banner.dart";
import "package:flash_mail/presentation/widgets/message/message_view.dart";
import "package:smol_mail/data/providers/providers.dart";
import "package:smol_mail/presentation/widgets/image_banner.dart";
import "package:smol_mail/presentation/widgets/message/message_view.dart";
import "package:smol_mail/presentation/theme/app_colors.dart";
import "package:smol_mail/shared/utils/snackbar.dart";
import "package:smol_mail/smol/errors.dart";
/// Fetched mail is acknowledged off the server during fetch, so deleting here
/// removes only the local sealed copy.
@RoutePage()
class MessageDetailScreen extends ConsumerWidget {
final String folder;
final String id;
final String subject;
const MessageDetailScreen({
super.key,
required this.folder,
required this.id,
required this.subject,
});
void deleteMessage(BuildContext context, WidgetRef ref) async {
await ref.read(clientProvider).deleteMessage(id);
ref.invalidate(messagesProvider);
Future<void> _delete(BuildContext context, WidgetRef ref) async {
await ref.read(storeProvider).deleteMessage(folder, id);
ref.read(revisionProvider.notifier).bump();
if (context.mounted) {
AutoRouter.of(context).pop();
}
}
Future<void> _nameSender(BuildContext context, WidgetRef ref,
Uint8List senderKey) async {
final client = ref.read(clientProvider);
final controller = TextEditingController();
final address = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Theme.of(context).extension<AppColors>()!.cardFill,
content: TextField(
controller: controller,
decoration: InputDecoration(
hintText: "user@host or smol://…",
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(controller.text),
child: const Text("Save"),
),
],
),
);
if (address == null || address.trim().isEmpty) return;
try {
await client.nameSender(address, senderKey);
ref.read(revisionProvider.notifier).bump();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Address saved for this sender")),
);
}
} on SmolError catch (err) {
if (context.mounted) {
showErrorSnackBar(context, err.message);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final message = ref.watch(messageDetailProvider(id));
final record = ref.watch(storeProvider).getMessage(folder, id);
if (record == null) {
return Scaffold(
appBar: AppBar(),
body: const ImageBanner(
imgSrc: "assets/images/error.png",
text: "Message not found!",
),
);
}
final client = ref.read(clientProvider);
final opened = client.describe(record);
final isSent = folder == "sent";
final who = isSent
? (record.recipient ?? "")
: (opened.sender == null
? ""
: ref.watch(storeProvider).addressForKey(opened.sender!) ?? "");
return Scaffold(
appBar: AppBar(
title: who.isEmpty
? null
: Text(
who,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "Inter",
fontWeight: FontWeight.w500,
fontSize: 18,
color: Theme.of(context).colorScheme.onSurface,
),
),
actions: [
IconButton(
onPressed: () => deleteMessage(context, ref),
icon: const Icon(Icons.delete, color: Colors.white),
)
tooltip: "Delete local copy",
onPressed: () => _delete(context, ref),
icon: const Icon(Icons.delete),
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Column(
children: [
Container(
alignment: Alignment.centerLeft,
child: Text(
subject,
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontSize: 20),
),
),
const SizedBox(height: 30),
message.when(
data: (data) => MessageView(message: data),
error: (err, _) => const ImageBanner(
imgSrc: "assets/images/error.png",
text: "Something went wrong!",
),
loading: () => const SizedBox.shrink(),
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: MessageView(
record: record,
opened: opened,
onNameSender: opened.sender == null
? null
: (key) => _nameSender(context, ref, key),
),
),
);