From 00cb615e565678b8ae2c7904926cb9ea519ed147 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 21 May 2026 14:20:15 +0200 Subject: [PATCH] finish verification badge --- .../generated/app_localizations.dart | 24 + .../generated/app_localizations_de.dart | 15 + .../generated/app_localizations_en.dart | 14 + lib/src/localization/translations | 2 +- lib/src/services/profile.service.dart | 1 + lib/src/utils/misc.dart | 3 +- .../group_list_item.comp.dart | 250 ++++----- .../message_input.dart | 528 +++++++++--------- .../unverified_contact_warning.comp.dart | 121 ++++ 9 files changed, 564 insertions(+), 394 deletions(-) create mode 100644 lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 4476dafb..1d4500b3 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -3469,6 +3469,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Ask a friend'** String get replyAskAFriend; + + /// No description provided for @unverifiedWarningDirectTitle. + /// + /// In en, this message translates to: + /// **'Identity not verified in person'** + String get unverifiedWarningDirectTitle; + + /// No description provided for @unverifiedWarningGroupTitle. + /// + /// In en, this message translates to: + /// **'Not all members are verified in person'** + String get unverifiedWarningGroupTitle; + + /// No description provided for @unverifiedWarningBody. + /// + /// In en, this message translates to: + /// **'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'** + String get unverifiedWarningBody; + + /// No description provided for @unverifiedWarningButton. + /// + /// In en, this message translates to: + /// **'Verify now'** + String get unverifiedWarningButton; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index fe89b7e6..cfd37cb7 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1973,4 +1973,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get replyAskAFriend => 'Einen Freund fragen'; + + @override + String get unverifiedWarningDirectTitle => + 'Identität nicht persönlich verifiziert'; + + @override + String get unverifiedWarningGroupTitle => + 'Nicht alle Mitglieder sind persönlich verifiziert'; + + @override + String get unverifiedWarningBody => + '*Teile keine geheimen Daten*. Jemand könnte sich *als dein Freund ausgeben*.'; + + @override + String get unverifiedWarningButton => 'Jetzt verifizieren'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 2b0122c5..3cfe0b43 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1958,4 +1958,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get replyAskAFriend => 'Ask a friend'; + + @override + String get unverifiedWarningDirectTitle => 'Identity not verified in person'; + + @override + String get unverifiedWarningGroupTitle => + 'Not all members are verified in person'; + + @override + String get unverifiedWarningBody => + '*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'; + + @override + String get unverifiedWarningButton => 'Verify now'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 5f60fcc1..c33a4c3b 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 5f60fcc10450b40f75b3c170d27caa4398d84e4a +Subproject commit c33a4c3be99b38596abd0cfa91333db3a340dee2 diff --git a/lib/src/services/profile.service.dart b/lib/src/services/profile.service.dart index 501d223e..5a80f5cb 100644 --- a/lib/src/services/profile.service.dart +++ b/lib/src/services/profile.service.dart @@ -4,4 +4,5 @@ enum SecurityProfile { normal, strict } extension SecurityProfileExtension on SecurityProfile { bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict; + bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal; } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 873bfae3..6d7414bf 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -293,9 +293,10 @@ Future> sha256File(File file) async { List formattedText( BuildContext context, String input, { + Color? textColor, Color? boldTextColor, }) { - final defaultColor = Theme.of(context).colorScheme.onSurface; + final defaultColor = textColor ?? Theme.of(context).colorScheme.onSurface; final regex = RegExp(r'\*(.*?)\*'); final spans = []; diff --git a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart index 225a3195..77a19625 100644 --- a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; +import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; @@ -73,37 +74,31 @@ class _UserListItem extends State { }); }); - _lastReactionStream = twonlyDB.reactionsDao - .watchLastReactions(widget.group.groupId) - .listen((update) { - if (!mounted) return; - setState(() { - _lastReaction = update; - }); - }); + _lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) { + if (!mounted) return; + setState(() { + _lastReaction = update; + }); + }); - _messagesNotOpenedStream = twonlyDB.messagesDao - .watchMessageNotOpened(widget.group.groupId) - .listen((update) { - protectUpdateState.protect(() async { - await updateState(_lastMessage, update); - }); - }); + _messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) { + protectUpdateState.protect(() async { + await updateState(_lastMessage, update); + }); + }); - _lastMediaFilesStream = twonlyDB.mediaFilesDao - .watchNewestMediaFiles() - .listen((mediaFiles) { - if (!mounted) return; - for (final mediaFile in mediaFiles) { - final index = _previewMediaFiles.indexWhere( - (t) => t.mediaId == mediaFile.mediaId, - ); - if (index >= 0) { - _previewMediaFiles[index] = mediaFile; - } - } - setState(() {}); - }); + _lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { + if (!mounted) return; + for (final mediaFile in mediaFiles) { + final index = _previewMediaFiles.indexWhere( + (t) => t.mediaId == mediaFile.mediaId, + ); + if (index >= 0) { + _previewMediaFiles[index] = mediaFile; + } + } + setState(() {}); + }); final groupContacts = await twonlyDB.groupsDao.getGroupContact( widget.group.groupId, @@ -125,9 +120,7 @@ class _UserListItem extends State { _previewMessages = []; } else if (newMessagesNotOpened.isNotEmpty) { // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. - final receivedMessages = newMessagesNotOpened - .where((x) => x.senderId != null) - .toList(); + final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); if (receivedMessages.isNotEmpty) { _previewMessages = receivedMessages; @@ -151,9 +144,7 @@ class _UserListItem extends State { } } - final msgs = _previewMessages - .where((x) => x.type == MessageType.media.name) - .toList(); + final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); if (msgs.isNotEmpty && msgs.first.type == MessageType.media.name && !msgs.first.isDeletedFromSender && @@ -165,8 +156,7 @@ class _UserListItem extends State { } for (final message in _previewMessages) { - if (message.mediaId != null && - !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( message.mediaId!, ); @@ -191,9 +181,7 @@ class _UserListItem extends State { } if (_hasNonOpenedMediaFile) { - final msgs = _previewMessages - .where((x) => x.type == MessageType.media.name) - .toList(); + final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( msgs.first.mediaId!, ); @@ -219,97 +207,99 @@ class _UserListItem extends State { @override Widget build(BuildContext context) { - return GroupContextMenu( - group: widget.group, - child: ListTile( - title: Row( - children: [ - Text( - substringBy(widget.group.groupName, 30), - ), - const SizedBox(width: 3), - VerificationBadgeComp( - group: widget.group, - showOnlyIfVerified: true, - clickable: false, - size: 12, - ), - ], - ), - subtitle: _receiverDeletedAccount - ? Text(context.lang.userDeletedAccount) - : (_currentMessage == null) - ? (widget.group.totalMediaCounter == 0) - ? Text(context.lang.chatsTapToSend) - : Row( - children: [ - LastMessageTimeComp( - dateTime: widget.group.lastMessageExchange, - ), - FlameCounterWidget( - groupId: widget.group.groupId, - prefix: true, - ), - ], - ) - : Row( - children: [ - TypingIndicatorSubtitleComp( - groupId: widget.group.groupId, - ), - MessageSendStateIcon( - _previewMessages, - _previewMediaFiles, - lastReaction: _lastReaction, - group: widget.group, - ), - const Text('•'), - const SizedBox(width: 5), - if (_currentMessage != null) - LastMessageTimeComp(message: _currentMessage), - FlameCounterWidget( - groupId: widget.group.groupId, - prefix: true, - ), - ], - ), - leading: GestureDetector( - onTap: () async { - if (widget.group.isDirectChat) { - final contacts = await twonlyDB.groupsDao.getGroupContact( - widget.group.groupId, - ); - if (!context.mounted) return; - await context.push(Routes.profileContact(contacts.first.userId)); - return; - } else { - await context.push(Routes.profileGroup(widget.group.groupId)); - } - }, - child: AvatarIcon(group: widget.group), - ), - trailing: (widget.group.leftGroup || _receiverDeletedAccount) - ? null - : IconButton( - onPressed: () { - if (_hasNonOpenedMediaFile) { - context.push(Routes.chatsMessages(widget.group.groupId)); - } else { - context.push( - Routes.chatsCameraSendTo, - extra: widget.group, - ); - } - }, - icon: FaIcon( - _hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + return GroupContextMenu( + group: widget.group, + child: ListTile( + title: Row( + children: [ + Text( + substringBy(widget.group.groupName, 30), ), - ), - onTap: onTap, - ), + const SizedBox(width: 3), + VerificationBadgeComp( + group: widget.group, + showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList, + clickable: false, + size: 12, + ), + ], + ), + subtitle: _receiverDeletedAccount + ? Text(context.lang.userDeletedAccount) + : (_currentMessage == null) + ? (widget.group.totalMediaCounter == 0) + ? Text(context.lang.chatsTapToSend) + : Row( + children: [ + LastMessageTimeComp( + dateTime: widget.group.lastMessageExchange, + ), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ) + : Row( + children: [ + TypingIndicatorSubtitleComp( + groupId: widget.group.groupId, + ), + MessageSendStateIcon( + _previewMessages, + _previewMediaFiles, + lastReaction: _lastReaction, + group: widget.group, + ), + const Text('•'), + const SizedBox(width: 5), + if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ), + leading: GestureDetector( + onTap: () async { + if (widget.group.isDirectChat) { + final contacts = await twonlyDB.groupsDao.getGroupContact( + widget.group.groupId, + ); + if (!context.mounted) return; + await context.push(Routes.profileContact(contacts.first.userId)); + return; + } else { + await context.push(Routes.profileGroup(widget.group.groupId)); + } + }, + child: AvatarIcon(group: widget.group), + ), + trailing: (widget.group.leftGroup || _receiverDeletedAccount) + ? null + : IconButton( + onPressed: () { + if (_hasNonOpenedMediaFile) { + context.push(Routes.chatsMessages(widget.group.groupId)); + } else { + context.push( + Routes.chatsCameraSendTo, + extra: widget.group, + ); + } + }, + icon: FaIcon( + _hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), + ), + onTap: onTap, + ), + ); + }, ); } } diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index 5b88e4ed..915a7d55 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -19,6 +19,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; +import 'package:twonly/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; @@ -236,287 +237,290 @@ class _MessageInputState extends State { flameOnRightSide: true, ), ), - Padding( - padding: const EdgeInsets.only( - bottom: 10, - left: 10, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 3, - ), - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - if (_recordingState != RecordingState.recording) - GestureDetector( - onTap: () { - setState(() { - _emojiShowing = !_emojiShowing; - if (_emojiShowing) { - widget.textFieldFocus.unfocus(); - } else { - widget.textFieldFocus.requestFocus(); - } - }); - }, - child: ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 12, - right: 8, - ), - child: FaIcon( - size: 20, - _emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile, + UnverifiedContactWarningComp( + group: widget.group, + child: Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 10, + top: 5, + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + if (_recordingState != RecordingState.recording) + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile, + ), ), ), ), - ), - Expanded( - child: Stack( - children: [ - TextField( - controller: _textFieldController, - focusNode: widget.textFieldFocus, - keyboardType: TextInputType.multiline, - showCursor: _recordingState != RecordingState.recording, - maxLines: 4, - minLines: 1, - onChanged: (value) async { - setState(() {}); - await twonlyDB.groupsDao.updateGroup( - widget.group.groupId, - GroupsCompanion( - draftMessage: Value( - _textFieldController.text, + Expanded( + child: Stack( + children: [ + TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + showCursor: _recordingState != RecordingState.recording, + maxLines: 4, + minLines: 1, + onChanged: (value) async { + setState(() {}); + await twonlyDB.groupsDao.updateGroup( + widget.group.groupId, + GroupsCompanion( + draftMessage: Value( + _textFieldController.text, + ), ), - ), - ); - }, - onSubmitted: (_) { - _sendMessage(); - }, - style: const TextStyle(fontSize: 17), - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - ), - ), - if (_recordingState == RecordingState.recording) - Container( - decoration: BoxDecoration( - color: context.color.surfaceContainer, - borderRadius: BorderRadius.circular(20), + ); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, ), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.only( - top: 14, - bottom: 14, - left: 12, - right: 8, - ), - child: FaIcon( - FontAwesomeIcons.microphone, - size: 20, - color: Colors.red, - ), - ), - const SizedBox(width: 10), - Text( - formatMsToMinSec( - _currentDuration, - ), - style: TextStyle( - color: isDarkMode(context) ? Colors.white : Colors.black, - fontSize: 12, - ), - ), - if (!_audioRecordingLock) ...[ - SizedBox( - width: (100 - _cancelSlideOffset) % 101, + ), + if (_recordingState == RecordingState.recording) + Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only( + top: 14, + bottom: 14, + left: 12, + right: 8, + ), + child: FaIcon( + FontAwesomeIcons.microphone, + size: 20, + color: Colors.red, + ), ), + const SizedBox(width: 10), Text( - context.lang.voiceMessageSlideToCancel, + formatMsToMinSec( + _currentDuration, + ), + style: TextStyle( + color: isDarkMode(context) ? Colors.white : Colors.black, + fontSize: 12, + ), ), - ] else ...[ - Expanded( - child: Container(), - ), - GestureDetector( - onTap: _cancelAudioRecording, - child: Text( - context.lang.voiceMessageCancel, - style: const TextStyle( - color: Colors.red, + if (!_audioRecordingLock) ...[ + SizedBox( + width: (100 - _cancelSlideOffset) % 101, + ), + Text( + context.lang.voiceMessageSlideToCancel, + ), + ] else ...[ + Expanded( + child: Container(), + ), + GestureDetector( + onTap: _cancelAudioRecording, + child: Text( + context.lang.voiceMessageCancel, + style: const TextStyle( + color: Colors.red, + ), ), ), - ), - const SizedBox(width: 20), + const SizedBox(width: 20), + ], ], - ], - ), - ), - ], - ), - ), - if (_textFieldController.text == '') - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, - ), - if (_textFieldController.text == '') - GestureDetector( - onLongPressMoveUpdate: (details) { - if (_audioRecordingLock) return; - if (_recordingOffset.dy - details.localPosition.dy >= 100) { - HapticFeedback.heavyImpact(); - setState(() { - _audioRecordingLock = true; - }); - } - if (_recordingOffset.dx - details.localPosition.dx >= 90 && - _recordingState == RecordingState.recording) { - _recordingState = RecordingState.none; - HapticFeedback.heavyImpact(); - _cancelAudioRecording(); - } - - setState(() { - final a = _recordingOffset.dx - details.localPosition.dx; - if (a > 0 && a <= 90) { - _cancelSlideOffset = _recordingOffset.dx - details.localPosition.dx; - } - }); - }, - onLongPressStart: (a) { - _recordingOffset = a.localPosition; - _startAudioRecording(); - }, - onLongPressCancel: _cancelAudioRecording, - onLongPressEnd: (a) { - if (_recordingState != RecordingState.recording) { - return; - } - if (!_audioRecordingLock) { - _stopAudioRecording(); - } - }, - child: Stack( - clipBehavior: Clip.none, - children: [ - if (_recordingState == RecordingState.recording && !_audioRecordingLock) - Positioned.fill( - top: -120, - left: -5, - child: Align( - alignment: AlignmentGeometry.topCenter, - child: Container( - padding: const EdgeInsets.only(top: 13), - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), - color: isDarkMode(context) ? Colors.black : Colors.white, - ), - child: const Center( - child: Column( - children: [ - FaIcon( - FontAwesomeIcons.lock, - size: 16, - ), - SizedBox(height: 5), - FaIcon( - FontAwesomeIcons.angleUp, - size: 16, - ), - ], - ), - ), - ), - ), - ), - if (_recordingState == RecordingState.recording && !_audioRecordingLock) - Positioned.fill( - top: -20, - left: -25, - bottom: -20, - right: -20, - child: Container( - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(90), - ), - width: 60, - height: 60, - ), - ), - if (!_audioRecordingLock) - ColoredBox( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8, - bottom: 8, - left: 8, - right: 12, - ), - child: FaIcon( - size: 20, - color: (_recordingState == RecordingState.recording) ? Colors.white : null, - (_recordingState == RecordingState.none) - ? FontAwesomeIcons.microphone - : (_recordingState == RecordingState.recording) - ? FontAwesomeIcons.stop - : FontAwesomeIcons.play, - ), ), ), ], ), ), - ], + if (_textFieldController.text == '') + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + if (_textFieldController.text == '') + GestureDetector( + onLongPressMoveUpdate: (details) { + if (_audioRecordingLock) return; + if (_recordingOffset.dy - details.localPosition.dy >= 100) { + HapticFeedback.heavyImpact(); + setState(() { + _audioRecordingLock = true; + }); + } + if (_recordingOffset.dx - details.localPosition.dx >= 90 && + _recordingState == RecordingState.recording) { + _recordingState = RecordingState.none; + HapticFeedback.heavyImpact(); + _cancelAudioRecording(); + } + + setState(() { + final a = _recordingOffset.dx - details.localPosition.dx; + if (a > 0 && a <= 90) { + _cancelSlideOffset = _recordingOffset.dx - details.localPosition.dx; + } + }); + }, + onLongPressStart: (a) { + _recordingOffset = a.localPosition; + _startAudioRecording(); + }, + onLongPressCancel: _cancelAudioRecording, + onLongPressEnd: (a) { + if (_recordingState != RecordingState.recording) { + return; + } + if (!_audioRecordingLock) { + _stopAudioRecording(); + } + }, + child: Stack( + clipBehavior: Clip.none, + children: [ + if (_recordingState == RecordingState.recording && !_audioRecordingLock) + Positioned.fill( + top: -120, + left: -5, + child: Align( + alignment: AlignmentGeometry.topCenter, + child: Container( + padding: const EdgeInsets.only(top: 13), + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + color: isDarkMode(context) ? Colors.black : Colors.white, + ), + child: const Center( + child: Column( + children: [ + FaIcon( + FontAwesomeIcons.lock, + size: 16, + ), + SizedBox(height: 5), + FaIcon( + FontAwesomeIcons.angleUp, + size: 16, + ), + ], + ), + ), + ), + ), + ), + if (_recordingState == RecordingState.recording && !_audioRecordingLock) + Positioned.fill( + top: -20, + left: -25, + bottom: -20, + right: -20, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(90), + ), + width: 60, + height: 60, + ), + ), + if (!_audioRecordingLock) + ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 8, + right: 12, + ), + child: FaIcon( + size: 20, + color: (_recordingState == RecordingState.recording) ? Colors.white : null, + (_recordingState == RecordingState.none) + ? FontAwesomeIcons.microphone + : (_recordingState == RecordingState.recording) + ? FontAwesomeIcons.stop + : FontAwesomeIcons.play, + ), + ), + ), + ], + ), + ), + ], + ), ), ), - ), - if (_textFieldController.text != '' || _audioRecordingLock) - IconButton( - padding: const EdgeInsets.all(15), - icon: FaIcon( - color: context.color.primary, - FontAwesomeIcons.solidPaperPlane, + if (_textFieldController.text != '' || _audioRecordingLock) + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _audioRecordingLock ? _stopAudioRecording : _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.plus), + padding: const EdgeInsets.all(15), + onPressed: () => _showAdditionalShareModal(context), ), - onPressed: _audioRecordingLock ? _stopAudioRecording : _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.plus), - padding: const EdgeInsets.all(15), - onPressed: () => _showAdditionalShareModal(context), - ), - ], + ], + ), ), ), Offstage( diff --git a/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart b/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart new file mode 100644 index 00000000..789c5763 --- /dev/null +++ b/lib/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/key_verification.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; + +class UnverifiedContactWarningComp extends StatelessWidget { + const UnverifiedContactWarningComp({ + required this.group, + required this.child, + super.key, + }); + + final Group group; + final Widget child; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, _) { + if (!userService.currentUser.securityProfile.showWarningForNonVerifiedContacts) { + return child; + } + return StreamBuilder( + stream: twonlyDB.keyVerificationDao.watchAllGroupMembersVerified(group.groupId), + builder: (context, snapshot) { + final status = snapshot.data; + if (status == null || status == VerificationStatus.trusted) { + return child; + } + + return Container( + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: context.color.errorContainer.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: context.color.error.withValues(alpha: 0.5)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), + child: Row( + children: [ + VerificationBadgeComp( + group: group, + size: 24, + clickable: false, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + group.isDirectChat + ? context.lang.unverifiedWarningDirectTitle + : context.lang.unverifiedWarningGroupTitle, + style: TextStyle( + color: context.color.onErrorContainer, + fontWeight: FontWeight.bold, + fontSize: 13, + height: 1.2, + ), + ), + const SizedBox(height: 4), + RichText( + text: TextSpan( + style: TextStyle( + color: context.color.onErrorContainer, + fontSize: 11, + ), + children: formattedText( + context, + context.lang.unverifiedWarningBody, + textColor: context.color.onErrorContainer, + ), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + SizedBox( + height: 30, + child: FilledButton.tonal( + style: FilledButton.styleFrom( + backgroundColor: context.color.onErrorContainer, + foregroundColor: context.color.errorContainer, + padding: const EdgeInsets.symmetric(horizontal: 10), + textStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), + ), + onPressed: () async { + if (group.isDirectChat) { + await context.push(Routes.settingsHelpFaqVerifyBadge); + } else { + await context.push(Routes.profileGroup(group.groupId)); + } + }, + child: Text(context.lang.unverifiedWarningButton), + ), + ), + ], + ), + ), + child, + ], + ), + ); + }, + ); + }, + ); + } +}