finish verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-05-21 14:20:15 +02:00
parent 1ad304ec2e
commit 00cb615e56
9 changed files with 564 additions and 394 deletions

View file

@ -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

View file

@ -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';
}

View file

@ -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';
}

@ -1 +1 @@
Subproject commit 5f60fcc10450b40f75b3c170d27caa4398d84e4a
Subproject commit c33a4c3be99b38596abd0cfa91333db3a340dee2

View file

@ -4,4 +4,5 @@ enum SecurityProfile { normal, strict }
extension SecurityProfileExtension on SecurityProfile {
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
}

View file

@ -293,9 +293,10 @@ Future<List<int>> sha256File(File file) async {
List<TextSpan> 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 = <TextSpan>[];

View file

@ -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,26 +74,20 @@ class _UserListItem extends State<GroupListItemComp> {
});
});
_lastReactionStream = twonlyDB.reactionsDao
.watchLastReactions(widget.group.groupId)
.listen((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) {
_messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) {
protectUpdateState.protect(() async {
await updateState(_lastMessage, update);
});
});
_lastMediaFilesStream = twonlyDB.mediaFilesDao
.watchNewestMediaFiles()
.listen((mediaFiles) {
_lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
if (!mounted) return;
for (final mediaFile in mediaFiles) {
final index = _previewMediaFiles.indexWhere(
@ -125,9 +120,7 @@ class _UserListItem extends State<GroupListItemComp> {
_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<GroupListItemComp> {
}
}
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<GroupListItemComp> {
}
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<GroupListItemComp> {
}
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,6 +207,9 @@ class _UserListItem extends State<GroupListItemComp> {
@override
Widget build(BuildContext context) {
return StreamBuilder<void>(
stream: userService.onUserUpdated,
builder: (context, snapshot) {
return GroupContextMenu(
group: widget.group,
child: ListTile(
@ -230,7 +221,7 @@ class _UserListItem extends State<GroupListItemComp> {
const SizedBox(width: 3),
VerificationBadgeComp(
group: widget.group,
showOnlyIfVerified: true,
showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList,
clickable: false,
size: 12,
),
@ -265,8 +256,7 @@ class _UserListItem extends State<GroupListItemComp> {
),
const Text(''),
const SizedBox(width: 5),
if (_currentMessage != null)
LastMessageTimeComp(message: _currentMessage),
if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage),
FlameCounterWidget(
groupId: widget.group.groupId,
prefix: true,
@ -302,14 +292,14 @@ class _UserListItem extends State<GroupListItemComp> {
}
},
icon: FaIcon(
_hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera,
_hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150),
),
),
onTap: onTap,
),
);
},
);
}
}

View file

@ -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,11 +237,13 @@ class _MessageInputState extends State<MessageInput> {
flameOnRightSide: true,
),
),
Padding(
UnverifiedContactWarningComp(
group: widget.group,
child: Padding(
padding: const EdgeInsets.only(
bottom: 10,
left: 10,
top: 10,
top: 5,
),
child: Row(
children: [
@ -519,6 +522,7 @@ class _MessageInputState extends State<MessageInput> {
],
),
),
),
Offstage(
offstage: !_emojiShowing,
child: EmojiPicker(

View file

@ -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<void>(
stream: userService.onUserUpdated,
builder: (context, _) {
if (!userService.currentUser.securityProfile.showWarningForNonVerifiedContacts) {
return child;
}
return StreamBuilder<VerificationStatus>(
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,
],
),
);
},
);
},
);
}
}