Merge branch 'krille/nicer-encryption-page' into 'main'

design: New encryption page

See merge request famedly/fluffychat!1059
This commit is contained in:
Krille 2022-12-26 17:07:00 +00:00
commit 1294479974
7 changed files with 240 additions and 297 deletions

BIN
assets/encryption.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -2979,5 +2979,10 @@
"oldDisplayName": {} "oldDisplayName": {}
} }
}, },
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities." "newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
"encryptThisChat": "Encrypt this chat",
"endToEndEncryption": "End to end encryption",
"disableEncryptionWarning": "For security reasons you can not disable encryption in a chat, where it has been enabled before.",
"sorryThatsNotPossible": "Sorry... that is not possible",
"deviceKeys": "Device keys:"
} }

View File

@ -24,6 +24,8 @@ abstract class AppConfig {
static String get privacyUrl => _privacyUrl; static String get privacyUrl => _privacyUrl;
static const String enablePushTutorial = static const String enablePushTutorial =
'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services'; 'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services';
static const String encryptionTutorial =
'https://gitlab.com/famedly/fluffychat/-/wikis/How-to-use-end-to-end-encryption-in-FluffyChat';
static const String appId = 'im.fluffychat.FluffyChat'; static const String appId = 'im.fluffychat.FluffyChat';
static const String appOpenUrlScheme = 'im.fluffychat'; static const String appOpenUrlScheme = 'im.fluffychat';
static String _webBaseUrl = 'https://fluffychat.im/web'; static String _webBaseUrl = 'https://fluffychat.im/web';

View File

@ -1,93 +1,45 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
class EncryptionButton extends StatefulWidget { class EncryptionButton extends StatelessWidget {
final Room room; final Room room;
const EncryptionButton(this.room, {Key? key}) : super(key: key); const EncryptionButton(this.room, {Key? key}) : super(key: key);
@override
EncryptionButtonState createState() => EncryptionButtonState();
}
class EncryptionButtonState extends State<EncryptionButton> {
StreamSubscription? _onSyncSub;
void _enableEncryptionAction() async {
if (widget.room.encrypted) {
VRouter.of(context).toSegments(['rooms', widget.room.id, 'encryption']);
return;
}
if (widget.room.joinRules == JoinRules.public) {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
okLabel: L10n.of(context)!.ok,
message: L10n.of(context)!.noEncryptionForPublicRooms,
);
return;
}
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.enableEncryption,
message: widget.room.client.encryptionEnabled
? L10n.of(context)!.enableEncryptionWarning
: L10n.of(context)!.needPantalaimonWarning,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => widget.room.enableEncryption(),
);
// we want to enable the lock icon
setState(() {});
}
}
@override
void dispose() {
_onSyncSub?.cancel();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.room.encrypted) { return StreamBuilder<SyncUpdate>(
_onSyncSub ??= Matrix.of(context) stream: Matrix.of(context)
.client .client
.onSync .onSync
.stream .stream
.where((s) => s.deviceLists != null) .where((s) => s.deviceLists != null),
.listen((s) => setState(() {})); builder: (context, snapshot) {
} return FutureBuilder<EncryptionHealthState>(
return FutureBuilder<EncryptionHealthState>( future: room.calcEncryptionHealthState(),
future: widget.room.calcEncryptionHealthState(), builder: (BuildContext context, snapshot) => IconButton(
builder: (BuildContext context, snapshot) => IconButton( tooltip: room.encrypted
tooltip: widget.room.encrypted ? L10n.of(context)!.encrypted
? L10n.of(context)!.encrypted : L10n.of(context)!.encryptionNotEnabled,
: L10n.of(context)!.encryptionNotEnabled, icon: Icon(
icon: Icon( room.encrypted
widget.room.encrypted ? Icons.lock_outlined
? Icons.lock_outlined : Icons.lock_open_outlined,
: Icons.lock_open_outlined, size: 20,
size: 20, color: room.joinRules != JoinRules.public &&
color: widget.room.joinRules != JoinRules.public && !room.encrypted
!widget.room.encrypted ? Colors.red
? Colors.red : snapshot.data ==
: snapshot.data == EncryptionHealthState.unverifiedDevices EncryptionHealthState.unverifiedDevices
? Colors.orange ? Colors.orange
: null), : null),
onPressed: _enableEncryptionAction, onPressed: () => VRouter.of(context)
)); .toSegments(['rooms', room.id, 'encryption']),
));
});
} }
} }

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/encryption.dart'; import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
@ -19,49 +22,68 @@ class ChatEncryptionSettings extends StatefulWidget {
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> { class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
String? get roomId => VRouter.of(context).pathParameters['roomid']; String? get roomId => VRouter.of(context).pathParameters['roomid'];
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
Future<void> unblock(DeviceKeys key) async { Future<void> unblock(DeviceKeys key) async {
if (key.blocked) { if (key.blocked) {
await key.setBlocked(false); await key.setBlocked(false);
} }
} }
Future<void> onSelected( void enableEncryption(_) async {
BuildContext context, String action, DeviceKeys key) async { if (room.encrypted) {
final room = Matrix.of(context).client.getRoomById(roomId!); showOkAlertDialog(
switch (action) { context: context,
case 'verify': title: L10n.of(context)!.sorryThatsNotPossible,
await unblock(key); message: L10n.of(context)!.disableEncryptionWarning,
final req = key.startVerification(); );
req.onUpdate = () { return;
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
break;
case 'verify_user':
await unblock(key);
final req =
await room!.client.userDeviceKeys[key.userId]!.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
break;
case 'block':
if (key.directVerified) {
await key.setVerified(false);
}
await key.setBlocked(true);
setState(() {});
break;
case 'unblock':
await unblock(key);
setState(() {});
break;
} }
if (room.joinRules == JoinRules.public) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.noEncryptionForPublicRooms,
);
return;
}
if (!room.canChangeStateEvent(EventTypes.Encryption)) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.noPermission,
);
return;
}
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context)!.areYouSure,
message: L10n.of(context)!.enableEncryptionWarning,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
);
if (consent != OkCancelResult.ok) return;
await showFutureLoadingDialog(
context: context,
future: () => room.enableEncryption(),
);
}
void startVerification() async {
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() {});
}
};
await KeyVerificationDialog(request: req).show(context);
}
void toggleDeviceKey(DeviceKeys key) {
setState(() {
key.setBlocked(!key.blocked);
});
} }
@override @override

View File

@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/matrix_sdk_extensions.dart/device_extension.dart';
import '../../widgets/m2_popup_menu_button.dart';
class ChatEncryptionSettingsView extends StatelessWidget { class ChatEncryptionSettingsView extends StatelessWidget {
final ChatEncryptionSettingsController controller; final ChatEncryptionSettingsController controller;
@ -19,184 +16,148 @@ class ChatEncryptionSettingsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(controller.roomId!)!; final room = controller.room;
return StreamBuilder<Object>(
return Scaffold( stream: room.client.onSync.stream.where(
appBar: AppBar( (s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
leading: IconButton( builder: (context, _) => Scaffold(
icon: const Icon(Icons.close_outlined), appBar: AppBar(
onPressed: () => leading: IconButton(
VRouter.of(context).toSegments(['rooms', controller.roomId!]), icon: const Icon(Icons.close_outlined),
), onPressed: () => VRouter.of(context)
title: Text(L10n.of(context)!.tapOnDeviceToVerify), .toSegments(['rooms', controller.roomId!]),
elevation: 0, ),
), title: Text(L10n.of(context)!.endToEndEncryption),
body: MaxWidthBody( actions: [
withScrolling: true, TextButton(
child: Column( onPressed: () => launch(AppConfig.encryptionTutorial),
mainAxisSize: MainAxisSize.min, child: Text(L10n.of(context)!.help),
children: [ ),
ListTile( ],
title: Text(L10n.of(context)!.deviceVerifyDescription),
leading: CircleAvatar(
backgroundColor: Theme.of(context).secondaryHeaderColor,
foregroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.lock),
), ),
), body: ListView(
const Divider(height: 1), children: [
StreamBuilder( SwitchListTile(
stream: room.onUpdate.stream, secondary: CircleAvatar(
builder: (context, snapshot) { foregroundColor:
return FutureBuilder<List<DeviceKeys>>( Theme.of(context).colorScheme.onPrimaryContainer,
future: room.getUserDeviceKeys(), backgroundColor:
builder: (BuildContext context, snapshot) { Theme.of(context).colorScheme.primaryContainer,
if (snapshot.hasError) { child: const Icon(Icons.lock_outlined)),
return Center( title: Text(L10n.of(context)!.encryptThisChat),
child: Text( value: room.encrypted,
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'), onChanged: controller.enableEncryption,
); ),
} Center(
if (!snapshot.hasData) { child: Image.asset(
return const Center( 'assets/encryption.png',
child: CircularProgressIndicator.adaptive( width: 212,
strokeWidth: 2)); ),
} ),
final deviceKeys = snapshot.data!; const Divider(height: 1),
return ListView.builder( if (room.isDirectChat)
shrinkWrap: true, Padding(
physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(16.0),
itemCount: deviceKeys.length, child: Center(
itemBuilder: (BuildContext context, int i) => Column( child: ElevatedButton.icon(
mainAxisSize: MainAxisSize.min, onPressed: controller.startVerification,
children: <Widget>[ icon: const Icon(Icons.verified_outlined),
if (i == 0 || label: Text(L10n.of(context)!.verifyStart),
deviceKeys[i].userId !=
deviceKeys[i - 1].userId) ...{
const Divider(height: 1, thickness: 1),
M2PopupMenuButton(
onSelected: (dynamic action) => controller
.onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
final items = <PopupMenuEntry<String>>[];
if (room
.client
.userDeviceKeys[deviceKeys[i].userId]!
.verified ==
UserVerifiedStatus.unknown) {
items.add(PopupMenuItem(
value: 'verify_user',
child: Text(L10n.of(context)!.verifyUser),
));
}
return items;
},
child: ListTile(
leading: Avatar(
mxContent: room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.avatarUrl,
name: room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(),
),
title: Text(
room
.unsafeGetUserFromMemoryOrFallback(
deviceKeys[i].userId)
.calcDisplayname(),
),
subtitle: Text(
deviceKeys[i].userId,
style: const TextStyle(
fontWeight: FontWeight.w300),
),
),
),
},
M2PopupMenuButton(
onSelected: (dynamic action) => controller
.onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
final items = <PopupMenuEntry<String>>[];
if (deviceKeys[i].blocked ||
!deviceKeys[i].verified) {
items.add(PopupMenuItem(
value: deviceKeys[i].userId ==
room.client.userID
? 'verify'
: 'verify_user',
child: Text(L10n.of(context)!.verifyStart),
));
}
if (deviceKeys[i].blocked) {
items.add(PopupMenuItem(
value: 'unblock',
child:
Text(L10n.of(context)!.unblockDevice),
));
}
if (!deviceKeys[i].blocked) {
items.add(PopupMenuItem(
value: 'block',
child: Text(L10n.of(context)!.blockDevice),
));
}
return items;
},
child: ListTile(
leading: CircleAvatar(
foregroundColor: Colors.white,
backgroundColor: deviceKeys[i].color,
child: Icon(deviceKeys[i].icon),
),
title: Text(
deviceKeys[i].displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [
Text(
deviceKeys[i].deviceId!,
style: const TextStyle(
fontWeight: FontWeight.w300),
),
const Spacer(),
Text(
deviceKeys[i].blocked
? L10n.of(context)!.blocked
: deviceKeys[i].verified
? L10n.of(context)!.verified
: L10n.of(context)!.unverified,
style: TextStyle(
fontSize: 14,
color: deviceKeys[i].color,
),
),
],
),
),
),
],
), ),
); ),
}, ),
); if (room.encrypted) ...[
}), const Divider(height: 1),
], ListTile(
), title: Text(
), L10n.of(context)!.deviceKeys,
); style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) =>
FutureBuilder<List<DeviceKeys>>(
future: room.getUserDeviceKeys(),
builder: (BuildContext context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
);
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2));
}
final deviceKeys = snapshot.data!;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: deviceKeys.length,
itemBuilder: (BuildContext context, int i) =>
SwitchListTile(
value: !deviceKeys[i].blocked,
activeColor: deviceKeys[i].verified
? Colors.green
: Colors.orange,
onChanged: (_) => controller
.toggleDeviceKey(deviceKeys[i]),
title: Row(
children: [
Expanded(
child: Text(
deviceKeys[i].deviceId ??
L10n.of(context)!.unknownDevice,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0),
child: Chip(
label: Text(
deviceKeys[i].userId,
style: const TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
),
),
],
),
subtitle: Text(
deviceKeys[i]
.ed25519Key
?.replaceAllMapped(
RegExp(r'.{4}'),
(s) => '${s.group(0)} ') ??
L10n.of(context)!
.unknownEncryptionAlgorithm,
style:
const TextStyle(fontFamily: 'Mono'),
),
),
);
}),
),
] else
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
L10n.of(context)!.encryptionNotEnabled,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
],
),
));
} }
} }
extension on DeviceKeys {
Color get color => blocked
? Colors.red
: verified
? Colors.green
: Colors.orange;
}

View File

@ -64,6 +64,7 @@ class FluffyChatAppState extends State<FluffyChatApp> {
return VRouter( return VRouter(
key: FluffyChatApp.routerKey, key: FluffyChatApp.routerKey,
title: AppConfig.applicationName, title: AppConfig.applicationName,
debugShowCheckedModeBanner: false,
themeMode: themeMode, themeMode: themeMode,
theme: FluffyThemes.buildTheme(Brightness.light, primaryColor), theme: FluffyThemes.buildTheme(Brightness.light, primaryColor),
darkTheme: FluffyThemes.buildTheme(Brightness.dark, primaryColor), darkTheme: FluffyThemes.buildTheme(Brightness.dark, primaryColor),