feat: adopt smolmail protocol 1.1, adaptive nav shell, and server mail retention

This commit is contained in:
randogoth 2026-09-27 11:07:11 +03:00
parent eaaa3f2ede
commit c693e6fcb9
27 changed files with 1190 additions and 592 deletions

View file

@ -24,6 +24,34 @@ class ContactDetailScreen extends ConsumerStatefulWidget {
class _ContactDetailScreenState extends ConsumerState<ContactDetailScreen> {
bool resolving = false;
bool tokenBusy = false;
// §5.8: admitting or withdrawing a contact's accept token pushes the
// change to the server right away, since it only takes effect once the
// server holds the changed set.
Future<void> _toggleAccepted(bool currentlyAccepted) async {
final client = ref.read(clientProvider);
setState(() => tokenBusy = true);
try {
if (currentlyAccepted) {
await client.blockContact(widget.address);
} else {
await client.acceptContact(widget.address);
}
ref.read(revisionProvider.notifier).bump();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(currentlyAccepted
? "${widget.address} blocked; their mail now lands in requests"
: "${widget.address} accepted; their mail now lands in your main tier")),
);
}
} on SmolError catch (err) {
if (mounted) showErrorSnackBar(context, err.message);
} finally {
if (mounted) setState(() => tokenBusy = false);
}
}
Future<void> _reResolve() async {
final client = ref.read(clientProvider);
@ -54,7 +82,11 @@ class _ContactDetailScreenState extends ConsumerState<ContactDetailScreen> {
@override
Widget build(BuildContext context) {
final contact = ref.watch(storeProvider).contact(widget.address);
ref.watch(revisionProvider);
final store = ref.read(storeProvider);
final contact = store.contact(widget.address);
final accepted = store.accepted(widget.address);
final isAccepted = accepted != null && accepted.active;
return Scaffold(
appBar: AppBar(title: Text(widget.address)),
@ -69,6 +101,29 @@ class _ContactDetailScreenState extends ConsumerState<ContactDetailScreen> {
? "verified key"
: "key pinned on first use"),
const SizedBox(height: 20),
Text(
"Accept token (SPEC.md §5.8)",
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 5),
Text(
isAccepted
? "This contact's mail lands in your main tier."
: "This contact's mail lands in requests until accepted.",
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 10),
tokenBusy
? const Center(child: CircularProgressIndicator())
: TextButton.icon(
onPressed: () => _toggleAccepted(isAccepted),
icon: Icon(isAccepted ? Icons.block : Icons.check_circle_outline,
size: 18),
label: Text(isAccepted ? "Block" : "Accept"),
),
const SizedBox(height: 20),
const Divider(height: 1),
const SizedBox(height: 20),
_field(context, "fingerprint", fingerprint(contact.key)),
_field(context, "public key", b32encode(contact.key)),
Row(