import 'dart:async'; import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/contact_groups.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/select_contact_groups.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); final int userId; @override State createState() => _ContactViewState(); } class _ContactViewState extends State { Contact? _contact; List _memberOfGroups = []; late StreamSubscription _streamContact; late StreamSubscription> _streamMemberOfGroups; @override void initState() { super.initState(); _streamContact = twonlyDB.contactsDao.watchContact(widget.userId).listen(( update, ) { if (update != null) { setState(() { _contact = update; }); } }); _streamMemberOfGroups = twonlyDB.groupsDao .watchContactGroupMember(widget.userId) .listen((groups) async { if (!mounted) return; setState(() { _memberOfGroups = groups; }); }); } @override void dispose() { _streamContact.cancel(); _streamMemberOfGroups.cancel(); super.dispose(); } Future handleUserRemoveRequest(Contact contact) async { var delete = true; for (final groupM in _memberOfGroups) { final group = await twonlyDB.groupsDao.getGroup(groupM.groupId); if (group?.deletedContent ?? false) { await twonlyDB.groupsDao.deleteGroup(group!.groupId); } else { delete = false; } } if (!mounted) return; if (!delete) { showSnackbar(context, context.lang.deleteUserErrorMessage); return; } final remove = await showAlertDialog( context, context.lang.contactRemoveTitle( getContactDisplayName(contact, maxLength: 20), ), context.lang.contactRemoveBody, ); if (remove) { await twonlyDB.contactsDao.deleteContactByUserId(contact.userId); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } } } Future handleUserBlockRequest(Contact contact) async { final block = await showAlertDialog( context, context.lang.contactBlockTitle(getContactDisplayName(contact)), context.lang.contactBlockBody, ); if (block) { const update = ContactsCompanion(blocked: Value(true)); if (context.mounted) { await twonlyDB.contactsDao.updateContact(contact.userId, update); } if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } } } Future handleReportUser(Contact contact) async { final reason = await showReportDialog(context, contact); if (reason == null) return; final res = await rustApiResult( RustApi.reportUser(userId: contact.userId, reason: reason), ); if (!mounted) return; if (res.isSuccess) { showSnackbar( context, context.lang.userGotReported, level: SnackbarLevel.info, ); } else { showNetworkIssue(context); } } @override Widget build(BuildContext context) { if (_contact == null) return Container(); final contact = _contact!; return Scaffold( appBar: AppBar( title: const Text(''), ), body: ListView( key: ValueKey(contact.userId), children: [ Center( child: GestureDetector( onTap: () { // ignore: inference_failure_on_function_invocation showDialog( context: context, builder: (context) { return Dialog( backgroundColor: Colors.transparent, elevation: 0, child: Column( mainAxisSize: MainAxisSize.min, children: [ AvatarIcon(contactId: contact.userId, fontSize: 200), ], ), ); }, ); }, child: Padding( padding: const EdgeInsets.all(10), child: AvatarIcon(contactId: contact.userId, fontSize: 30), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( getContactDisplayName(contact, maxLength: 20), style: const TextStyle(fontSize: 20), ), Padding( padding: const EdgeInsets.only(left: 1, right: 8), child: VerificationBadgeComp( contact: contact, ), ), FlameCounterWidget( contactId: contact.userId, ), ], ), if (getContactDisplayName(contact) != contact.username) Center(child: Text('(${contact.username})')), const SizedBox(height: 50), BetterListTile( icon: FontAwesomeIcons.solidComments, text: context.lang.contactViewMessage, onTap: () async { final group = await twonlyDB.groupsDao.getDirectChat( contact.userId, ); if (group != null && context.mounted) { await context.push(Routes.chatsMessages(group.groupId)); } }, ), const Divider(), BetterListTile( icon: FontAwesomeIcons.pencil, text: context.lang.contactNickname, onTap: () async { final nickName = await showNicknameChangeDialog(context, contact); if (context.mounted && nickName != null && nickName != '') { final update = ContactsCompanion(nickName: Value(nickName)); await twonlyDB.contactsDao.updateContact( contact.userId, update, ); } }, ), SelectChatDeletionTimeListTitle( groupId: getUUIDforDirectChat( widget.userId, userService.currentUser.userId, ), ), ContactGroupsSubtitleBuilder( userId: contact.userId, builder: (context, subtitleWidget) { return BetterListTile( icon: FontAwesomeIcons.tag, text: context.lang.contactGroupsTitle, subtitle: subtitleWidget, onTap: () { context.navPush( SelectContactGroupsView(userId: contact.userId), ); }, ); }, ), const Divider(), RestoreFlameComp( contactId: widget.userId, ), VerificationExpansionTileComp( contact: contact, ), MutualGroupsExpansionTileComp( contact: contact, ), UserDiscoveryContactSettingsComp( contact: contact, ), const Divider(), BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, onTap: () => handleReportUser(contact), ), BetterListTile( icon: FontAwesomeIcons.ban, text: context.lang.contactBlock, onTap: () => handleUserBlockRequest(contact), ), BetterListTile( icon: FontAwesomeIcons.userMinus, iconSize: 16, color: Colors.red, text: context.lang.contactRemove, onTap: () => handleUserRemoveRequest(contact), ), if (userService.currentUser.isDeveloper) ...[ if (contact.signalVersion != SignalVersion.v2) BetterListTile( icon: FontAwesomeIcons.arrowsRotate, text: 'Update Connection to V2 (PQXDH)', onTap: () async { try { await RustApi.establishSignalSession( contactId: contact.userId, ); final updatedContact = await twonlyDB.contactsDao .getContactById(contact.userId); final isV2 = updatedContact?.signalVersion == SignalVersion.v2; if (context.mounted) { showSnackbar( context, isV2 ? 'Connection updated to V2 successfully' : 'Failed to update connection to V2', level: isV2 ? SnackbarLevel.success : SnackbarLevel.error, ); } } catch (e) { if (context.mounted) { showSnackbar( context, 'Failed to update connection to V2: $e', ); } } }, ), const Divider(), Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Center( child: Text( 'Encryption: ${switch (contact.signalVersion) { SignalVersion.v1 => 'Signal Protocol (v1)', SignalVersion.v2 => 'PQXDH (v2)', }}', style: const TextStyle(color: Colors.grey), ), ), ), ], ], ), ); } } Future showNicknameChangeDialog( BuildContext context, Contact contact, ) { final controller = TextEditingController( text: getContactDisplayName(contact), ); return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(context.lang.contactNickname), content: TextField( controller: controller, autofocus: true, textCapitalization: TextCapitalization.words, decoration: InputDecoration( hintText: context.lang.contactNicknameNew, ), ), actions: [ TextButton( child: Text(context.lang.cancel), onPressed: () { Navigator.of(context).pop(); // Close the dialog }, ), TextButton( child: Text(context.lang.ok), onPressed: () { Navigator.of( context, ).pop(controller.text); // Return the input text }, ), ], ); }, ); } Future showReportDialog( BuildContext context, Contact contact, ) { final controller = TextEditingController(); return showDialog( context: context, builder: (context) { return AlertDialog( title: Text( context.lang.reportUserTitle(getContactDisplayName(contact)), ), content: TextField( controller: controller, autofocus: true, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration(hintText: context.lang.reportUserReason), ), actions: [ TextButton( child: Text(context.lang.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: Text(context.lang.ok), onPressed: () { Navigator.of(context).pop(controller.text); }, ), ], ); }, ); }