// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'package:tatchat/config/app_config.dart'; import 'package:tatchat/l10n/l10n.dart'; import 'package:tatchat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:tatchat/utils/beautify_string_extension.dart'; import 'package:tatchat/utils/date_time_extension.dart'; import 'package:tatchat/widgets/layouts/max_width_body.dart'; import 'package:go_router/go_router.dart'; import 'package:material_ui/material_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ChatEncryptionSettingsView extends StatelessWidget { final ChatEncryptionSettingsController controller; const ChatEncryptionSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final room = controller.room; return StreamBuilder( stream: room.client.onSync.stream.where( (s) => s.rooms?.join?[room.id] != null || s.deviceLists != null, ), builder: (context, _) => Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close_outlined), onPressed: () => context.go('/rooms/${controller.roomId!}'), ), title: Text(L10n.of(context).encryption), actions: [ TextButton( onPressed: () => launchUrlString(AppConfig.encryptionTutorial), child: Text(L10n.of(context).help), ), ], ), body: MaxWidthBody( child: Column( mainAxisSize: .min, children: [ SwitchListTile( contentPadding: EdgeInsets.symmetric(horizontal: 16), title: Text(L10n.of(context).encryptThisChat), value: room.encrypted, onChanged: controller.enableEncryption, ), Divider(color: theme.dividerColor, height: 1), if (room.isDirectChat && room.encrypted) ...[ const SizedBox(height: 16), ListTile( title: Text(L10n.of(context).interactiveVerification), subtitle: Text( L10n.of(context).interactiveVerificationDescription, style: TextStyle(fontSize: 11), ), ), Padding( padding: const EdgeInsets.only( bottom: 16.0, left: 16, right: 16, ), child: SizedBox( width: double.infinity, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( backgroundColor: theme.colorScheme.primaryContainer, foregroundColor: theme.colorScheme.onPrimaryContainer, side: BorderSide( color: theme.colorScheme.onPrimaryContainer, width: 1, ), ), onPressed: controller.startVerification, icon: const Icon(Icons.verified_outlined), label: Text(L10n.of(context).verifyStart), ), ), ), Divider(color: theme.dividerColor, height: 1), ], if (room.encrypted) ...[ // TODO: Display device keys SelectionArea( child: FutureBuilder( future: room.requestParticipants(), builder: (context, snapshot) { final users = snapshot.data; if (users == null) { return Center( child: CircularProgressIndicator.adaptive(), ); } return Column( children: users.map((user) { final userDeviceKeys = room.client.userDeviceKeys[user.id]; final masterKey = userDeviceKeys?.masterKey; final tofuSince = masterKey?.trustOnFirstUseSince; return Column( mainAxisSize: .min, children: [ ListTile( leading: CircleAvatar( child: Text( (userDeviceKeys?.deviceKeys.length ?? 0) .toString(), ), ), title: Row( children: [ Expanded( child: Text( user.calcDisplayname(), maxLines: 1, overflow: .ellipsis, ), ), ], ), subtitle: Column( crossAxisAlignment: .start, mainAxisSize: .min, children: [ Text( user.id, maxLines: 1, overflow: .ellipsis, style: TextStyle( fontStyle: FontStyle.italic, ), ), Text( masterKey == null ? L10n.of(context).noUserKeyFound : masterKey.verified == true ? L10n.of(context).verified : tofuSince != null ? L10n.of(context).knownSince( tofuSince.localizedTime(context), ) : L10n.of(context).unverified, style: TextStyle( color: masterKey == null ? theme.colorScheme.onErrorContainer : masterKey.verified ? Colors.green : tofuSince != null ? theme.colorScheme.primary : null, ), ), ], ), trailing: IconButton( onPressed: () => controller.uncollapse(user.id), icon: Icon( controller.uncollapsedUserId == user.id ? Icons.keyboard_arrow_up_outlined : Icons.keyboard_arrow_down_outlined, ), ), ), if (controller.uncollapsedUserId == user.id && userDeviceKeys != null) ...[ ...userDeviceKeys.deviceKeys.values.map(( device, ) { final signedDevice = device .hasValidSignatureChain( verifiedOnly: false, verifiedByTheirMasterKey: true, ); return ListTile( title: Text( device.verified ? L10n.of(context).verified : device.blocked ? L10n.of(context).blocked : !signedDevice ? L10n.of(context).unsignedDevice : L10n.of(context).signedDevice, style: TextStyle( color: device.verified ? Colors.green : device.blocked ? theme.colorScheme.error : !signedDevice ? theme.colorScheme.onErrorContainer : theme.colorScheme.primary, ), ), subtitle: Text( device.curve25519Key?.beautified ?? L10n.of(context).noCurve25519KeyFound, ), trailing: IconButton( icon: Icon(Icons.block_outlined), style: IconButton.styleFrom( foregroundColor: device.blocked ? theme.colorScheme.error : null, ), tooltip: device.blocked ? L10n.of(context).unblockDevice : L10n.of(context).blockDevice, onPressed: () => controller.toggleBlocked(device), ), ); }), ], Divider(color: theme.dividerColor, height: 1), ], ); }).toList(), ); }, ), ), ] else Padding( padding: const EdgeInsets.all(16.0), child: Center( child: Text( L10n.of(context).encryptionNotEnabled, style: const TextStyle(fontStyle: FontStyle.italic), ), ), ), ], ), ), ), ); } }