Merge pull request #3005 from krille-chan/krille/permission-dialog-for-encryption

feat: Replace encryption button with unverified devices warning and p…
pull/3007/merge
Krille-chan 9 hours ago committed by GitHub
commit af0ad13fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2758,5 +2758,37 @@
"appSubtitle": "Secure [matrix] Communication",
"appDescription": "Communicate encrypted over the decentralized [matrix] network in an easy and accessible way for everyone.",
"interactiveVerification": "Interactive verification",
"interactiveVerificationDescription": "If you are next to each other or communicate via a secure channel then you can verify all devices at once by comparing a security number or emojis."
"interactiveVerificationDescription": "If you are next to each other or communicate via a secure channel then you can verify all devices at once by comparing a security number or emojis.",
"countUnverifiedDevices": "{count} unverified devices in the chat.",
"@countUnverifiedDevices": {
"type": "int",
"placeholders": {
"count": {
"type": "int"
}
}
},
"check": "Check",
"encryptedMessage": "Encrypted message",
"unencryptedMessage": "Unencrypted message",
"allow": "Allow",
"allowEncryptedCommunicationWith": "Allow encrypted communication with {name}?",
"@allowEncryptedCommunicationWith": {
"type": "String",
"placeholders": {
"name": {
"type": "String"
}
}
},
"publicKey": "Public key: {key}",
"@publicKey": {
"type": "String",
"placeholders": {
"key": {
"type": "String"
}
}
},
"onlyThisTime": "Only this time"
}

@ -17,6 +17,7 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat_view.dart';
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/start_poll_bottom_sheet.dart';
import 'package:fluffychat/pages/chat/trust_user_key_dialog.dart';
import 'package:fluffychat/pages/chat/utils/web_file_to_x_file.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
@ -276,7 +277,7 @@ class ChatController extends State<ChatPageWithRoom>
}
}
void _shareItems([_]) {
Future<void> _shareItems([_]) async {
final shareItems = widget.shareItems;
if (shareItems == null || shareItems.isEmpty) return;
if (!room.otherPartyCanReceiveMessages) {
@ -294,6 +295,8 @@ class ChatController extends State<ChatPageWithRoom>
);
return;
}
final proceed = await showTrustUserInRoomDialog(context, room);
if (!mounted || !proceed) return;
for (final item in shareItems) {
if (item is FileShareItem) continue;
if (item is TextShareItem) room.sendTextEvent(item.value);
@ -621,6 +624,8 @@ class ChatController extends State<ChatPageWithRoom>
});
Future<void> send() async {
final proceed = await showTrustUserInRoomDialog(context, room);
if (!mounted || !proceed) return;
if (sendController.text.trim().isEmpty) return;
_storeInputTimeoutTimer?.cancel();
final prefs = Matrix.of(context).store;
@ -825,6 +830,8 @@ class ChatController extends State<ChatPageWithRoom>
List<int> waveform,
String fileName,
) async {
final proceed = await showTrustUserInRoomDialog(context, room);
if (!mounted || !proceed) return;
final scaffoldMessenger = ScaffoldMessenger.of(context);
final audioFile = XFile(path);

@ -7,6 +7,7 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/sticker_picker_dialog.dart';
import 'package:fluffychat/pages/chat/trust_user_key_dialog.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -75,7 +76,12 @@ class ChatEmojiPicker extends StatelessWidget {
),
StickerPickerDialog(
room: controller.room,
onSelected: (sticker) {
onSelected: (sticker) async {
final proceed = await showTrustUserInRoomDialog(
context,
controller.room,
);
if (!proceed) return;
controller.room.sendEvent(
{
'body': sticker.body,

@ -7,6 +7,7 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/encrpytion_info.dart';
import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
@ -84,6 +85,7 @@ class ChatEventList extends StatelessWidget {
children: [
if (events.isNotEmpty) SeenByRow(event: events.first),
TypingIndicators(controller),
EncryptionInfo(room: controller.room),
],
);
}

@ -306,7 +306,9 @@ class ChatInputRow extends StatelessWidget {
top: 3.0,
),
counter: const SizedBox.shrink(),
hintText: L10n.of(context).writeAMessage,
hintText: controller.room.encrypted
? L10n.of(context).encryptedMessage
: L10n.of(context).unencryptedMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,

@ -14,7 +14,6 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
import 'package:fluffychat/pages/chat/chat_event_list.dart';
import 'package:fluffychat/pages/chat/encryption_button.dart';
import 'package:fluffychat/pages/chat/jitsi_popup_button.dart';
import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
@ -242,7 +241,6 @@ class ChatView extends StatelessWidget {
)
else if (AppSettings.jitsiFeature.value)
JitsiPopupButton(controller.room),
EncryptionButton(controller.room),
ChatSettingsPopupMenu(controller.room, true),
],
],

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
class EncryptionInfo extends StatelessWidget {
final Room room;
const EncryptionInfo({super.key, required this.room});
Future<int> _unverifiedDevices() async {
if (!room.encrypted) return 0;
final users = await room.requestParticipants();
final devicesKeysLists = users
.map((user) => room.client.userDeviceKeys[user.id])
.nonNulls;
final devices = devicesKeysLists.fold<List<DeviceKeys>>(
[],
(devices, devicesKeysList) => [
...devices,
...devicesKeysList.deviceKeys.values,
],
);
return devices
.where(
(device) =>
!device.verified && !device.blocked && device.encryptToDevice,
)
.length;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: FutureBuilder(
future: _unverifiedDevices(),
builder: (context, asyncSnapshot) {
final unverifiedDevices = asyncSnapshot.data ?? 0;
if (unverifiedDevices == 0) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Center(
child: Padding(
padding: const EdgeInsets.all(4),
child: Material(
color: theme.colorScheme.surface.withAlpha(128),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 3,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Icon(Icons.lock_person_outlined, size: 13),
),
TextSpan(text: ' '),
TextSpan(
text: L10n.of(
context,
).countUnverifiedDevices(unverifiedDevices),
),
TextSpan(text: ' '),
TextSpan(
style: TextStyle(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: theme.colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () =>
context.go('/rooms/${room.id}/encryption'),
text: 'Check',
),
],
),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11 * AppSettings.fontSizeFactor.value,
),
),
),
),
),
),
);
},
),
);
}
}

@ -1,67 +0,0 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:badges/badges.dart' as b;
import 'package:fluffychat/l10n/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/matrix.dart';
class EncryptionButton extends StatelessWidget {
final Room room;
const EncryptionButton(this.room, {super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return StreamBuilder<SyncUpdate>(
stream: Matrix.of(
context,
).client.onSync.stream.where((s) => s.deviceLists != null),
builder: (context, snapshot) {
final shouldBeEncrypted = room.joinRules != JoinRules.public;
return FutureBuilder<EncryptionHealthState>(
future: room.encrypted
? room.calcEncryptionHealthState()
: Future.value(EncryptionHealthState.allVerified),
builder: (BuildContext context, snapshot) => IconButton(
tooltip: room.encrypted
? L10n.of(context).encrypted
: L10n.of(context).encryptionNotEnabled,
icon: b.Badge(
badgeAnimation: const b.BadgeAnimation.fade(),
showBadge:
snapshot.data == EncryptionHealthState.unverifiedDevices,
badgeStyle: b.BadgeStyle(
badgeColor: theme.colorScheme.error,
elevation: 4,
),
badgeContent: Text(
'!',
style: TextStyle(
fontSize: 9,
color: theme.colorScheme.onError,
fontWeight: FontWeight.bold,
),
),
child: Icon(
room.encrypted
? Icons.lock_outlined
: Icons.no_encryption_outlined,
size: 20,
color: (shouldBeEncrypted && !room.encrypted)
? theme.colorScheme.error
: theme.colorScheme.onSurface,
),
),
onPressed: () => context.go('/rooms/${room.id}/encryption'),
),
);
},
);
}
}

@ -7,6 +7,7 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/trust_user_key_dialog.dart';
import 'package:fluffychat/utils/markdown_context_builder.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:flutter/material.dart';
@ -406,7 +407,9 @@ class InputBar extends StatelessWidget {
controller: controller,
),
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
onContentInserted: (KeyboardInsertedContent content) async {
final proceed = await showTrustUserInRoomDialog(context, room);
if (!proceed) return;
final data = content.data;
if (data == null) return;

@ -7,6 +7,7 @@ import 'package:async/async.dart' show Result;
import 'package:cross_file/cross_file.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/trust_user_key_dialog.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/other_party_can_receive.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -51,6 +52,9 @@ class SendFileDialogState extends State<SendFileDialog> {
Future<void> _send() async {
final l10n = L10n.of(context);
final proceed = await showTrustUserInRoomDialog(context, widget.room);
if (!context.mounted || !proceed) return;
showFutureLoadingDialog(
context: widget.outerContext,
title: l10n.sendingAttachment,

@ -4,6 +4,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/trust_user_key_dialog.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -32,6 +33,8 @@ class _StartPollBottomSheetState extends State<StartPollBottomSheet> {
String? _txid;
Future<void> _createPoll() async {
final proceed = await showTrustUserInRoomDialog(context, widget.room);
if (!proceed || !mounted) return;
try {
var id = 0;
_txid ??= widget.room.client.generateUniqueTransactionId();

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2019-Present Christian Kußowski
// SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/beautify_string_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
Future<bool> showTrustUserInRoomDialog(BuildContext context, Room room) async {
if (!room.encrypted) return true;
final users = await room.requestParticipants();
if (!context.mounted) return false;
users.removeWhere((user) {
if (user.id == room.client.userID) return true;
final keys = room.client.userDeviceKeys[user.id];
final masterKey = keys?.masterKey;
if (keys == null || masterKey == null || masterKey.verified) return true;
return false;
});
if (users.isEmpty) return true;
final l10n = L10n.of(context);
final theme = Theme.of(context);
final allow = await showAdaptiveDialog<bool>(
context: context,
builder: (context) => AlertDialog.adaptive(
title: Center(
child: Icon(
Icons.lock_outlined,
size: 32,
color: theme.colorScheme.primary,
),
),
content: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 128),
child: SelectionArea(
child: Column(
crossAxisAlignment: .stretch,
mainAxisSize: .min,
children: [
Center(
child: Text(
users.length == 1
? l10n.allowEncryptedCommunicationWith(
users.single.calcDisplayname(),
)
: 'Allow encrypted communication with ${users.length} users?',
style: TextStyle(fontSize: 16),
textAlign: .center,
),
),
const SizedBox(height: 16),
for (final user in users) ...[
Row(
children: [
Avatar(
name: user.calcDisplayname(),
mxContent: user.avatarUrl,
size: 14,
),
const SizedBox(width: 4),
Expanded(
child: Text(
user.calcDisplayname(),
style: theme.textTheme.labelSmall,
maxLines: 1,
textAlign: TextAlign.start,
),
),
],
),
DialogTextField(
controller: TextEditingController(
text: l10n.publicKey(
room
.client
.userDeviceKeys[user.id]
?.masterKey
?.publicKey
?.beautifiedOneLine ??
'???',
),
),
textStyle: theme.textTheme.labelSmall,
readOnly: true,
maxLines: 2,
),
],
],
),
),
),
actions: [
AdaptiveDialogAction(
bigButtons: true,
onPressed: () {
for (final user in users) {
room.client.userDeviceKeys[user.id]?.masterKey?.setVerified(true);
}
Navigator.of(context).pop(true);
},
child: Text(L10n.of(context).allow),
),
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop(true),
child: Text(L10n.of(context).onlyThisTime),
),
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop(false),
child: Text(l10n.cancel),
),
],
),
);
if (allow != true) return false;
return true;
}

@ -100,7 +100,11 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
void toggleVerified(DeviceKeys key) {
setState(() {
if (!key.verified && key.blocked) key.setBlocked(false);
key.setVerified(!key.verified);
if (key.crossVerified) {
room.client.userDeviceKeys[key.userId]?.masterKey?.setVerified(false);
} else {
key.setVerified(!key.verified);
}
});
}

@ -4,6 +4,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
extension BeautifyStringExtension on String {
String get beautifiedOneLine {
var beautifiedStr = '';
for (var i = 0; i < length; i++) {
beautifiedStr += substring(i, i + 1);
if (i % 4 == 3) {
beautifiedStr += ' ';
}
}
return beautifiedStr;
}
String get beautified {
var beautifiedStr = '';
for (var i = 0; i < length; i++) {

@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -22,6 +23,8 @@ class DialogTextField extends StatelessWidget {
final TextInputType? keyboardType;
final int? maxLength;
final bool autocorrect = true;
final bool readOnly;
final TextStyle? textStyle;
const DialogTextField({
super.key,
@ -38,6 +41,8 @@ class DialogTextField extends StatelessWidget {
this.counterText,
this.errorText,
this.obscureText = false,
this.readOnly = false,
this.textStyle,
});
@override
@ -52,6 +57,7 @@ class DialogTextField extends StatelessWidget {
case TargetPlatform.linux:
case TargetPlatform.windows:
return TextField(
readOnly: readOnly,
controller: controller,
obscureText: obscureText,
minLines: minLines,
@ -59,7 +65,18 @@ class DialogTextField extends StatelessWidget {
maxLength: maxLength,
keyboardType: keyboardType,
autocorrect: autocorrect,
style: textStyle,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
borderSide: BorderSide(color: theme.dividerColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
borderSide: BorderSide(color: theme.dividerColor),
),
filled: true,
fillColor: theme.colorScheme.surfaceBright,
errorText: errorText,
hintText: hintText,
labelText: labelText,
@ -77,12 +94,14 @@ class DialogTextField extends StatelessWidget {
height: placeholder == null ? null : ((maxLines ?? 1) + 1) * 20,
child: CupertinoTextField(
controller: controller,
readOnly: readOnly,
obscureText: obscureText,
minLines: minLines,
maxLines: maxLines,
maxLength: maxLength,
keyboardType: keyboardType,
autocorrect: autocorrect,
style: textStyle,
prefix: prefixText != null ? Text(prefixText) : null,
suffix: suffixText != null ? Text(suffixText) : null,
placeholder: placeholder,

@ -14,7 +14,15 @@ import 'package:matrix/matrix.dart';
import 'matrix.dart';
enum ChatPopupMenuActions { details, mute, unmute, emote, leave, search }
enum ChatPopupMenuActions {
details,
mute,
unmute,
encryption,
emote,
leave,
search,
}
class ChatSettingsPopupMenu extends StatefulWidget {
final Room room;
@ -101,6 +109,9 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
break;
case ChatPopupMenuActions.emote:
goToEmoteSettings();
case ChatPopupMenuActions.encryption:
context.go('/rooms/${widget.room.id}/encryption');
break;
}
},
itemBuilder: (BuildContext context) => [
@ -147,6 +158,16 @@ class ChatSettingsPopupMenuState extends State<ChatSettingsPopupMenu> {
],
),
),
PopupMenuItem<ChatPopupMenuActions>(
value: ChatPopupMenuActions.encryption,
child: Row(
children: [
const Icon(Icons.lock_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).encryption),
],
),
),
PopupMenuItem<ChatPopupMenuActions>(
value: ChatPopupMenuActions.emote,
child: Row(

Loading…
Cancel
Save