import "package:auto_route/auto_route.dart"; import "package:flutter/material.dart"; 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/shared/utils/format.dart"; import "package:smol_mail/shared/utils/snackbar.dart"; import "package:smol_mail/smol/client.dart"; import "package:smol_mail/smol/store.dart"; import "package:smol_mail/smol/ui.dart"; /// The opened message: trust row (fingerprint and how the sender's key is /// known), frontmatter fields, and the plain body — smol mail has no HTML. class MessageView extends ConsumerWidget { final String folder; final MailRecord record; final OpenedRecord opened; final void Function(String senderKey)? onNameSender; const MessageView({ super.key, required this.folder, required this.record, required this.opened, this.onNameSender, }); @override Widget build(BuildContext context, WidgetRef ref) { if (opened.error != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("", style: TextStyle(color: Theme.of(context).colorScheme.error)), ], ); } final store = ref.watch(storeProvider); final client = ref.read(clientProvider); // Sent copies carry no signature from a third party; the trust row then // just says what they are. final isSent = folder == "sent"; final isRequest = folder == "requests"; final senderKey = opened.sender; final known = senderKey == null ? null : store.addressForKey(senderKey); final contact = known == null ? null : store.contact(known); final verifiedSender = contact?.verified ?? false; final replyTo = isSent ? null : client.replyAddress(opened); final accepted = known == null ? null : store.accepted(known); final subject = opened.subject.isEmpty ? "(no subject)" : opened.subject; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(subject, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 14), if (isSent) _badge(context, "your own sealed copy") else _badge( context, verifiedSender ? "verified sender key" : known != null ? "key pinned on first use" : "key bound to no address", alert: known == null, ), if (isRequest) ...[ const SizedBox(height: 10), _badge(context, "arrived without an accept token (SPEC.md §5.8)", alert: true), ], if (!isSent && senderKey != null) ...[ const SizedBox(height: 10), _keyBlock(context, "${fingerprint(senderKey)}\n$senderKey"), ], const SizedBox(height: 18), _field(context, isSent ? "To" : "From", isSent ? (record.recipient ?? "") : (known ?? "unknown sender")), _field(context, "Date", formatTime(opened.time)), for (final entry in opened.fields.entries) if (entry.key != "subject" && entry.key != "reply-to" && entry.key != "accept") _field(context, entry.key, entry.value), if (!isSent && replyTo != null) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _field(context, "Reply-To", replyTo.short)), TextButton( onPressed: () async { try { await client.saveReplyAddress(replyTo, senderKey!); ref.read(revisionProvider.notifier).bump(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Address saved for this sender")), ); } } catch (err) { if (context.mounted) { showErrorSnackBar(context, err.toString()); } } }, child: const Text("Save"), ), ], ), if (!isSent && senderKey != null && onNameSender != null) Align( alignment: Alignment.centerLeft, child: TextButton( onPressed: () => onNameSender!(senderKey), child: const Text("Name this sender"), ), ), if (isRequest && known != null && !(accepted?.active ?? false)) Align( alignment: Alignment.centerLeft, child: TextButton.icon( icon: const Icon(Icons.check_circle_outline, size: 18), label: const Text("Accept sender"), onPressed: () async { try { await client.acceptContact(known); ref.read(revisionProvider.notifier).bump(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("$known accepted")), ); } } catch (err) { if (context.mounted) showErrorSnackBar(context, err.toString()); } }, ), ), const SizedBox(height: 10), const Divider(height: 1), const SizedBox(height: 20), SelectableText( opened.body, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 25), if (!isSent && senderKey != null) Align( alignment: Alignment.centerRight, child: OutlinedButton.icon( icon: const Icon(Icons.reply, size: 18), label: const Text("Reply"), onPressed: () => AutoRouter.of(context).push( ComposeRoute( to: known ?? replyTo?.short ?? "", subject: replySubject(opened.subject), ), ), ), ), ], ); } Widget _badge(BuildContext context, String text, {bool alert = false}) { final scheme = Theme.of(context).colorScheme; final background = alert ? scheme.errorContainer : scheme.primaryContainer; final foreground = alert ? scheme.onErrorContainer : scheme.onPrimaryContainer; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: background, borderRadius: BorderRadius.circular(10), ), child: Text( text, style: Theme.of(context) .textTheme .labelSmall! .copyWith(color: foreground, fontWeight: FontWeight.w500), ), ); } /// Technical detail (fingerprint, full key) — quiet by design, so it never /// competes with the subject or body for attention. Widget _keyBlock(BuildContext context, String text) => Text( text, style: Theme.of(context).textTheme.bodySmall!.copyWith( fontSize: 12, height: 1.5, fontWeight: FontWeight.normal, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ); Widget _field(BuildContext context, String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 90, child: Text(label, style: Theme.of(context).textTheme.labelSmall), ), Expanded( child: Text(value, style: Theme.of(context).textTheme.bodySmall), ), ], ), ); }