kirakira/lib/presentation/screens/contacts_screen.dart

134 lines
4.7 KiB
Dart

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/presentation/theme/app_colors.dart";
import "package:smol_mail/shared/utils/snackbar.dart";
import "package:smol_mail/smol/errors.dart";
import "package:smol_mail/smol/ui.dart";
/// Contacts: the addresses bound to keys, with the trust badge and the number
/// of keys each has replaced — the §8 surface for noticing rotations.
@RoutePage()
class ContactsScreen extends ConsumerWidget {
const ContactsScreen({super.key});
Future<void> _import(BuildContext context, WidgetRef ref) async {
final client = ref.read(clientProvider);
final controller = TextEditingController();
final text = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Theme.of(context).extension<AppColors>()!.cardFill,
title: const Text("Import contact"),
content: TextField(
controller: controller,
decoration: InputDecoration(
hintText: "smol://user@host/key",
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(controller.text),
child: const Text("Import"),
),
],
),
);
if (text == null || text.trim().isEmpty) return;
try {
client.importContact(text);
ref.read(revisionProvider.notifier).bump();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("contact imported (verified key)")),
);
}
} on SmolError catch (err) {
if (context.mounted) {
showErrorSnackBar(context, err.message);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final contacts = ref.watch(contactsProvider);
return Scaffold(
appBar: AppBar(
title: const Text("Contacts"),
actions: [
IconButton(
tooltip: "Import smol:// address",
onPressed: () => _import(context, ref),
icon: const Icon(Icons.person_add),
),
],
),
body: contacts.isEmpty
? Center(
child: Text(
"no contacts yet — import a smol:// address, or write to someone",
style: Theme.of(context).textTheme.labelSmall,
textAlign: TextAlign.center,
),
)
: ListView.builder(
itemCount: contacts.length,
itemBuilder: (ctx, i) {
final (address, contact) = contacts[i];
final accepted = ref.read(storeProvider).accepted(address);
return ListTile(
splashColor: Theme.of(context).extension<AppColors>()!.highlight,
onTap: () => AutoRouter.of(context)
.push(ContactDetailRoute(address: address)),
title: Text(address),
subtitle: Text(
fingerprint(contact.key),
style: Theme.of(context).textTheme.labelSmall,
),
trailing: Wrap(
spacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_chip(context, contact.verified ? "verified" : "tofu",
ok: contact.verified),
if (contact.history.isNotEmpty)
_chip(context, "${contact.history.length} previous",
ok: false),
if (accepted != null && accepted.active)
_chip(context, "accepted", ok: true),
],
),
);
},
),
);
}
Widget _chip(BuildContext context, String text, {required bool ok}) =>
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
border: Border.all(
color: ok
? Theme.of(context).primaryColor
: Theme.of(context).textTheme.labelSmall!.color!,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: ok
? Theme.of(context).primaryColor
: Theme.of(context).textTheme.labelSmall!.color,
),
),
);
}