kirakira/lib/presentation/widgets/message/message_view.dart

191 lines
6.7 KiB
Dart
Raw Normal View History

import "dart:typed_data";
import "package:auto_route/auto_route.dart";
import "package:flutter/material.dart";
2024-02-17 20:32:01 +05:30
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/proto.dart";
import "package:smol_mail/smol/store.dart";
2024-02-17 20:32:01 +05:30
/// 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 MailRecord record;
final OpenedRecord opened;
final void Function(Uint8List senderKey)? onNameSender;
2024-02-17 20:32:01 +05:30
const MessageView({
super.key,
required this.record,
required this.opened,
this.onNameSender,
});
2024-02-17 20:32:01 +05:30
@override
Widget build(BuildContext context, WidgetRef ref) {
if (opened.error != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("<unreadable: ${opened.error}>",
style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
);
}
final folder = ref.watch(folderProvider);
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 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 subject =
opened.subject.isEmpty ? "(no subject)" : opened.subject;
2024-02-17 20:32:01 +05:30
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
2024-02-17 20:32:01 +05:30
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 (!isSent && senderKey != null) ...[
const SizedBox(height: 10),
_keyBlock(context, "${fingerprint(senderKey)}\n${b32encode(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")
_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"),
),
],
2024-02-17 20:32:01 +05:30
),
if (!isSent && senderKey != null && onNameSender != null)
Align(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () => onNameSender!(senderKey),
child: const Text("Name this sender"),
),
),
const SizedBox(height: 10),
const Divider(height: 1),
const SizedBox(height: 20),
SelectableText(
opened.body,
style: Theme.of(context).textTheme.bodyMedium,
2024-02-17 20:32:01 +05:30
),
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),
),
),
),
),
2024-02-17 20:32:01 +05:30
],
);
}
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),
),
],
),
);
2024-02-17 20:32:01 +05:30
}