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,37 +74,31 @@ class _UserListItem extends State<GroupListItemComp> {
});
});
_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<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,97 +207,99 @@ class _UserListItem extends State<GroupListItemComp> {
@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<void>(
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,
),
);
},
);
}
}

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,287 +237,290 @@ class _MessageInputState extends State<MessageInput> {
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(

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,
],
),
);
},
);
},
);
}
}