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": {}
}
},
"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 const String enablePushTutorial =
'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 appOpenUrlScheme = 'im.fluffychat';
static String _webBaseUrl = 'https://fluffychat.im/web';

View File

@ -1,93 +1,45 @@
import 'dart:async';
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/matrix.dart';
import 'package:vrouter/vrouter.dart';
import '../../widgets/matrix.dart';
class EncryptionButton extends StatefulWidget {
class EncryptionButton extends StatelessWidget {
final Room room;
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
Widget build(BuildContext context) {
if (widget.room.encrypted) {
_onSyncSub ??= Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.deviceLists != null)
.listen((s) => setState(() {}));
}
return FutureBuilder<EncryptionHealthState>(
future: widget.room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: widget.room.encrypted
? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled,
icon: Icon(
widget.room.encrypted
? Icons.lock_outlined
: Icons.lock_open_outlined,
size: 20,
color: widget.room.joinRules != JoinRules.public &&
!widget.room.encrypted
? Colors.red
: snapshot.data == EncryptionHealthState.unverifiedDevices
? Colors.orange
: null),
onPressed: _enableEncryptionAction,
));
return StreamBuilder<SyncUpdate>(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((s) => s.deviceLists != null),
builder: (context, snapshot) {
return FutureBuilder<EncryptionHealthState>(
future: room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: room.encrypted
? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled,
icon: Icon(
room.encrypted
? Icons.lock_outlined
: Icons.lock_open_outlined,
size: 20,
color: room.joinRules != JoinRules.public &&
!room.encrypted
? Colors.red
: snapshot.data ==
EncryptionHealthState.unverifiedDevices
? Colors.orange
: null),
onPressed: () => VRouter.of(context)
.toSegments(['rooms', room.id, 'encryption']),
));
});
}
}

View File

@ -1,5 +1,8 @@
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/matrix.dart';
import 'package:vrouter/vrouter.dart';
@ -19,49 +22,68 @@ class ChatEncryptionSettings extends StatefulWidget {
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
String? get roomId => VRouter.of(context).pathParameters['roomid'];
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
Future<void> unblock(DeviceKeys key) async {
if (key.blocked) {
await key.setBlocked(false);
}
}
Future<void> onSelected(
BuildContext context, String action, DeviceKeys key) async {
final room = Matrix.of(context).client.getRoomById(roomId!);
switch (action) {
case 'verify':
await unblock(key);
final req = key.startVerification();
req.onUpdate = () {
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;
void enableEncryption(_) async {
if (room.encrypted) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.sorryThatsNotPossible,
message: L10n.of(context)!.disableEncryptionWarning,
);
return;
}
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

View File

@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.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/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 {
final ChatEncryptionSettingsController controller;
@ -19,184 +16,148 @@ class ChatEncryptionSettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(controller.roomId!)!;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () =>
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
),
title: Text(L10n.of(context)!.tapOnDeviceToVerify),
elevation: 0,
),
body: MaxWidthBody(
withScrolling: true,
child: Column(
mainAxisSize: MainAxisSize.min,
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),
final room = controller.room;
return StreamBuilder<Object>(
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: () => VRouter.of(context)
.toSegments(['rooms', controller.roomId!]),
),
title: Text(L10n.of(context)!.endToEndEncryption),
actions: [
TextButton(
onPressed: () => launch(AppConfig.encryptionTutorial),
child: Text(L10n.of(context)!.help),
),
],
),
),
const Divider(height: 1),
StreamBuilder(
stream: room.onUpdate.stream,
builder: (context, snapshot) {
return 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) => Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (i == 0 ||
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,
),
),
],
),
),
),
],
body: ListView(
children: [
SwitchListTile(
secondary: CircleAvatar(
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
child: const Icon(Icons.lock_outlined)),
title: Text(L10n.of(context)!.encryptThisChat),
value: room.encrypted,
onChanged: controller.enableEncryption,
),
Center(
child: Image.asset(
'assets/encryption.png',
width: 212,
),
),
const Divider(height: 1),
if (room.isDirectChat)
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: ElevatedButton.icon(
onPressed: controller.startVerification,
icon: const Icon(Icons.verified_outlined),
label: Text(L10n.of(context)!.verifyStart),
),
);
},
);
}),
],
),
),
);
),
),
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(
key: FluffyChatApp.routerKey,
title: AppConfig.applicationName,
debugShowCheckedModeBanner: false,
themeMode: themeMode,
theme: FluffyThemes.buildTheme(Brightness.light, primaryColor),
darkTheme: FluffyThemes.buildTheme(Brightness.dark, primaryColor),