mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42:13 +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:
|
||||
/// **'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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -4,4 +4,5 @@ enum SecurityProfile { normal, strict }
|
|||
|
||||
extension SecurityProfileExtension on SecurityProfile {
|
||||
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(
|
||||
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>[];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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