mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 06:42:12 +00:00
finish verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
1ad304ec2e
commit
00cb615e56
9 changed files with 564 additions and 394 deletions
|
|
@ -3469,6 +3469,30 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Ask a friend'**
|
/// **'Ask a friend'**
|
||||||
String get replyAskAFriend;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1973,4 +1973,19 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get replyAskAFriend => 'Einen Freund fragen';
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1958,4 +1958,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get replyAskAFriend => 'Ask a friend';
|
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
|
||||||
|
|
@ -4,4 +4,5 @@ enum SecurityProfile { normal, strict }
|
||||||
|
|
||||||
extension SecurityProfileExtension on SecurityProfile {
|
extension SecurityProfileExtension on SecurityProfile {
|
||||||
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
|
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
|
||||||
|
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,9 +293,10 @@ Future<List<int>> sha256File(File file) async {
|
||||||
List<TextSpan> formattedText(
|
List<TextSpan> formattedText(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String input, {
|
String input, {
|
||||||
|
Color? textColor,
|
||||||
Color? boldTextColor,
|
Color? boldTextColor,
|
||||||
}) {
|
}) {
|
||||||
final defaultColor = Theme.of(context).colorScheme.onSurface;
|
final defaultColor = textColor ?? Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
final regex = RegExp(r'\*(.*?)\*');
|
final regex = RegExp(r'\*(.*?)\*');
|
||||||
final spans = <TextSpan>[];
|
final spans = <TextSpan>[];
|
||||||
|
|
|
||||||
|
|
@ -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/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.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/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
|
|
@ -73,37 +74,31 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_lastReactionStream = twonlyDB.reactionsDao
|
_lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) {
|
||||||
.watchLastReactions(widget.group.groupId)
|
if (!mounted) return;
|
||||||
.listen((update) {
|
setState(() {
|
||||||
if (!mounted) return;
|
_lastReaction = update;
|
||||||
setState(() {
|
});
|
||||||
_lastReaction = update;
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
_messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) {
|
||||||
.watchMessageNotOpened(widget.group.groupId)
|
protectUpdateState.protect(() async {
|
||||||
.listen((update) {
|
await updateState(_lastMessage, update);
|
||||||
protectUpdateState.protect(() async {
|
});
|
||||||
await updateState(_lastMessage, update);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
_lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
||||||
.watchNewestMediaFiles()
|
if (!mounted) return;
|
||||||
.listen((mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
if (!mounted) return;
|
final index = _previewMediaFiles.indexWhere(
|
||||||
for (final mediaFile in mediaFiles) {
|
(t) => t.mediaId == mediaFile.mediaId,
|
||||||
final index = _previewMediaFiles.indexWhere(
|
);
|
||||||
(t) => t.mediaId == mediaFile.mediaId,
|
if (index >= 0) {
|
||||||
);
|
_previewMediaFiles[index] = mediaFile;
|
||||||
if (index >= 0) {
|
}
|
||||||
_previewMediaFiles[index] = mediaFile;
|
}
|
||||||
}
|
setState(() {});
|
||||||
}
|
});
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
|
|
||||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
widget.group.groupId,
|
widget.group.groupId,
|
||||||
|
|
@ -125,9 +120,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
_previewMessages = [];
|
_previewMessages = [];
|
||||||
} else if (newMessagesNotOpened.isNotEmpty) {
|
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||||
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||||
final receivedMessages = newMessagesNotOpened
|
final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||||
.where((x) => x.senderId != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (receivedMessages.isNotEmpty) {
|
if (receivedMessages.isNotEmpty) {
|
||||||
_previewMessages = receivedMessages;
|
_previewMessages = receivedMessages;
|
||||||
|
|
@ -151,9 +144,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||||
.where((x) => x.type == MessageType.media.name)
|
|
||||||
.toList();
|
|
||||||
if (msgs.isNotEmpty &&
|
if (msgs.isNotEmpty &&
|
||||||
msgs.first.type == MessageType.media.name &&
|
msgs.first.type == MessageType.media.name &&
|
||||||
!msgs.first.isDeletedFromSender &&
|
!msgs.first.isDeletedFromSender &&
|
||||||
|
|
@ -165,8 +156,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final message in _previewMessages) {
|
for (final message in _previewMessages) {
|
||||||
if (message.mediaId != null &&
|
if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||||
!_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
message.mediaId!,
|
message.mediaId!,
|
||||||
);
|
);
|
||||||
|
|
@ -191,9 +181,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_hasNonOpenedMediaFile) {
|
if (_hasNonOpenedMediaFile) {
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||||
.where((x) => x.type == MessageType.media.name)
|
|
||||||
.toList();
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
msgs.first.mediaId!,
|
msgs.first.mediaId!,
|
||||||
);
|
);
|
||||||
|
|
@ -219,97 +207,99 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GroupContextMenu(
|
return StreamBuilder<void>(
|
||||||
group: widget.group,
|
stream: userService.onUserUpdated,
|
||||||
child: ListTile(
|
builder: (context, snapshot) {
|
||||||
title: Row(
|
return GroupContextMenu(
|
||||||
children: [
|
group: widget.group,
|
||||||
Text(
|
child: ListTile(
|
||||||
substringBy(widget.group.groupName, 30),
|
title: Row(
|
||||||
),
|
children: [
|
||||||
const SizedBox(width: 3),
|
Text(
|
||||||
VerificationBadgeComp(
|
substringBy(widget.group.groupName, 30),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 3),
|
||||||
onTap: onTap,
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/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/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/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/chats/chat_messages_components/user_discovery_manual_approval.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.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,
|
flameOnRightSide: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
UnverifiedContactWarningComp(
|
||||||
padding: const EdgeInsets.only(
|
group: widget.group,
|
||||||
bottom: 10,
|
child: Padding(
|
||||||
left: 10,
|
padding: const EdgeInsets.only(
|
||||||
top: 10,
|
bottom: 10,
|
||||||
),
|
left: 10,
|
||||||
child: Row(
|
top: 5,
|
||||||
children: [
|
),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Container(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(
|
Expanded(
|
||||||
horizontal: 3,
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: 3,
|
||||||
color: context.color.surfaceContainer,
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
decoration: BoxDecoration(
|
||||||
),
|
color: context.color.surfaceContainer,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(20),
|
||||||
children: [
|
),
|
||||||
if (_recordingState != RecordingState.recording)
|
child: Row(
|
||||||
GestureDetector(
|
children: [
|
||||||
onTap: () {
|
if (_recordingState != RecordingState.recording)
|
||||||
setState(() {
|
GestureDetector(
|
||||||
_emojiShowing = !_emojiShowing;
|
onTap: () {
|
||||||
if (_emojiShowing) {
|
setState(() {
|
||||||
widget.textFieldFocus.unfocus();
|
_emojiShowing = !_emojiShowing;
|
||||||
} else {
|
if (_emojiShowing) {
|
||||||
widget.textFieldFocus.requestFocus();
|
widget.textFieldFocus.unfocus();
|
||||||
}
|
} else {
|
||||||
});
|
widget.textFieldFocus.requestFocus();
|
||||||
},
|
}
|
||||||
child: ColoredBox(
|
});
|
||||||
color: Colors.transparent,
|
},
|
||||||
child: Padding(
|
child: ColoredBox(
|
||||||
padding: const EdgeInsets.only(
|
color: Colors.transparent,
|
||||||
top: 8,
|
child: Padding(
|
||||||
bottom: 8,
|
padding: const EdgeInsets.only(
|
||||||
left: 12,
|
top: 8,
|
||||||
right: 8,
|
bottom: 8,
|
||||||
),
|
left: 12,
|
||||||
child: FaIcon(
|
right: 8,
|
||||||
size: 20,
|
),
|
||||||
_emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile,
|
child: FaIcon(
|
||||||
|
size: 20,
|
||||||
|
_emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
TextField(
|
||||||
TextField(
|
controller: _textFieldController,
|
||||||
controller: _textFieldController,
|
focusNode: widget.textFieldFocus,
|
||||||
focusNode: widget.textFieldFocus,
|
keyboardType: TextInputType.multiline,
|
||||||
keyboardType: TextInputType.multiline,
|
showCursor: _recordingState != RecordingState.recording,
|
||||||
showCursor: _recordingState != RecordingState.recording,
|
maxLines: 4,
|
||||||
maxLines: 4,
|
minLines: 1,
|
||||||
minLines: 1,
|
onChanged: (value) async {
|
||||||
onChanged: (value) async {
|
setState(() {});
|
||||||
setState(() {});
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
widget.group.groupId,
|
||||||
widget.group.groupId,
|
GroupsCompanion(
|
||||||
GroupsCompanion(
|
draftMessage: Value(
|
||||||
draftMessage: Value(
|
_textFieldController.text,
|
||||||
_textFieldController.text,
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
onSubmitted: (_) {
|
||||||
onSubmitted: (_) {
|
_sendMessage();
|
||||||
_sendMessage();
|
},
|
||||||
},
|
style: const TextStyle(fontSize: 17),
|
||||||
style: const TextStyle(fontSize: 17),
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
hintText: context.lang.chatListDetailInput,
|
||||||
hintText: context.lang.chatListDetailInput,
|
contentPadding: EdgeInsets.zero,
|
||||||
contentPadding: EdgeInsets.zero,
|
border: InputBorder.none,
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_recordingState == RecordingState.recording)
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.color.surfaceContainer,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
),
|
||||||
children: [
|
if (_recordingState == RecordingState.recording)
|
||||||
const Padding(
|
Container(
|
||||||
padding: EdgeInsets.only(
|
decoration: BoxDecoration(
|
||||||
top: 14,
|
color: context.color.surfaceContainer,
|
||||||
bottom: 14,
|
borderRadius: BorderRadius.circular(20),
|
||||||
left: 12,
|
),
|
||||||
right: 8,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
child: FaIcon(
|
const Padding(
|
||||||
FontAwesomeIcons.microphone,
|
padding: EdgeInsets.only(
|
||||||
size: 20,
|
top: 14,
|
||||||
color: Colors.red,
|
bottom: 14,
|
||||||
),
|
left: 12,
|
||||||
),
|
right: 8,
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
Text(
|
child: FaIcon(
|
||||||
formatMsToMinSec(
|
FontAwesomeIcons.microphone,
|
||||||
_currentDuration,
|
size: 20,
|
||||||
),
|
color: Colors.red,
|
||||||
style: TextStyle(
|
),
|
||||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_audioRecordingLock) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: (100 - _cancelSlideOffset) % 101,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
context.lang.voiceMessageSlideToCancel,
|
formatMsToMinSec(
|
||||||
|
_currentDuration,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
if (!_audioRecordingLock) ...[
|
||||||
Expanded(
|
SizedBox(
|
||||||
child: Container(),
|
width: (100 - _cancelSlideOffset) % 101,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
Text(
|
||||||
onTap: _cancelAudioRecording,
|
context.lang.voiceMessageSlideToCancel,
|
||||||
child: Text(
|
),
|
||||||
context.lang.voiceMessageCancel,
|
] else ...[
|
||||||
style: const TextStyle(
|
Expanded(
|
||||||
color: Colors.red,
|
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)
|
||||||
if (_textFieldController.text != '' || _audioRecordingLock)
|
IconButton(
|
||||||
IconButton(
|
padding: const EdgeInsets.all(15),
|
||||||
padding: const EdgeInsets.all(15),
|
icon: FaIcon(
|
||||||
icon: FaIcon(
|
color: context.color.primary,
|
||||||
color: context.color.primary,
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
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(
|
Offstage(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue