From 15731b977c16a5c0a2c280c6ef200a902af6c49e Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Thu, 15 Apr 2021 09:46:43 +0200 Subject: [PATCH] refactor: MVC device settings view --- lib/config/routes.dart | 2 +- .../device_settings_controller.dart | 140 +++++++ lib/views/device_settings_view.dart | 97 +++++ lib/views/settings_devices.dart | 344 ------------------ .../list_items/user_device_list_item.dart | 134 +++++++ 5 files changed, 372 insertions(+), 345 deletions(-) create mode 100644 lib/controllers/device_settings_controller.dart create mode 100644 lib/views/device_settings_view.dart delete mode 100644 lib/views/settings_devices.dart create mode 100644 lib/views/widgets/list_items/user_device_list_item.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 5348c0ca..28a2d335 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -20,7 +20,7 @@ import 'package:fluffychat/controllers/new_private_chat_controller.dart'; import 'package:fluffychat/views/search_view.dart'; import 'package:fluffychat/views/settings.dart'; import 'package:fluffychat/views/settings_3pid.dart'; -import 'package:fluffychat/views/settings_devices.dart'; +import 'package:fluffychat/controllers/device_settings_controller.dart'; import 'package:fluffychat/views/settings_emotes.dart'; import 'package:fluffychat/views/settings_ignore_list.dart'; import 'package:fluffychat/views/settings_multiple_emotes.dart'; diff --git a/lib/controllers/device_settings_controller.dart b/lib/controllers/device_settings_controller.dart new file mode 100644 index 00000000..10b40a73 --- /dev/null +++ b/lib/controllers/device_settings_controller.dart @@ -0,0 +1,140 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:famedlysdk/encryption/utils/key_verification.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/device_settings_view.dart'; +import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../views/widgets/matrix.dart'; + +class DevicesSettings extends StatefulWidget { + @override + DevicesSettingsController createState() => DevicesSettingsController(); +} + +class DevicesSettingsController extends State { + List devices; + Future loadUserDevices(BuildContext context) async { + if (devices != null) return true; + devices = await Matrix.of(context).client.requestDevices(); + return true; + } + + void reload() => setState(() => devices = null); + + bool loadingDeletingDevices = false; + String errorDeletingDevices; + + void removeDevicesAction(List devices) async { + if (await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).yes, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + ) == + OkCancelResult.cancel) return; + final matrix = Matrix.of(context); + final deviceIds = []; + for (final userDevice in devices) { + deviceIds.add(userDevice.deviceId); + } + + try { + setState(() { + loadingDeletingDevices = true; + errorDeletingDevices = null; + }); + await matrix.client.uiaRequestBackground( + (auth) => matrix.client.deleteDevices( + deviceIds, + auth: auth, + ), + ); + reload(); + } catch (e, s) { + Logs().v('Error while deleting devices', e, s); + setState(() => errorDeletingDevices = e.toString()); + } finally { + setState(() => loadingDeletingDevices = false); + } + } + + void renameDeviceAction(Device device) async { + final displayName = await showTextInputDialog( + context: context, + title: L10n.of(context).changeDeviceName, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + hintText: device.displayName, + ) + ], + ); + if (displayName == null) return; + final success = await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .client + .setDeviceMetadata(device.deviceId, displayName: displayName.single), + ); + if (success.error == null) { + reload(); + } + } + + void verifyDeviceAction(Device device) async { + final req = Matrix.of(context) + .client + .userDeviceKeys[Matrix.of(context).client.userID] + .deviceKeys[device.deviceId] + .startVerification(); + req.onUpdate = () { + if ({KeyVerificationState.error, KeyVerificationState.done} + .contains(req.state)) { + setState(() => null); + } + }; + await KeyVerificationDialog(request: req).show(context); + } + + void blockDeviceAction(Device device) async { + final key = Matrix.of(context) + .client + .userDeviceKeys[Matrix.of(context).client.userID] + .deviceKeys[device.deviceId]; + if (key.directVerified) { + await key.setVerified(false); + } + await key.setBlocked(true); + setState(() => null); + } + + void unblockDeviceAction(Device device) async { + final key = Matrix.of(context) + .client + .userDeviceKeys[Matrix.of(context).client.userID] + .deviceKeys[device.deviceId]; + await key.setBlocked(false); + setState(() => null); + } + + bool _isOwnDevice(Device userDevice) => + userDevice.deviceId == Matrix.of(context).client.deviceID; + + Device get thisDevice => devices.firstWhere( + _isOwnDevice, + orElse: () => null, + ); + + List get notThisDevice => List.from(devices) + ..removeWhere(_isOwnDevice) + ..sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs)); + + @override + Widget build(BuildContext context) => DevicesSettingsView(this); +} diff --git a/lib/views/device_settings_view.dart b/lib/views/device_settings_view.dart new file mode 100644 index 00000000..3b606d20 --- /dev/null +++ b/lib/views/device_settings_view.dart @@ -0,0 +1,97 @@ +import 'package:fluffychat/controllers/device_settings_controller.dart'; +import 'package:fluffychat/views/widgets/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'widgets/list_items/user_device_list_item.dart'; + +class DevicesSettingsView extends StatelessWidget { + final DevicesSettingsController controller; + + const DevicesSettingsView(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: BackButton(), + title: Text(L10n.of(context).devices), + ), + body: MaxWidthBody( + child: FutureBuilder( + future: controller.loadUserDevices(context), + builder: (BuildContext context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outlined), + Text(snapshot.error.toString()), + ], + ), + ); + } + if (!snapshot.hasData || controller.devices == null) { + return Center(child: CircularProgressIndicator()); + } + return Column( + children: [ + if (controller.thisDevice != null) + UserDeviceListItem( + controller.thisDevice, + rename: controller.renameDeviceAction, + remove: (d) => controller.removeDevicesAction([d]), + verify: controller.verifyDeviceAction, + block: controller.blockDeviceAction, + unblock: controller.unblockDeviceAction, + ), + Divider(height: 1), + if (controller.notThisDevice.isNotEmpty) + ListTile( + title: Text( + controller.errorDeletingDevices ?? + L10n.of(context).removeAllOtherDevices, + style: TextStyle(color: Colors.red), + ), + trailing: controller.loadingDeletingDevices + ? CircularProgressIndicator() + : Icon(Icons.delete_outline), + onTap: controller.loadingDeletingDevices + ? null + : () => controller + .removeDevicesAction(controller.notThisDevice), + ), + Divider(height: 1), + Expanded( + child: controller.notThisDevice.isEmpty + ? Center( + child: Icon( + Icons.devices_other, + size: 60, + color: Theme.of(context).secondaryHeaderColor, + ), + ) + : ListView.separated( + separatorBuilder: (BuildContext context, int i) => + Divider(height: 1), + itemCount: controller.notThisDevice.length, + itemBuilder: (BuildContext context, int i) => + UserDeviceListItem( + controller.notThisDevice[i], + rename: controller.renameDeviceAction, + remove: (d) => controller.removeDevicesAction([d]), + verify: controller.verifyDeviceAction, + block: controller.blockDeviceAction, + unblock: controller.unblockDeviceAction, + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/views/settings_devices.dart b/lib/views/settings_devices.dart deleted file mode 100644 index 73968b65..00000000 --- a/lib/views/settings_devices.dart +++ /dev/null @@ -1,344 +0,0 @@ -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:famedlysdk/encryption/utils/key_verification.dart'; -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart'; -import 'package:fluffychat/views/widgets/max_width_body.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import '../views/widgets/matrix.dart'; -import '../utils/date_time_extension.dart'; -import '../utils/device_extension.dart'; - -class DevicesSettings extends StatefulWidget { - @override - DevicesSettingsState createState() => DevicesSettingsState(); -} - -class DevicesSettingsState extends State { - List devices; - Future _loadUserDevices(BuildContext context) async { - if (devices != null) return true; - devices = await Matrix.of(context).client.requestDevices(); - return true; - } - - void reload() => setState(() => devices = null); - - bool _loadingDeletingDevices = false; - String _errorDeletingDevices; - - void _removeDevicesAction(BuildContext context, List devices) async { - if (await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).yes, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - ) == - OkCancelResult.cancel) return; - final matrix = Matrix.of(context); - final deviceIds = []; - for (final userDevice in devices) { - deviceIds.add(userDevice.deviceId); - } - - try { - setState(() { - _loadingDeletingDevices = true; - _errorDeletingDevices = null; - }); - await matrix.client.uiaRequestBackground( - (auth) => matrix.client.deleteDevices( - deviceIds, - auth: auth, - ), - ); - reload(); - } catch (e, s) { - Logs().v('Error while deleting devices', e, s); - setState(() => _errorDeletingDevices = e.toString()); - } finally { - setState(() => _loadingDeletingDevices = false); - } - } - - void _renameDeviceAction(BuildContext context, Device device) async { - final displayName = await showTextInputDialog( - context: context, - title: L10n.of(context).changeDeviceName, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - hintText: device.displayName, - ) - ], - ); - if (displayName == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .setDeviceMetadata(device.deviceId, displayName: displayName.single), - ); - if (success.error == null) { - reload(); - } - } - - void _verifyDeviceAction(BuildContext context, Device device) async { - final req = Matrix.of(context) - .client - .userDeviceKeys[Matrix.of(context).client.userID] - .deviceKeys[device.deviceId] - .startVerification(); - req.onUpdate = () { - if ({KeyVerificationState.error, KeyVerificationState.done} - .contains(req.state)) { - setState(() => null); - } - }; - await KeyVerificationDialog(request: req).show(context); - } - - void _blockDeviceAction(BuildContext context, Device device) async { - final key = Matrix.of(context) - .client - .userDeviceKeys[Matrix.of(context).client.userID] - .deviceKeys[device.deviceId]; - if (key.directVerified) { - await key.setVerified(false); - } - await key.setBlocked(true); - setState(() => null); - } - - void _unblockDeviceAction(BuildContext context, Device device) async { - final key = Matrix.of(context) - .client - .userDeviceKeys[Matrix.of(context).client.userID] - .deviceKeys[device.deviceId]; - await key.setBlocked(false); - setState(() => null); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: BackButton(), - title: Text(L10n.of(context).devices), - ), - body: MaxWidthBody( - child: FutureBuilder( - future: _loadUserDevices(context), - builder: (BuildContext context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.error_outlined), - Text(snapshot.error.toString()), - ], - ), - ); - } - if (!snapshot.hasData || this.devices == null) { - return Center(child: CircularProgressIndicator()); - } - final Function isOwnDevice = (Device userDevice) => - userDevice.deviceId == Matrix.of(context).client.deviceID; - final devices = List.from(this.devices); - final thisDevice = - devices.firstWhere(isOwnDevice, orElse: () => null); - devices.removeWhere(isOwnDevice); - devices.sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs)); - return Column( - children: [ - if (thisDevice != null) - UserDeviceListItem( - thisDevice, - rename: (d) => _renameDeviceAction(context, d), - remove: (d) => _removeDevicesAction(context, [d]), - verify: (d) => _verifyDeviceAction(context, d), - block: (d) => _blockDeviceAction(context, d), - unblock: (d) => _unblockDeviceAction(context, d), - ), - Divider(height: 1), - if (devices.isNotEmpty) - ListTile( - title: Text( - _errorDeletingDevices ?? - L10n.of(context).removeAllOtherDevices, - style: TextStyle(color: Colors.red), - ), - trailing: _loadingDeletingDevices - ? CircularProgressIndicator() - : Icon(Icons.delete_outline), - onTap: _loadingDeletingDevices - ? null - : () => _removeDevicesAction(context, devices), - ), - Divider(height: 1), - Expanded( - child: devices.isEmpty - ? Center( - child: Icon( - Icons.devices_other, - size: 60, - color: Theme.of(context).secondaryHeaderColor, - ), - ) - : ListView.separated( - separatorBuilder: (BuildContext context, int i) => - Divider(height: 1), - itemCount: devices.length, - itemBuilder: (BuildContext context, int i) => - UserDeviceListItem( - devices[i], - rename: (d) => _renameDeviceAction(context, d), - remove: (d) => _removeDevicesAction(context, [d]), - verify: (d) => _verifyDeviceAction(context, d), - block: (d) => _blockDeviceAction(context, d), - unblock: (d) => _unblockDeviceAction(context, d), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } -} - -enum UserDeviceListItemAction { - rename, - remove, - verify, - block, - unblock, -} - -class UserDeviceListItem extends StatelessWidget { - final Device userDevice; - final void Function(Device) remove; - final void Function(Device) rename; - final void Function(Device) verify; - final void Function(Device) block; - final void Function(Device) unblock; - - const UserDeviceListItem( - this.userDevice, { - @required this.remove, - @required this.rename, - @required this.verify, - @required this.block, - @required this.unblock, - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final keys = Matrix.of(context) - .client - .userDeviceKeys[Matrix.of(context).client.userID] - ?.deviceKeys[userDevice.deviceId]; - - return ListTile( - onTap: () async { - final action = await showModalActionSheet( - context: context, - actions: [ - SheetAction( - key: UserDeviceListItemAction.rename, - label: L10n.of(context).changeDeviceName, - ), - SheetAction( - key: UserDeviceListItemAction.verify, - label: L10n.of(context).verifyStart, - ), - if (keys != null) ...{ - if (!keys.blocked) - SheetAction( - key: UserDeviceListItemAction.block, - label: L10n.of(context).blockDevice, - isDestructiveAction: true, - ), - if (keys.blocked) - SheetAction( - key: UserDeviceListItemAction.unblock, - label: L10n.of(context).unblockDevice, - isDestructiveAction: true, - ), - SheetAction( - key: UserDeviceListItemAction.remove, - label: L10n.of(context).delete, - isDestructiveAction: true, - ), - }, - ], - ); - switch (action) { - case UserDeviceListItemAction.rename: - rename(userDevice); - break; - case UserDeviceListItemAction.remove: - remove(userDevice); - break; - case UserDeviceListItemAction.verify: - verify(userDevice); - break; - case UserDeviceListItemAction.block: - block(userDevice); - break; - case UserDeviceListItemAction.unblock: - unblock(userDevice); - break; - } - }, - leading: CircleAvatar( - foregroundColor: Theme.of(context).textTheme.bodyText1.color, - backgroundColor: Theme.of(context).secondaryHeaderColor, - child: Icon(userDevice.icon), - ), - title: Row( - children: [ - Text( - userDevice.displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Spacer(), - Text(userDevice.lastSeenTs.localizedTimeShort(context)), - ], - ), - subtitle: Row( - children: [ - Text(userDevice.deviceId), - Spacer(), - if (keys != null) - Text( - keys.blocked - ? L10n.of(context).blocked - : keys.verified - ? L10n.of(context).verified - : L10n.of(context).unknownDevice, - style: TextStyle( - color: keys.blocked - ? Colors.red - : keys.verified - ? Colors.green - : Colors.orange, - ), - ), - ], - ), - ); - } -} diff --git a/lib/views/widgets/list_items/user_device_list_item.dart b/lib/views/widgets/list_items/user_device_list_item.dart new file mode 100644 index 00000000..149511f3 --- /dev/null +++ b/lib/views/widgets/list_items/user_device_list_item.dart @@ -0,0 +1,134 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../views/widgets/matrix.dart'; +import '../../../utils/date_time_extension.dart'; +import '../../../utils/device_extension.dart'; + +enum UserDeviceListItemAction { + rename, + remove, + verify, + block, + unblock, +} + +class UserDeviceListItem extends StatelessWidget { + final Device userDevice; + final void Function(Device) remove; + final void Function(Device) rename; + final void Function(Device) verify; + final void Function(Device) block; + final void Function(Device) unblock; + + const UserDeviceListItem( + this.userDevice, { + @required this.remove, + @required this.rename, + @required this.verify, + @required this.block, + @required this.unblock, + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final keys = Matrix.of(context) + .client + .userDeviceKeys[Matrix.of(context).client.userID] + ?.deviceKeys[userDevice.deviceId]; + + return ListTile( + onTap: () async { + final action = await showModalActionSheet( + context: context, + actions: [ + SheetAction( + key: UserDeviceListItemAction.rename, + label: L10n.of(context).changeDeviceName, + ), + SheetAction( + key: UserDeviceListItemAction.verify, + label: L10n.of(context).verifyStart, + ), + if (keys != null) ...{ + if (!keys.blocked) + SheetAction( + key: UserDeviceListItemAction.block, + label: L10n.of(context).blockDevice, + isDestructiveAction: true, + ), + if (keys.blocked) + SheetAction( + key: UserDeviceListItemAction.unblock, + label: L10n.of(context).unblockDevice, + isDestructiveAction: true, + ), + SheetAction( + key: UserDeviceListItemAction.remove, + label: L10n.of(context).delete, + isDestructiveAction: true, + ), + }, + ], + ); + switch (action) { + case UserDeviceListItemAction.rename: + rename(userDevice); + break; + case UserDeviceListItemAction.remove: + remove(userDevice); + break; + case UserDeviceListItemAction.verify: + verify(userDevice); + break; + case UserDeviceListItemAction.block: + block(userDevice); + break; + case UserDeviceListItemAction.unblock: + unblock(userDevice); + break; + } + }, + leading: CircleAvatar( + foregroundColor: Theme.of(context).textTheme.bodyText1.color, + backgroundColor: Theme.of(context).secondaryHeaderColor, + child: Icon(userDevice.icon), + ), + title: Row( + children: [ + Text( + userDevice.displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Spacer(), + Text(userDevice.lastSeenTs.localizedTimeShort(context)), + ], + ), + subtitle: Row( + children: [ + Text(userDevice.deviceId), + Spacer(), + if (keys != null) + Text( + keys.blocked + ? L10n.of(context).blocked + : keys.verified + ? L10n.of(context).verified + : L10n.of(context).unknownDevice, + style: TextStyle( + color: keys.blocked + ? Colors.red + : keys.verified + ? Colors.green + : Colors.orange, + ), + ), + ], + ), + ); + } +}