mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-03 22:07:23 +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": {}
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "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 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';
 | 
			
		||||
 | 
			
		||||
@ -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']),
 | 
			
		||||
                  ));
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user