mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-23 20:49:26 +01:00
Merge branch 'krille/nicer-encryption-page' into 'main'
design: New encryption page See merge request famedly/fluffychat!1059
This commit is contained in:
commit
1294479974
BIN
assets/encryption.png
Normal file
BIN
assets/encryption.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
@ -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:"
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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']),
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user