diff --git a/lib/components/dialogs/simple_dialogs.dart b/lib/components/dialogs/simple_dialogs.dart index b45bf6d..639ba9b 100644 --- a/lib/components/dialogs/simple_dialogs.dart +++ b/lib/components/dialogs/simple_dialogs.dart @@ -14,6 +14,7 @@ class SimpleDialogs { String labelText, String prefixText, String suffixText, + bool password = false, bool multiLine = false, }) async { final TextEditingController controller = TextEditingController(); @@ -31,6 +32,7 @@ class SimpleDialogs { }, minLines: multiLine ? 3 : 1, maxLines: multiLine ? 3 : 1, + obscureText: password, textInputAction: multiLine ? TextInputAction.newline : null, decoration: InputDecoration( hintText: hintText, diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index b084ce2..2d9e5c8 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -60,16 +60,30 @@ class MatrixState extends State { BuildContext _loadingDialogContext; - Future tryRequestWithLoadingDialog(Future request) async { + Future tryRequestWithLoadingDialog(Future request, + {Function(MatrixException) onAdditionalAuth}) async { showLoadingDialog(context); - final dynamic = await tryRequestWithErrorToast(request); + final dynamic = await tryRequestWithErrorToast(request, + onAdditionalAuth: onAdditionalAuth); hideLoadingDialog(); return dynamic; } - Future tryRequestWithErrorToast(Future request) async { + Future tryRequestWithErrorToast(Future request, + {Function(MatrixException) onAdditionalAuth}) async { try { return await request; + } on MatrixException catch (exception) { + if (exception.requireAdditionalAuthentication && + onAdditionalAuth != null) { + return await tryRequestWithErrorToast(onAdditionalAuth(exception)); + } else { + Toast.show( + exception.errorMessage, + context, + duration: Toast.LENGTH_LONG, + ); + } } catch (exception) { Toast.show( exception.toString(), @@ -302,6 +316,17 @@ class MatrixState extends State { } } + Map getAuthByPassword(String password, String session) => { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": client.userID, + }, + "user": client.userID, + "password": password, + "session": session, + }; + @override void initState() { if (widget.client == null) { diff --git a/lib/i18n/i18n.dart b/lib/i18n/i18n.dart index 29c4548..ae7be8b 100644 --- a/lib/i18n/i18n.dart +++ b/lib/i18n/i18n.dart @@ -256,6 +256,8 @@ class I18n { String get deleteMessage => Intl.message("Delete message"); + String get devices => Intl.message("Devices"); + String get discardPicture => Intl.message("Discard picture"); String get displaynameHasBeenChanged => @@ -324,6 +326,8 @@ class I18n { String get homeserverIsNotCompatible => Intl.message("Homeserver is not compatible"); + String get id => Intl.message("ID"); + String get inviteContact => Intl.message("Invite contact"); String inviteContactToGroup(String groupName) => Intl.message( @@ -382,6 +386,8 @@ class I18n { args: [username], ); + String get lastSeenIp => Intl.message("Last seen IP"); + String get license => Intl.message("License"); String get loadingPleaseWait => Intl.message("Loading... Please wait"); @@ -473,12 +479,16 @@ class I18n { args: [username], ); + String get removeAllOtherDevices => Intl.message("Remove all other devices"); + String removedBy(String username) => Intl.message( "Removed by $username", name: "removedBy", args: [username], ); + String get removeDevice => Intl.message("Remove device"); + String get removeExile => Intl.message("Remove exile"); String get revokeAllPermissions => Intl.message("Revoke all permissions"); @@ -621,6 +631,8 @@ class I18n { String get unmuteChat => Intl.message('Unmute chat'); + String get unknownDevice => Intl.message("Unknown device"); + String unknownEvent(String type) => Intl.message( "Unknown event '$type'", name: "unknownEvent", diff --git a/lib/i18n/intl_de.arb b/lib/i18n/intl_de.arb index fe3423b..4cad9a3 100644 --- a/lib/i18n/intl_de.arb +++ b/lib/i18n/intl_de.arb @@ -340,6 +340,11 @@ "type": "text", "placeholders": {} }, + "Devices": "Geräte", + "@Devices": { + "type": "text", + "placeholders": {} + }, "Discard picture": "Bild verwerfen", "@Discard picture": { "type": "text", @@ -472,6 +477,11 @@ "type": "text", "placeholders": {} }, + "ID": "ID", + "@ID": { + "type": "text", + "placeholders": {} + }, "Invite contact": "Kontakt einladen", "@Invite contact": { "type": "text", @@ -565,6 +575,11 @@ "username": {} } }, + "Last seen IP": "Zuletzt bekannte IP", + "@Last seen IP": { + "type": "text", + "placeholders": {} + }, "License": "Lizenz", "@License": { "type": "text", @@ -735,6 +750,11 @@ "username": {} } }, + "Remove all other devices": "Alle anderen Geräte entfernen", + "@Remove all other devices": { + "type": "text", + "placeholders": {} + }, "removedBy": "Entfernt von {username}", "@removedBy": { "type": "text", @@ -742,6 +762,11 @@ "username": {} } }, + "Remove device": "Gerät entfernen", + "@Remove device": { + "type": "text", + "placeholders": {} + }, "Remove exile": "Verbannung aufheben", "@Remove exile": { "type": "text", @@ -991,6 +1016,11 @@ "type": "text", "placeholders": {} }, + "Unknown device": "Unbekanntes Gerät", + "@Unknown device": { + "type": "text", + "placeholders": {} + }, "unknownEvent": "Unbekanntes Event '{type}'", "@unknownEvent": { "type": "text", diff --git a/lib/i18n/intl_messages.arb b/lib/i18n/intl_messages.arb index 6039028..99fd684 100644 --- a/lib/i18n/intl_messages.arb +++ b/lib/i18n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2020-02-16T12:36:34.703154", + "@@last_modified": "2020-02-19T16:20:13.752724", "About": "About", "@About": { "type": "text", @@ -340,6 +340,11 @@ "type": "text", "placeholders": {} }, + "Devices": "Devices", + "@Devices": { + "type": "text", + "placeholders": {} + }, "Discard picture": "Discard picture", "@Discard picture": { "type": "text", @@ -472,6 +477,11 @@ "type": "text", "placeholders": {} }, + "ID": "ID", + "@ID": { + "type": "text", + "placeholders": {} + }, "Invite contact": "Invite contact", "@Invite contact": { "type": "text", @@ -565,6 +575,11 @@ "username": {} } }, + "Last seen IP": "Last seen IP", + "@Last seen IP": { + "type": "text", + "placeholders": {} + }, "License": "License", "@License": { "type": "text", @@ -735,6 +750,11 @@ "username": {} } }, + "Remove all other devices": "Remove all other devices", + "@Remove all other devices": { + "type": "text", + "placeholders": {} + }, "removedBy": "Removed by {username}", "@removedBy": { "type": "text", @@ -742,6 +762,11 @@ "username": {} } }, + "Remove device": "Remove device", + "@Remove device": { + "type": "text", + "placeholders": {} + }, "Remove exile": "Remove exile", "@Remove exile": { "type": "text", @@ -991,6 +1016,11 @@ "type": "text", "placeholders": {} }, + "Unknown device": "Unknown device", + "@Unknown device": { + "type": "text", + "placeholders": {} + }, "unknownEvent": "Unknown event '{type}'", "@unknownEvent": { "type": "text", diff --git a/lib/i18n/messages_de.dart b/lib/i18n/messages_de.dart index c6e64d4..b54faba 100644 --- a/lib/i18n/messages_de.dart +++ b/lib/i18n/messages_de.dart @@ -170,6 +170,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dark" : MessageLookupByLibrary.simpleMessage("Dunkel"), "Delete" : MessageLookupByLibrary.simpleMessage("Löschen"), "Delete message" : MessageLookupByLibrary.simpleMessage("Nachricht löschen"), + "Devices" : MessageLookupByLibrary.simpleMessage("Geräte"), "Discard picture" : MessageLookupByLibrary.simpleMessage("Bild verwerfen"), "Displayname has been changed" : MessageLookupByLibrary.simpleMessage("Anzeigename wurde geändert"), "Donate" : MessageLookupByLibrary.simpleMessage("Spenden"), @@ -193,11 +194,13 @@ class MessageLookup extends MessageLookupByLibrary { "Guests can join" : MessageLookupByLibrary.simpleMessage("Gäste dürfen beitreten"), "Help" : MessageLookupByLibrary.simpleMessage("Hilfe"), "Homeserver is not compatible" : MessageLookupByLibrary.simpleMessage("Homeserver ist nicht kompatibel"), + "ID" : MessageLookupByLibrary.simpleMessage("ID"), "Invite contact" : MessageLookupByLibrary.simpleMessage("Kontakt einladen"), "Invited" : MessageLookupByLibrary.simpleMessage("Eingeladen"), "Invited users only" : MessageLookupByLibrary.simpleMessage("Nur eingeladene Benutzer"), "It seems that you have no google services on your phone. That\'s a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/" : MessageLookupByLibrary.simpleMessage("Es sieht so aus als hättest du keine Google Dienste auf deinem Gerät. Das ist eine gute Entscheidung für deine Privatsphäre. Um Push Benachrichtigungen in FluffyChat zu erhalten, empfehlen wir die Verwendung von microG: https://microg.org/"), "Kick from chat" : MessageLookupByLibrary.simpleMessage("Aus dem Chat hinauswerfen"), + "Last seen IP" : MessageLookupByLibrary.simpleMessage("Zuletzt bekannte IP"), "Leave" : MessageLookupByLibrary.simpleMessage("Verlassen"), "Left the chat" : MessageLookupByLibrary.simpleMessage("Hat den Chat verlassen"), "License" : MessageLookupByLibrary.simpleMessage("Lizenz"), @@ -228,6 +231,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please enter your username" : MessageLookupByLibrary.simpleMessage("Bitte deinen Benutzernamen eingeben"), "Rejoin" : MessageLookupByLibrary.simpleMessage("Wieder beitreten"), "Remove" : MessageLookupByLibrary.simpleMessage("Entfernen"), + "Remove all other devices" : MessageLookupByLibrary.simpleMessage("Alle anderen Geräte entfernen"), + "Remove device" : MessageLookupByLibrary.simpleMessage("Gerät entfernen"), "Remove exile" : MessageLookupByLibrary.simpleMessage("Verbannung aufheben"), "Remove message" : MessageLookupByLibrary.simpleMessage("Nachricht entfernen"), "Reply" : MessageLookupByLibrary.simpleMessage("Antworten"), @@ -253,6 +258,7 @@ class MessageLookup extends MessageLookupByLibrary { "Thursday" : MessageLookupByLibrary.simpleMessage("Donnerstag"), "Try to send again" : MessageLookupByLibrary.simpleMessage("Nochmal versuchen zu senden"), "Tuesday" : MessageLookupByLibrary.simpleMessage("Tuesday"), + "Unknown device" : MessageLookupByLibrary.simpleMessage("Unbekanntes Gerät"), "Unmute chat" : MessageLookupByLibrary.simpleMessage("Stumm aus"), "Use Amoled compatible colors?" : MessageLookupByLibrary.simpleMessage("Amoled optimierte Farben verwenden?"), "Username" : MessageLookupByLibrary.simpleMessage("Benutzername"), diff --git a/lib/i18n/messages_messages.dart b/lib/i18n/messages_messages.dart index 7529315..942878d 100644 --- a/lib/i18n/messages_messages.dart +++ b/lib/i18n/messages_messages.dart @@ -170,6 +170,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dark" : MessageLookupByLibrary.simpleMessage("Dark"), "Delete" : MessageLookupByLibrary.simpleMessage("Delete"), "Delete message" : MessageLookupByLibrary.simpleMessage("Delete message"), + "Devices" : MessageLookupByLibrary.simpleMessage("Devices"), "Discard picture" : MessageLookupByLibrary.simpleMessage("Discard picture"), "Displayname has been changed" : MessageLookupByLibrary.simpleMessage("Displayname has been changed"), "Donate" : MessageLookupByLibrary.simpleMessage("Donate"), @@ -193,11 +194,13 @@ class MessageLookup extends MessageLookupByLibrary { "Guests can join" : MessageLookupByLibrary.simpleMessage("Guests can join"), "Help" : MessageLookupByLibrary.simpleMessage("Help"), "Homeserver is not compatible" : MessageLookupByLibrary.simpleMessage("Homeserver is not compatible"), + "ID" : MessageLookupByLibrary.simpleMessage("ID"), "Invite contact" : MessageLookupByLibrary.simpleMessage("Invite contact"), "Invited" : MessageLookupByLibrary.simpleMessage("Invited"), "Invited users only" : MessageLookupByLibrary.simpleMessage("Invited users only"), "It seems that you have no google services on your phone. That\'s a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/" : MessageLookupByLibrary.simpleMessage("It seems that you have no google services on your phone. That\'s a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/"), "Kick from chat" : MessageLookupByLibrary.simpleMessage("Kick from chat"), + "Last seen IP" : MessageLookupByLibrary.simpleMessage("Last seen IP"), "Leave" : MessageLookupByLibrary.simpleMessage("Leave"), "Left the chat" : MessageLookupByLibrary.simpleMessage("Left the chat"), "License" : MessageLookupByLibrary.simpleMessage("License"), @@ -228,6 +231,8 @@ class MessageLookup extends MessageLookupByLibrary { "Please enter your username" : MessageLookupByLibrary.simpleMessage("Please enter your username"), "Rejoin" : MessageLookupByLibrary.simpleMessage("Rejoin"), "Remove" : MessageLookupByLibrary.simpleMessage("Remove"), + "Remove all other devices" : MessageLookupByLibrary.simpleMessage("Remove all other devices"), + "Remove device" : MessageLookupByLibrary.simpleMessage("Remove device"), "Remove exile" : MessageLookupByLibrary.simpleMessage("Remove exile"), "Remove message" : MessageLookupByLibrary.simpleMessage("Remove message"), "Reply" : MessageLookupByLibrary.simpleMessage("Reply"), @@ -253,6 +258,7 @@ class MessageLookup extends MessageLookupByLibrary { "Thursday" : MessageLookupByLibrary.simpleMessage("Thursday"), "Try to send again" : MessageLookupByLibrary.simpleMessage("Try to send again"), "Tuesday" : MessageLookupByLibrary.simpleMessage("Tuesday"), + "Unknown device" : MessageLookupByLibrary.simpleMessage("Unknown device"), "Unmute chat" : MessageLookupByLibrary.simpleMessage("Unmute chat"), "Use Amoled compatible colors?" : MessageLookupByLibrary.simpleMessage("Use Amoled compatible colors?"), "Username" : MessageLookupByLibrary.simpleMessage("Username"), diff --git a/lib/views/settings.dart b/lib/views/settings.dart index c32b18f..99b5776 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/settings_devices.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:toast/toast.dart'; @@ -156,7 +157,16 @@ class _SettingsState extends State { ), ), ), - Divider(thickness: 1), + ListTile( + trailing: Icon(Icons.devices_other), + title: Text(I18n.of(context).devices), + onTap: () async => await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + DevicesSettingsView(), + ), + ), + ), ListTile( trailing: Icon(Icons.exit_to_app), title: Text(I18n.of(context).logout), diff --git a/lib/views/settings_devices.dart b/lib/views/settings_devices.dart new file mode 100644 index 0000000..9254c62 --- /dev/null +++ b/lib/views/settings_devices.dart @@ -0,0 +1,179 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; +import 'package:flutter/material.dart'; + +import '../utils/date_time_extension.dart'; +import '../components/adaptive_page_layout.dart'; +import '../components/matrix.dart'; +import '../i18n/i18n.dart'; +import 'chat_list.dart'; + +class DevicesSettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: DevicesSettings(), + ); + } +} + +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.requestUserDevices(); + return true; + } + + void reload() => setState(() => devices = null); + + void _removeDevicesAction( + BuildContext context, List devices) async { + if (await SimpleDialogs(context).askConfirmation() == false) return; + MatrixState matrix = Matrix.of(context); + List deviceIds = []; + for (UserDevice userDevice in devices) { + deviceIds.add(userDevice.deviceId); + } + final success = await matrix + .tryRequestWithLoadingDialog(matrix.client.deleteDevices(deviceIds), + onAdditionalAuth: (MatrixException exception) async { + final String password = await SimpleDialogs(context).enterText( + titleText: I18n.of(context).pleaseEnterYourPassword, + labelText: I18n.of(context).pleaseEnterYourPassword, + hintText: "******", + password: true); + if (password == null) return; + await matrix.client.deleteDevices(deviceIds, + auth: matrix.getAuthByPassword(password, exception.session)); + return; + }); + if (success != false) { + reload(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(I18n.of(context).devices)), + body: FutureBuilder( + future: _loadUserDevices(context), + builder: (BuildContext context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error), + Text(snapshot.error.toString()), + ], + ), + ); + } + if (!snapshot.hasData || this.devices == null) { + return Center(child: CircularProgressIndicator()); + } + Function isOwnDevice = (UserDevice userDevice) => + userDevice.deviceId == Matrix.of(context).client.deviceID; + final List devices = List.from(this.devices); + UserDevice thisDevice = + devices.firstWhere(isOwnDevice, orElse: () => null); + devices.removeWhere(isOwnDevice); + return Column( + children: [ + if (thisDevice != null) + UserDeviceListItem( + thisDevice, + remove: (d) => _removeDevicesAction(context, [d]), + ), + Divider(height: 1), + if (devices.isNotEmpty) + ListTile( + title: Text( + I18n.of(context).removeAllOtherDevices, + style: TextStyle(color: Colors.red), + ), + trailing: Icon(Icons.delete_outline), + onTap: () => _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], + remove: (d) => _removeDevicesAction(context, [d]), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +class UserDeviceListItem extends StatelessWidget { + final UserDevice userDevice; + final Function remove; + + const UserDeviceListItem(this.userDevice, {this.remove, Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + onSelected: (String action) { + if (action == "remove" && this.remove != null) { + remove(userDevice); + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: "remove", + child: Text(I18n.of(context).removeDevice, + style: TextStyle(color: Colors.red)), + ), + ], + child: ListTile( + contentPadding: EdgeInsets.all(16.0), + title: Row( + children: [ + Text((userDevice.displayName?.isNotEmpty ?? false) + ? userDevice.displayName + : I18n.of(context).unknownDevice), + Spacer(), + Text(userDevice.lastSeenTs.localizedTimeShort(context)), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${I18n.of(context).id}: ${userDevice.deviceId}"), + Text("${I18n.of(context).lastSeenIp}: ${userDevice.lastSeenIp}"), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e446727..3051a69 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -124,8 +124,8 @@ packages: dependency: "direct main" description: path: "." - ref: "083dd8eb295f0c6624e78d54f2ffeaa30de5de41" - resolved-ref: "083dd8eb295f0c6624e78d54f2ffeaa30de5de41" + ref: "6dd3b879b6cfcf1c7da9dedfa62626237afa6d9a" + resolved-ref: "6dd3b879b6cfcf1c7da9dedfa62626237afa6d9a" url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" @@ -377,8 +377,8 @@ packages: dependency: transitive description: path: "." - ref: "09eb49dbdb1ad9ed71c6bf74562250ecd3d4198b" - resolved-ref: "09eb49dbdb1ad9ed71c6bf74562250ecd3d4198b" + ref: "307dc133867eb5bf80d4f5c7412e58621dfca3cf" + resolved-ref: "307dc133867eb5bf80d4f5c7412e58621dfca3cf" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git version: "0.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 10980dc..c1aabe8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: ce1fd3ecd86999c0397208a707e359ec956ff52a + ref: 6dd3b879b6cfcf1c7da9dedfa62626237afa6d9a localstorage: ^3.0.1+4 bubble: ^1.1.9+1