mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
implementing voice messages #251
This commit is contained in:
parent
3706a36cf9
commit
95c9db86d6
42 changed files with 1252 additions and 379 deletions
|
|
@ -219,6 +219,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
|||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||
.video: "hat ein Video{inGroup} gesendet.",
|
||||
.image: "hat ein Bild{inGroup} gesendet.",
|
||||
.audio: "hat eine Sprachnachricht{inGroup} gesendet.",
|
||||
.contactRequest: "möchte sich mit dir vernetzen.",
|
||||
.acceptRequest: "ist jetzt mit dir vernetzt.",
|
||||
.storedMediaFile: "hat dein Bild gespeichert.",
|
||||
|
|
@ -228,6 +229,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
|||
.reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
|
||||
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
|
||||
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
|
||||
.reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
|
||||
.response: "hat dir{inGroup} geantwortet.",
|
||||
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
|
||||
]
|
||||
|
|
@ -237,6 +239,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
|||
.twonly: "sent a twonly{inGroup}.",
|
||||
.video: "sent a video{inGroup}.",
|
||||
.image: "sent a image{inGroup}.",
|
||||
.audio: "sent a voice message{inGroup}.",
|
||||
.contactRequest: "wants to connect with you.",
|
||||
.acceptRequest: "is now connected with you.",
|
||||
.storedMediaFile: "has stored your image.",
|
||||
|
|
@ -246,6 +249,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
|||
.reactionToVideo: "has reacted with {{content}} to your video.",
|
||||
.reactionToText: "has reacted with {{content}} to your text.",
|
||||
.reactionToImage: "has reacted with {{content}} to your image.",
|
||||
.reactionToAudio: "has reacted with {{content}} to your voice message.",
|
||||
.response: "has responded{inGroup}.",
|
||||
.addedToGroup: "has added you to \"{{content}}\"",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
case reactionToVideo // = 11
|
||||
case reactionToText // = 12
|
||||
case reactionToImage // = 13
|
||||
case addedToGroup // = 14
|
||||
case reactionToAudio // = 14
|
||||
case addedToGroup // = 15
|
||||
case audio // = 16
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -60,7 +62,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
case 11: self = .reactionToVideo
|
||||
case 12: self = .reactionToText
|
||||
case 13: self = .reactionToImage
|
||||
case 14: self = .addedToGroup
|
||||
case 14: self = .reactionToAudio
|
||||
case 15: self = .addedToGroup
|
||||
case 16: self = .audio
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +85,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
case .reactionToVideo: return 11
|
||||
case .reactionToText: return 12
|
||||
case .reactionToImage: return 13
|
||||
case .addedToGroup: return 14
|
||||
case .reactionToAudio: return 14
|
||||
case .addedToGroup: return 15
|
||||
case .audio: return 16
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +108,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
|
|||
.reactionToVideo,
|
||||
.reactionToText,
|
||||
.reactionToImage,
|
||||
.reactionToAudio,
|
||||
.addedToGroup,
|
||||
.audio,
|
||||
]
|
||||
|
||||
}
|
||||
|
|
@ -218,7 +226,7 @@ struct PushKey: Sendable {
|
|||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
extension PushKind: SwiftProtobuf._ProtoNameProviding {
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}addedToGroup\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}reactionToAudio\0\u{1}addedToGroup\0\u{1}audio\0")
|
||||
}
|
||||
|
||||
extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
PODS:
|
||||
- audio_waveforms (0.0.1):
|
||||
- Flutter
|
||||
- background_downloader (0.0.1):
|
||||
- Flutter
|
||||
- camera_avfoundation (0.0.1):
|
||||
|
|
@ -247,6 +249,7 @@ PODS:
|
|||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
|
||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
|
|
@ -307,6 +310,8 @@ SPEC REPOS:
|
|||
- SwiftProtobuf
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audio_waveforms:
|
||||
:path: ".symlinks/plugins/audio_waveforms/ios"
|
||||
background_downloader:
|
||||
:path: ".symlinks/plugins/background_downloader/ios"
|
||||
camera_avfoundation:
|
||||
|
|
@ -369,6 +374,7 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
..where(
|
||||
(t) =>
|
||||
t.uploadState.equals(UploadState.initialized.name) |
|
||||
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
||||
t.uploadState.equals(UploadState.preprocessing.name),
|
||||
))
|
||||
.get();
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
mediaFiles.type.equals(MediaType.audio.name).not() &
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
messages.mediaId.isNotNull() &
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ enum MediaType {
|
|||
image,
|
||||
video,
|
||||
gif,
|
||||
audio,
|
||||
}
|
||||
|
||||
enum UploadState {
|
||||
|
|
|
|||
|
|
@ -648,6 +648,7 @@
|
|||
"appOutdatedBtn": "Jetzt aktualisieren.",
|
||||
"@appOutdatedBtn": {},
|
||||
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
|
||||
"uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.",
|
||||
"@doubleClickToReopen": {},
|
||||
"retransmissionRequested": "Wird erneut versucht.",
|
||||
"@retransmissionRequested": {},
|
||||
|
|
@ -784,6 +785,7 @@
|
|||
"notificationTwonly": "hat ein twonly{inGroup} gesendet.",
|
||||
"notificationVideo": "hat ein Video{inGroup} gesendet.",
|
||||
"notificationImage": "hat ein Bild{inGroup} gesendet.",
|
||||
"notificationAudio": "hat eine Sprachnachricht{inGroup} gesendet.",
|
||||
"notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.",
|
||||
"notificationContactRequest": "möchte sich mit dir vernetzen.",
|
||||
"notificationAcceptRequest": "ist jetzt mit dir vernetzt.",
|
||||
|
|
@ -793,6 +795,7 @@
|
|||
"notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.",
|
||||
"notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.",
|
||||
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
|
||||
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
|
||||
"notificationResponse": "hat dir{inGroup} geantwortet.",
|
||||
"notificationTitleUnknownUser": "Jemand",
|
||||
"notificationCategoryMessageTitle": "Nachrichten",
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@
|
|||
"appOutdated": "Your version of twonly is out of date.",
|
||||
"appOutdatedBtn": "Update Now",
|
||||
"doubleClickToReopen": "Double-click\nto open again",
|
||||
"uploadLimitReached": "The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.",
|
||||
"retransmissionRequested": "Retransmission requested",
|
||||
"testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!",
|
||||
"openChangeLog": "Open changelog automatically",
|
||||
|
|
@ -562,6 +563,7 @@
|
|||
"notificationTwonly": "sent a twonly{inGroup}.",
|
||||
"notificationVideo": "sent a video{inGroup}.",
|
||||
"notificationImage": "sent a image{inGroup}.",
|
||||
"notificationAudio": "sent a voice message{inGroup}.",
|
||||
"notificationAddedToGroup": "has added you to \"{groupname}\"",
|
||||
"notificationContactRequest": "wants to connect with you.",
|
||||
"notificationAcceptRequest": "is now connected with you.",
|
||||
|
|
@ -571,6 +573,7 @@
|
|||
"notificationReactionToVideo": "has reacted with {reaction} to your video.",
|
||||
"notificationReactionToText": "has reacted with {reaction} to your message.",
|
||||
"notificationReactionToImage": "has reacted with {reaction} to your image.",
|
||||
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
|
||||
"notificationResponse": "has responded{inGroup}.",
|
||||
"notificationTitleUnknownUser": "Someone",
|
||||
"notificationCategoryMessageTitle": "Messages",
|
||||
|
|
|
|||
|
|
@ -2072,6 +2072,12 @@ abstract class AppLocalizations {
|
|||
/// **'Double-click\nto open again'**
|
||||
String get doubleClickToReopen;
|
||||
|
||||
/// No description provided for @uploadLimitReached.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.'**
|
||||
String get uploadLimitReached;
|
||||
|
||||
/// No description provided for @retransmissionRequested.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2474,6 +2480,12 @@ abstract class AppLocalizations {
|
|||
/// **'sent a image{inGroup}.'**
|
||||
String notificationImage(Object inGroup);
|
||||
|
||||
/// No description provided for @notificationAudio.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'sent a voice message{inGroup}.'**
|
||||
String notificationAudio(Object inGroup);
|
||||
|
||||
/// No description provided for @notificationAddedToGroup.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2528,6 +2540,12 @@ abstract class AppLocalizations {
|
|||
/// **'has reacted with {reaction} to your image.'**
|
||||
String notificationReactionToImage(Object reaction);
|
||||
|
||||
/// No description provided for @notificationReactionToAudio.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'has reacted with {reaction} to your audio message.'**
|
||||
String notificationReactionToAudio(Object reaction);
|
||||
|
||||
/// No description provided for @notificationResponse.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get doubleClickToReopen => 'Doppelklicken zum\nerneuten Öffnen.';
|
||||
|
||||
@override
|
||||
String get uploadLimitReached =>
|
||||
'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.';
|
||||
|
||||
@override
|
||||
String get retransmissionRequested => 'Wird erneut versucht.';
|
||||
|
||||
|
|
@ -1352,6 +1356,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return 'hat ein Bild$inGroup gesendet.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationAudio(Object inGroup) {
|
||||
return 'hat eine Sprachnachricht$inGroup gesendet.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationAddedToGroup(Object groupname) {
|
||||
return 'hat dich zu \"$groupname\" hinzugefügt.';
|
||||
|
|
@ -1387,6 +1396,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return 'hat mit $reaction auf dein Bild reagiert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationReactionToAudio(Object reaction) {
|
||||
return 'hat mit $reaction auf deine Sprachnachricht reagiert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationResponse(Object inGroup) {
|
||||
return 'hat dir$inGroup geantwortet.';
|
||||
|
|
|
|||
|
|
@ -1091,6 +1091,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get doubleClickToReopen => 'Double-click\nto open again';
|
||||
|
||||
@override
|
||||
String get uploadLimitReached =>
|
||||
'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.';
|
||||
|
||||
@override
|
||||
String get retransmissionRequested => 'Retransmission requested';
|
||||
|
||||
|
|
@ -1344,6 +1348,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return 'sent a image$inGroup.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationAudio(Object inGroup) {
|
||||
return 'sent a voice message$inGroup.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationAddedToGroup(Object groupname) {
|
||||
return 'has added you to \"$groupname\"';
|
||||
|
|
@ -1379,6 +1388,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return 'has reacted with $reaction to your image.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationReactionToAudio(Object reaction) {
|
||||
return 'has reacted with $reaction to your audio message.';
|
||||
}
|
||||
|
||||
@override
|
||||
String notificationResponse(Object inGroup) {
|
||||
return 'has responded$inGroup.';
|
||||
|
|
|
|||
|
|
@ -71,12 +71,14 @@ class EncryptedContent_Media_Type extends $pb.ProtobufEnum {
|
|||
static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE');
|
||||
static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO');
|
||||
static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(3, _omitEnumNames ? '' : 'GIF');
|
||||
static const EncryptedContent_Media_Type AUDIO = EncryptedContent_Media_Type._(4, _omitEnumNames ? '' : 'AUDIO');
|
||||
|
||||
static const $core.List<EncryptedContent_Media_Type> values = <EncryptedContent_Media_Type> [
|
||||
REUPLOAD,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
GIF,
|
||||
AUDIO,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ const EncryptedContent_Media_Type$json = {
|
|||
{'1': 'IMAGE', '2': 1},
|
||||
{'1': 'VIDEO', '2': 2},
|
||||
{'1': 'GIF', '2': 3},
|
||||
{'1': 'AUDIO', '2': 4},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -409,7 +410,7 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo'
|
||||
'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE'
|
||||
'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH'
|
||||
'CgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW'
|
||||
'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW'
|
||||
'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD'
|
||||
'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG'
|
||||
'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1'
|
||||
|
|
@ -417,31 +418,31 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI'
|
||||
'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2'
|
||||
'5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu'
|
||||
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRV'
|
||||
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0'
|
||||
'SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl'
|
||||
'9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEK'
|
||||
'C01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYX'
|
||||
'RlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQi'
|
||||
'NgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAh'
|
||||
'p4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250'
|
||||
'YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEg'
|
||||
'oKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRD'
|
||||
'b250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGA'
|
||||
'IgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNl'
|
||||
'cm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZR'
|
||||
'ILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3Vz'
|
||||
'ZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3'
|
||||
'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ'
|
||||
'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG'
|
||||
'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r'
|
||||
'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm'
|
||||
'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv'
|
||||
'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG'
|
||||
'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf'
|
||||
'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW'
|
||||
'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l'
|
||||
'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX'
|
||||
'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi'
|
||||
'bGljS2V5');
|
||||
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV'
|
||||
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k'
|
||||
'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2'
|
||||
'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp'
|
||||
'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn'
|
||||
'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl'
|
||||
'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE'
|
||||
'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD'
|
||||
'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg'
|
||||
'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk'
|
||||
'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0'
|
||||
'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD'
|
||||
'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY'
|
||||
'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl'
|
||||
'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA'
|
||||
'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg'
|
||||
'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS'
|
||||
'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf'
|
||||
'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG'
|
||||
'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW'
|
||||
'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi'
|
||||
'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl'
|
||||
'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW'
|
||||
'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz'
|
||||
'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW'
|
||||
'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz'
|
||||
'ZW5kR3JvdXBQdWJsaWNLZXk=');
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ class PushKind extends $pb.ProtobufEnum {
|
|||
static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo');
|
||||
static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText');
|
||||
static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage');
|
||||
static const PushKind addedToGroup = PushKind._(14, _omitEnumNames ? '' : 'addedToGroup');
|
||||
static const PushKind reactionToAudio = PushKind._(14, _omitEnumNames ? '' : 'reactionToAudio');
|
||||
static const PushKind addedToGroup = PushKind._(15, _omitEnumNames ? '' : 'addedToGroup');
|
||||
static const PushKind audio = PushKind._(16, _omitEnumNames ? '' : 'audio');
|
||||
|
||||
static const $core.List<PushKind> values = <PushKind> [
|
||||
reaction,
|
||||
|
|
@ -45,7 +47,9 @@ class PushKind extends $pb.ProtobufEnum {
|
|||
reactionToVideo,
|
||||
reactionToText,
|
||||
reactionToImage,
|
||||
reactionToAudio,
|
||||
addedToGroup,
|
||||
audio,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ const PushKind$json = {
|
|||
{'1': 'reactionToVideo', '2': 11},
|
||||
{'1': 'reactionToText', '2': 12},
|
||||
{'1': 'reactionToImage', '2': 13},
|
||||
{'1': 'addedToGroup', '2': 14},
|
||||
{'1': 'reactionToAudio', '2': 14},
|
||||
{'1': 'addedToGroup', '2': 15},
|
||||
{'1': 'audio', '2': 16},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -41,7 +43,8 @@ final $typed_data.Uint8List pushKindDescriptor = $convert.base64Decode(
|
|||
'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw'
|
||||
'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ'
|
||||
'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0'
|
||||
'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEAoMYWRkZWRUb0dyb3VwEA4=');
|
||||
'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEwoPcmVhY3Rpb25Ub0F1ZGlvEA4SEAoMYWRkZWRUb0'
|
||||
'dyb3VwEA8SCQoFYXVkaW8QEA==');
|
||||
|
||||
@$core.Deprecated('Use encryptedPushNotificationDescriptor instead')
|
||||
const EncryptedPushNotification$json = {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ message EncryptedContent {
|
|||
IMAGE = 1;
|
||||
VIDEO = 2;
|
||||
GIF = 3;
|
||||
AUDIO = 4;
|
||||
}
|
||||
|
||||
string senderMessageId = 1;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ enum PushKind {
|
|||
reactionToVideo = 11;
|
||||
reactionToText = 12;
|
||||
reactionToImage = 13;
|
||||
addedToGroup = 14;
|
||||
reactionToAudio = 14;
|
||||
addedToGroup = 15;
|
||||
audio = 16;
|
||||
};
|
||||
|
||||
message PushNotification {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ Future<void> handleMedia(
|
|||
mediaType = MediaType.video;
|
||||
case EncryptedContent_Media_Type.GIF:
|
||||
mediaType = MediaType.gif;
|
||||
case EncryptedContent_Media_Type.AUDIO:
|
||||
mediaType = MediaType.audio;
|
||||
}
|
||||
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
|
|
|
|||
|
|
@ -30,38 +30,52 @@ Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
|||
enum DownloadMediaTypes {
|
||||
video,
|
||||
image,
|
||||
audio,
|
||||
}
|
||||
|
||||
Map<String, List<String>> defaultAutoDownloadOptions = {
|
||||
ConnectivityResult.mobile.name: [],
|
||||
ConnectivityResult.mobile.name: [
|
||||
DownloadMediaTypes.audio.name,
|
||||
],
|
||||
ConnectivityResult.wifi.name: [
|
||||
DownloadMediaTypes.video.name,
|
||||
DownloadMediaTypes.image.name,
|
||||
DownloadMediaTypes.audio.name,
|
||||
],
|
||||
};
|
||||
|
||||
Future<bool> isAllowedToDownload({required bool isVideo}) async {
|
||||
Future<bool> isAllowedToDownload(MediaType type) async {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
|
||||
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
||||
if (isVideo) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.video.name)) {
|
||||
return true;
|
||||
} else if (type == MediaType.audio) {
|
||||
if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.audio.name)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.image.name)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.image.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
if (isVideo) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.wifi.name]!
|
||||
.contains(DownloadMediaTypes.video.name)) {
|
||||
return true;
|
||||
}
|
||||
} else if (type == MediaType.audio) {
|
||||
if (options[ConnectivityResult.wifi.name]!
|
||||
.contains(DownloadMediaTypes.audio.name)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.wifi.name]!
|
||||
.contains(DownloadMediaTypes.image.name)) {
|
||||
return true;
|
||||
|
|
@ -110,8 +124,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!force &&
|
||||
!await isAllowedToDownload(isVideo: media.type == MediaType.video)) {
|
||||
if (!force && !await isAllowedToDownload(media.type)) {
|
||||
Log.warn(
|
||||
'Download blocked for ${media.mediaId} because of network state.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
final mediaId = update.task.taskId.replaceAll('upload_', '');
|
||||
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||
|
||||
if (update.status == TaskStatus.enqueued ||
|
||||
update.status == TaskStatus.running) {
|
||||
if (update.status == TaskStatus.running) {
|
||||
// Ignore these updates
|
||||
return;
|
||||
}
|
||||
|
|
@ -103,25 +102,34 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
Log.error(
|
||||
'Got HTTP error ${update.responseStatusCode} for $mediaId',
|
||||
);
|
||||
}
|
||||
|
||||
if (update.responseStatusCode == 429) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploadLimitReached),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (update.status == TaskStatus.notFound) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploadLimitReached),
|
||||
),
|
||||
);
|
||||
Log.info(
|
||||
'Background upload failed for $mediaId with status ${update.responseStatusCode}. Not trying again.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'Background upload failed for $mediaId with status ${update.status}. Trying again.',
|
||||
'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
|
||||
);
|
||||
|
||||
final mediaService = await MediaFileService.fromMedia(media);
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error(
|
||||
'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
|
||||
);
|
||||
final mediaService = await MediaFileService.fromMedia(media);
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploaded);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
await mediaService.setUploadState(UploadState.uploaded);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,8 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
|
|||
}
|
||||
}
|
||||
|
||||
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
|
||||
if (mediaService.mediaFile.uploadState == UploadState.uploading ||
|
||||
mediaService.mediaFile.uploadState == UploadState.uploadLimitReached) {
|
||||
await _uploadUploadRequest(mediaService);
|
||||
}
|
||||
}
|
||||
|
|
@ -180,11 +181,16 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
|
||||
final downloadToken = getRandomUint8List(32);
|
||||
|
||||
var type = EncryptedContent_Media_Type.IMAGE;
|
||||
if (media.mediaFile.type == MediaType.video) {
|
||||
type = EncryptedContent_Media_Type.VIDEO;
|
||||
} else if (media.mediaFile.type == MediaType.gif) {
|
||||
type = EncryptedContent_Media_Type.GIF;
|
||||
late EncryptedContent_Media_Type type;
|
||||
switch (media.mediaFile.type) {
|
||||
case MediaType.audio:
|
||||
type = EncryptedContent_Media_Type.AUDIO;
|
||||
case MediaType.image:
|
||||
type = EncryptedContent_Media_Type.IMAGE;
|
||||
case MediaType.gif:
|
||||
type = EncryptedContent_Media_Type.GIF;
|
||||
case MediaType.video:
|
||||
type = EncryptedContent_Media_Type.VIDEO;
|
||||
}
|
||||
|
||||
final notEncryptedContent = EncryptedContent(
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ class MediaFileService {
|
|||
// Message was not yet opened, so do not remove it.
|
||||
delete = false;
|
||||
}
|
||||
if (service.mediaFile.type == MediaType.audio) {
|
||||
delete = false; // do not delete voice messages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,6 +165,7 @@ class MediaFileService {
|
|||
}
|
||||
switch (mediaFile.type) {
|
||||
case MediaType.gif:
|
||||
case MediaType.audio:
|
||||
case MediaType.image:
|
||||
// all images are already compress..
|
||||
break;
|
||||
|
|
@ -181,6 +185,7 @@ class MediaFileService {
|
|||
await compressImage(originalPath, tempPath);
|
||||
case MediaType.video:
|
||||
await compressAndOverlayVideo(this);
|
||||
case MediaType.audio:
|
||||
case MediaType.gif:
|
||||
originalPath.copySync(tempPath.path);
|
||||
}
|
||||
|
|
@ -267,6 +272,8 @@ class MediaFileService {
|
|||
extension = 'mp4';
|
||||
case MediaType.gif:
|
||||
extension = 'gif';
|
||||
case MediaType.audio:
|
||||
extension = 'm4a';
|
||||
}
|
||||
}
|
||||
final mediaBaseDir =
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ String getPushNotificationText(PushNotification pushNotification) {
|
|||
PushKind.twonly.name: lang.notificationTwonly(inGroup),
|
||||
PushKind.video.name: lang.notificationVideo(inGroup),
|
||||
PushKind.image.name: lang.notificationImage(inGroup),
|
||||
PushKind.video.name: lang.notificationAudio(inGroup),
|
||||
PushKind.contactRequest.name: lang.notificationContactRequest,
|
||||
PushKind.acceptRequest.name: lang.notificationAcceptRequest,
|
||||
PushKind.storedMediaFile.name: lang.notificationStoredMediaFile,
|
||||
|
|
@ -256,6 +257,8 @@ String getPushNotificationText(PushNotification pushNotification) {
|
|||
PushKind.reopenedMedia.name: lang.notificationReopenedMedia,
|
||||
PushKind.reactionToVideo.name:
|
||||
lang.notificationReactionToVideo(pushNotification.additionalContent),
|
||||
PushKind.reactionToAudio.name:
|
||||
lang.notificationReactionToAudio(pushNotification.additionalContent),
|
||||
PushKind.reactionToText.name:
|
||||
lang.notificationReactionToText(pushNotification.additionalContent),
|
||||
PushKind.reactionToImage.name:
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
|||
switch (media.type) {
|
||||
case MediaType.image:
|
||||
kind = PushKind.reactionToImage;
|
||||
case MediaType.audio:
|
||||
kind = PushKind.reactionToAudio;
|
||||
case MediaType.video:
|
||||
kind = PushKind.reactionToVideo;
|
||||
case MediaType.gif:
|
||||
|
|
@ -241,13 +243,16 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
|||
}
|
||||
if (content.hasMedia()) {
|
||||
switch (content.media.type) {
|
||||
case EncryptedContent_Media_Type.REUPLOAD:
|
||||
return null;
|
||||
case EncryptedContent_Media_Type.IMAGE:
|
||||
kind = PushKind.image;
|
||||
case EncryptedContent_Media_Type.VIDEO:
|
||||
kind = PushKind.video;
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
return null;
|
||||
case EncryptedContent_Media_Type.GIF:
|
||||
kind = PushKind.image;
|
||||
case EncryptedContent_Media_Type.AUDIO:
|
||||
kind = PushKind.audio;
|
||||
}
|
||||
if (content.media.requiresAuthentication) {
|
||||
kind = PushKind.twonly;
|
||||
|
|
|
|||
|
|
@ -293,6 +293,8 @@ Color getMessageColorFromType(
|
|||
} else {
|
||||
if (mediaFile.type == MediaType.video) {
|
||||
color = const Color.fromARGB(255, 243, 33, 208);
|
||||
} else if (mediaFile.type == MediaType.audio) {
|
||||
color = const Color.fromARGB(255, 252, 149, 85);
|
||||
} else {
|
||||
color = Colors.redAccent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,22 +176,24 @@ class _UserListItem extends State<GroupListItem> {
|
|||
_previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
|
||||
if (mediaFile?.downloadState == null) return;
|
||||
if (mediaFile!.downloadState! == DownloadState.pending) {
|
||||
await startDownloadMedia(mediaFile, true);
|
||||
return;
|
||||
}
|
||||
if (mediaFile.downloadState! == DownloadState.ready) {
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(widget.group);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
if (mediaFile?.type != MediaType.audio) {
|
||||
if (mediaFile?.downloadState == null) return;
|
||||
if (mediaFile!.downloadState! == DownloadState.pending) {
|
||||
await startDownloadMedia(mediaFile, true);
|
||||
return;
|
||||
}
|
||||
if (mediaFile.downloadState! == DownloadState.ready) {
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(widget.group);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
|
@ -11,9 +12,9 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart'
|
||||
hide MessageActions;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
|
|
@ -136,13 +139,23 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
)
|
||||
: (mediaService == null)
|
||||
? null
|
||||
: ChatMediaEntry(
|
||||
message: widget.message,
|
||||
group: widget.group,
|
||||
mediaService: mediaService!,
|
||||
galleryItems: widget.galleryItems,
|
||||
minWidth: reactionsForWidth * 43,
|
||||
),
|
||||
: (mediaService!.mediaFile.type == MediaType.audio)
|
||||
? ChatAudioEntry(
|
||||
message: widget.message,
|
||||
nextMessage: widget.nextMessage,
|
||||
prevMessage: widget.prevMessage,
|
||||
mediaService: mediaService!,
|
||||
userIdToContact: widget.userIdToContact,
|
||||
borderRadius: borderRadius,
|
||||
minWidth: reactionsForWidth * 43,
|
||||
)
|
||||
: ChatMediaEntry(
|
||||
message: widget.message,
|
||||
group: widget.group,
|
||||
mediaService: mediaService!,
|
||||
galleryItems: widget.galleryItems,
|
||||
minWidth: reactionsForWidth * 43,
|
||||
),
|
||||
),
|
||||
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,221 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
||||
class ChatTextEntry extends StatelessWidget {
|
||||
const ChatTextEntry({
|
||||
required this.message,
|
||||
required this.nextMessage,
|
||||
required this.prevMessage,
|
||||
required this.borderRadius,
|
||||
required this.userIdToContact,
|
||||
required this.minWidth,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Message? nextMessage;
|
||||
final Message? prevMessage;
|
||||
final Map<int, Contact>? userIdToContact;
|
||||
final BorderRadius borderRadius;
|
||||
final double minWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var text = message.content ?? '';
|
||||
var textColor = Colors.white;
|
||||
|
||||
if (EmojiAnimation.supported(text)) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 100,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: EmojiAnimation(emoji: text),
|
||||
);
|
||||
}
|
||||
|
||||
var displayTime = !combineTextMessageWithNext(message, nextMessage);
|
||||
var displayUserName = '';
|
||||
if (message.senderId != null &&
|
||||
userIdToContact != null &&
|
||||
userIdToContact![message.senderId] != null) {
|
||||
if (prevMessage == null) {
|
||||
displayUserName =
|
||||
getContactDisplayName(userIdToContact![message.senderId]!);
|
||||
} else {
|
||||
if (!combineTextMessageWithNext(prevMessage!, message)) {
|
||||
displayUserName =
|
||||
getContactDisplayName(userIdToContact![message.senderId]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var spacerWidth = minWidth - measureTextWidth(text) - 53;
|
||||
if (spacerWidth < 0) spacerWidth = 0;
|
||||
|
||||
Color? color;
|
||||
var expanded = false;
|
||||
if (message.quotesMessageId == null) {
|
||||
color = getMessageColor(message);
|
||||
}
|
||||
if (message.isDeletedFromSender) {
|
||||
color = context.color.surfaceBright;
|
||||
displayTime = false;
|
||||
} else if (measureTextWidth(text) > 270) {
|
||||
expanded = true;
|
||||
}
|
||||
|
||||
if (message.isDeletedFromSender) {
|
||||
text = context.lang.messageWasDeleted;
|
||||
color = isDarkMode(context) ? Colors.black : Colors.grey;
|
||||
if (isDarkMode(context)) {
|
||||
textColor = const Color.fromARGB(255, 99, 99, 99);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
minWidth: minWidth,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (displayUserName != '')
|
||||
Text(
|
||||
displayUserName,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (expanded)
|
||||
Expanded(
|
||||
child: BetterText(text: text, textColor: textColor),
|
||||
)
|
||||
else ...[
|
||||
BetterText(text: text, textColor: textColor),
|
||||
SizedBox(
|
||||
width: spacerWidth,
|
||||
),
|
||||
],
|
||||
if (displayTime || message.modifiedAt != null)
|
||||
Align(
|
||||
alignment: AlignmentGeometry.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (message.modifiedAt != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.pencil,
|
||||
color: Colors.white.withAlpha(150),
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
friendlyTime(
|
||||
context,
|
||||
(message.modifiedAt != null)
|
||||
? message.modifiedAt!
|
||||
: message.createdAt,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withAlpha(150),
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double measureTextWidth(
|
||||
String text,
|
||||
) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(text: text, style: const TextStyle(fontSize: 17)),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout();
|
||||
return tp.size.width;
|
||||
}
|
||||
|
||||
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
||||
if (nextMessage != null && nextMessage.content != null) {
|
||||
if (nextMessage.senderId == message.senderId) {
|
||||
if (nextMessage.type == MessageType.text &&
|
||||
message.type == MessageType.text) {
|
||||
if (!EmojiAnimation.supported(nextMessage.content!)) {
|
||||
final diff =
|
||||
nextMessage.createdAt.difference(message.createdAt).inMinutes;
|
||||
if (diff <= 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String friendlyTime(BuildContext context, DateTime dt) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(dt);
|
||||
|
||||
if (diff.inMinutes >= 0 && diff.inMinutes < 60) {
|
||||
final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes;
|
||||
if (minutes <= 1) {
|
||||
return context.lang.now;
|
||||
}
|
||||
return '$minutes ${context.lang.minutesShort}';
|
||||
}
|
||||
|
||||
// Determine 24h vs 12h from system/local settings
|
||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||
|
||||
if (!use24Hour) {
|
||||
// 12-hour format with locale-aware AM/PM
|
||||
final format = DateFormat.jm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
} else {
|
||||
// 24-hour HH:mm, locale-aware
|
||||
final format = DateFormat.Hm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
||||
class ChatAudioEntry extends StatelessWidget {
|
||||
const ChatAudioEntry({
|
||||
required this.message,
|
||||
required this.nextMessage,
|
||||
required this.mediaService,
|
||||
required this.prevMessage,
|
||||
required this.borderRadius,
|
||||
required this.userIdToContact,
|
||||
required this.minWidth,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final MediaFileService mediaService;
|
||||
final Message? nextMessage;
|
||||
final Message? prevMessage;
|
||||
final Map<int, Contact>? userIdToContact;
|
||||
final BorderRadius borderRadius;
|
||||
final double minWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!mediaService.tempPath.existsSync() &&
|
||||
!mediaService.originalPath.existsSync()) {
|
||||
return Container(); // media file was purged
|
||||
}
|
||||
final info = getBubbleInfo(
|
||||
context,
|
||||
message,
|
||||
nextMessage,
|
||||
prevMessage,
|
||||
userIdToContact,
|
||||
minWidth,
|
||||
);
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
minWidth: 250,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: info.color,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (info.displayUserName != '')
|
||||
Text(
|
||||
info.displayUserName,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (info.text != '')
|
||||
Expanded(
|
||||
child: BetterText(text: info.text, textColor: info.textColor),
|
||||
)
|
||||
else ...[
|
||||
if (mediaService.mediaFile.downloadState ==
|
||||
DownloadState.ready ||
|
||||
mediaService.mediaFile.downloadState == null)
|
||||
mediaService.tempPath.existsSync()
|
||||
? InChatAudioPlayer(
|
||||
path: mediaService.tempPath.path,
|
||||
message: message,
|
||||
)
|
||||
: (mediaService.originalPath.existsSync())
|
||||
? InChatAudioPlayer(
|
||||
path: mediaService.originalPath.path,
|
||||
message: message,
|
||||
)
|
||||
: Container()
|
||||
else
|
||||
MessageSendStateIcon([message], [mediaService.mediaFile]),
|
||||
],
|
||||
if (info.displayTime || message.modifiedAt != null)
|
||||
FriendlyMessageTime(message: message),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InChatAudioPlayer extends StatefulWidget {
|
||||
const InChatAudioPlayer({
|
||||
required this.path,
|
||||
required this.message,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
State<InChatAudioPlayer> createState() => _InChatAudioPlayerState();
|
||||
}
|
||||
|
||||
class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
||||
final PlayerController _playerController = PlayerController();
|
||||
int _displayDuration = 0;
|
||||
int _maxDuration = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_playerController
|
||||
..preparePlayer(path: widget.path)
|
||||
..setFinishMode(finishMode: FinishMode.pause);
|
||||
|
||||
_playerController.onCompletion.listen((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_playerController.seekTo(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_playerController.onCurrentDurationChanged.listen((duration) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_displayDuration = _maxDuration - duration;
|
||||
});
|
||||
}
|
||||
});
|
||||
initAsync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_playerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
_displayDuration = await _playerController.getDuration(DurationType.max);
|
||||
_maxDuration = _displayDuration;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_isPlaying) {
|
||||
_playerController.pausePlayer();
|
||||
} else {
|
||||
_playerController.startPlayer();
|
||||
if (widget.message.senderId != null &&
|
||||
widget.message.openedAt == null) {
|
||||
notifyContactAboutOpeningMessage(
|
||||
widget.message.senderId!,
|
||||
[widget.message.messageId],
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_isPlaying = !_isPlaying;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: _isPlaying ? 2 : 0,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
color: Colors.transparent,
|
||||
child: FaIcon(
|
||||
_isPlaying ? FontAwesomeIcons.pause : FontAwesomeIcons.play,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatMsToMinSec(_displayDuration),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
AudioFileWaveforms(
|
||||
playerController: _playerController,
|
||||
size: const Size(150, 40),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String formatMsToMinSec(int milliseconds) {
|
||||
final d = Duration(milliseconds: milliseconds);
|
||||
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
||||
class ChatTextEntry extends StatelessWidget {
|
||||
const ChatTextEntry({
|
||||
required this.message,
|
||||
required this.nextMessage,
|
||||
required this.prevMessage,
|
||||
required this.borderRadius,
|
||||
required this.userIdToContact,
|
||||
required this.minWidth,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Message? nextMessage;
|
||||
final Message? prevMessage;
|
||||
final Map<int, Contact>? userIdToContact;
|
||||
final BorderRadius borderRadius;
|
||||
final double minWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = message.content ?? '';
|
||||
|
||||
if (EmojiAnimation.supported(text)) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 100,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: EmojiAnimation(emoji: text),
|
||||
);
|
||||
}
|
||||
|
||||
final info = getBubbleInfo(
|
||||
context,
|
||||
message,
|
||||
nextMessage,
|
||||
prevMessage,
|
||||
userIdToContact,
|
||||
minWidth,
|
||||
);
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
minWidth: minWidth,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: info.color,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (info.displayUserName != '')
|
||||
Text(
|
||||
info.displayUserName,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (info.expanded)
|
||||
Expanded(
|
||||
child: BetterText(text: text, textColor: info.textColor),
|
||||
)
|
||||
else ...[
|
||||
BetterText(text: text, textColor: info.textColor),
|
||||
SizedBox(
|
||||
width: info.spacerWidth,
|
||||
),
|
||||
],
|
||||
if (info.displayTime || message.modifiedAt != null)
|
||||
FriendlyMessageTime(message: message),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
101
lib/src/views/chats/chat_messages_components/entries/common.dart
Normal file
101
lib/src/views/chats/chat_messages_components/entries/common.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
|
||||
class BubbleInfo {
|
||||
late String text;
|
||||
late Color textColor;
|
||||
late bool displayTime;
|
||||
late String displayUserName;
|
||||
late Color color;
|
||||
late bool expanded;
|
||||
late double spacerWidth;
|
||||
}
|
||||
|
||||
BubbleInfo getBubbleInfo(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
Message? nextMessage,
|
||||
Message? prevMessage,
|
||||
Map<int, Contact>? userIdToContact,
|
||||
double minWidth,
|
||||
) {
|
||||
final info = BubbleInfo()
|
||||
..text = message.content ?? ''
|
||||
..textColor = Colors.white
|
||||
..color = getMessageColor(message)
|
||||
..displayTime = !combineTextMessageWithNext(message, nextMessage)
|
||||
..displayUserName = '';
|
||||
|
||||
if (message.senderId != null &&
|
||||
userIdToContact != null &&
|
||||
userIdToContact[message.senderId] != null) {
|
||||
if (prevMessage == null) {
|
||||
info.displayUserName =
|
||||
getContactDisplayName(userIdToContact[message.senderId]!);
|
||||
} else {
|
||||
if (!combineTextMessageWithNext(prevMessage, message)) {
|
||||
info.displayUserName =
|
||||
getContactDisplayName(userIdToContact[message.senderId]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info.spacerWidth = minWidth - measureTextWidth(info.text) - 53;
|
||||
if (info.spacerWidth < 0) info.spacerWidth = 0;
|
||||
|
||||
info.expanded = false;
|
||||
if (message.quotesMessageId == null) {
|
||||
info.color = getMessageColor(message);
|
||||
}
|
||||
if (message.isDeletedFromSender) {
|
||||
info
|
||||
..color = context.color.surfaceBright
|
||||
..displayTime = false;
|
||||
} else if (measureTextWidth(info.text) > 270) {
|
||||
info.expanded = true;
|
||||
}
|
||||
|
||||
if (message.isDeletedFromSender) {
|
||||
info
|
||||
..text = context.lang.messageWasDeleted
|
||||
..color = isDarkMode(context) ? Colors.black : Colors.grey;
|
||||
if (isDarkMode(context)) {
|
||||
info.textColor = const Color.fromARGB(255, 99, 99, 99);
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
double measureTextWidth(
|
||||
String text,
|
||||
) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(text: text, style: const TextStyle(fontSize: 17)),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout();
|
||||
return tp.size.width;
|
||||
}
|
||||
|
||||
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
||||
if (nextMessage != null && nextMessage.content != null) {
|
||||
if (nextMessage.senderId == message.senderId) {
|
||||
if (nextMessage.type == MessageType.text &&
|
||||
message.type == MessageType.text) {
|
||||
if (!EmojiAnimation.supported(nextMessage.content!)) {
|
||||
final diff =
|
||||
nextMessage.createdAt.difference(message.createdAt).inMinutes;
|
||||
if (diff <= 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class FriendlyMessageTime extends StatelessWidget {
|
||||
const FriendlyMessageTime({required this.message, super.key});
|
||||
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: AlignmentGeometry.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
if (message.modifiedAt != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.pencil,
|
||||
color: Colors.white.withAlpha(150),
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
friendlyTime(
|
||||
context,
|
||||
(message.modifiedAt != null)
|
||||
? message.modifiedAt!
|
||||
: message.createdAt,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withAlpha(150),
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String friendlyTime(BuildContext context, DateTime dt) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(dt);
|
||||
|
||||
if (diff.inMinutes >= 0 && diff.inMinutes < 60) {
|
||||
final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes;
|
||||
if (minutes <= 1) {
|
||||
return context.lang.now;
|
||||
}
|
||||
return '$minutes ${context.lang.minutesShort}';
|
||||
}
|
||||
|
||||
// Determine 24h vs 12h from system/local settings
|
||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||
|
||||
if (!use24Hour) {
|
||||
// 12-hour format with locale-aware AM/PM
|
||||
final format = DateFormat.jm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
} else {
|
||||
// 24-hour HH:mm, locale-aware
|
||||
final format = DateFormat.Hm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
|
||||
|
|
@ -25,10 +32,14 @@ class MessageInput extends StatefulWidget {
|
|||
State<MessageInput> createState() => _MessageInputState();
|
||||
}
|
||||
|
||||
enum RecordingState { none, recording, finished }
|
||||
|
||||
class _MessageInputState extends State<MessageInput> {
|
||||
late final TextEditingController _textFieldController;
|
||||
late final RecorderController recorderController;
|
||||
final bool isApple = Platform.isIOS;
|
||||
bool _emojiShowing = false;
|
||||
RecordingState _recordingState = RecordingState.none;
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_textFieldController.text == '') return;
|
||||
|
|
@ -48,6 +59,7 @@ class _MessageInputState extends State<MessageInput> {
|
|||
void initState() {
|
||||
_textFieldController = TextEditingController();
|
||||
widget.textFieldFocus.addListener(_handleTextFocusChange);
|
||||
_initializeControllers();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +70,14 @@ class _MessageInputState extends State<MessageInput> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeControllers() {
|
||||
recorderController = RecorderController()
|
||||
..androidEncoder = AndroidEncoder.aac
|
||||
..androidOutputFormat = AndroidOutputFormat.mpeg4
|
||||
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
|
||||
..sampleRate = 44100;
|
||||
}
|
||||
|
||||
void _handleTextFocusChange() {
|
||||
if (widget.textFieldFocus.hasFocus) {
|
||||
setState(() {
|
||||
|
|
@ -66,6 +86,33 @@ class _MessageInputState extends State<MessageInput> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _stopAudioRecording() async {
|
||||
await HapticFeedback.heavyImpact();
|
||||
setState(() {
|
||||
_recordingState = RecordingState.none;
|
||||
});
|
||||
|
||||
final audioTmpPath = await recorderController.stop();
|
||||
|
||||
if (audioTmpPath == null) return;
|
||||
|
||||
final mediaFileService = await initializeMediaUpload(
|
||||
MediaType.audio,
|
||||
null,
|
||||
);
|
||||
|
||||
if (mediaFileService == null) return;
|
||||
|
||||
File(audioTmpPath)
|
||||
..copySync(mediaFileService.originalPath.path)
|
||||
..deleteSync();
|
||||
|
||||
await insertMediaFileInMessagesTable(
|
||||
mediaFileService,
|
||||
[widget.group.groupId],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
|
@ -89,56 +136,164 @@ class _MessageInputState extends State<MessageInput> {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
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,
|
||||
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: TextField(
|
||||
controller: _textFieldController,
|
||||
focusNode: widget.textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
child: (_recordingState == RecordingState.recording)
|
||||
? AudioWaveforms(
|
||||
enableGesture: true,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width / 2,
|
||||
50,
|
||||
),
|
||||
recorderController: recorderController,
|
||||
waveStyle: WaveStyle(
|
||||
waveColor: isDarkMode(context)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
extendWaveform: true,
|
||||
showMiddleLine: false,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: context.color.surfaceContainer,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 18),
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 15),
|
||||
)
|
||||
: TextField(
|
||||
controller: _textFieldController,
|
||||
focusNode: widget.textFieldFocus,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
style: const TextStyle(fontSize: 17),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_textFieldController.text == '')
|
||||
GestureDetector(
|
||||
onLongPressStart: (a) async {
|
||||
if (!await Permission.microphone.isGranted) {
|
||||
final statuses = await [
|
||||
Permission.microphone,
|
||||
].request();
|
||||
if (statuses[Permission.microphone]!
|
||||
.isPermanentlyDenied) {
|
||||
await openAppSettings();
|
||||
return;
|
||||
}
|
||||
if (!await Permission.microphone.isGranted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_recordingState = RecordingState.recording;
|
||||
});
|
||||
await HapticFeedback.heavyImpact();
|
||||
final audioTmpPath =
|
||||
'${(await getApplicationCacheDirectory()).path}/recording.m4a';
|
||||
unawaited(
|
||||
recorderController.record(
|
||||
path: audioTmpPath,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
onLongPressCancel: () async {
|
||||
final path = await recorderController.stop();
|
||||
if (path == null) return;
|
||||
if (File(path).existsSync()) {
|
||||
File(path).deleteSync();
|
||||
}
|
||||
setState(() {
|
||||
_recordingState = RecordingState.none;
|
||||
});
|
||||
},
|
||||
style: const TextStyle(fontSize: 17),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
onLongPressEnd: (a) => _stopAudioRecording(),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
if (_recordingState == RecordingState.recording)
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/database/tables/messages.table.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
|
||||
enum MessageSendState {
|
||||
received,
|
||||
|
|
@ -85,6 +86,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
final kindsAlreadyShown = HashSet<MessageType>();
|
||||
|
||||
var hasLoader = false;
|
||||
GestureTapCallback? onTap;
|
||||
|
||||
for (final message in widget.messages) {
|
||||
if (icons.length == 2) break;
|
||||
|
|
@ -147,7 +149,27 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
|
||||
if (mediaFile != null) {
|
||||
if (mediaFile.uploadState == UploadState.uploadLimitReached) {
|
||||
text = 'Upload Limit erreicht';
|
||||
icon = FaIcon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
size: 12,
|
||||
color: color,
|
||||
);
|
||||
|
||||
textWidget = Text(
|
||||
context.lang.uploadLimitReached,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
|
||||
onTap = () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const SubscriptionView();
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
if (mediaFile.uploadState == UploadState.preprocessing) {
|
||||
text = 'Wird verarbeitet';
|
||||
|
|
@ -251,20 +273,23 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 3),
|
||||
if (textWidget != null)
|
||||
textWidget
|
||||
else
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 3),
|
||||
if (textWidget != null)
|
||||
textWidget
|
||||
else
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,9 +175,16 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
}
|
||||
}
|
||||
if (_message!.type == MessageType.media && _mediaService != null) {
|
||||
subtitle = _mediaService!.mediaFile.type == MediaType.video
|
||||
? context.lang.video
|
||||
: context.lang.image;
|
||||
switch (_mediaService!.mediaFile.type) {
|
||||
case MediaType.image:
|
||||
subtitle = context.lang.image;
|
||||
case MediaType.video:
|
||||
subtitle = context.lang.video;
|
||||
case MediaType.gif:
|
||||
subtitle = 'Gif';
|
||||
case MediaType.audio:
|
||||
subtitle = 'Audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (_message!.senderId == null) {
|
||||
|
|
@ -241,7 +248,8 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (_mediaService != null)
|
||||
if (_mediaService != null &&
|
||||
_mediaService!.mediaFile.type != MediaType.audio)
|
||||
SizedBox(
|
||||
height: widget.showBorder ? 100 : 210,
|
||||
child: Image.file(
|
||||
|
|
|
|||
242
lib/src/views/chats/chat_messages_components/test
Normal file
242
lib/src/views/chats/chat_messages_components/test
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||
import 'package:audio_waveforms_example/chat_bubble.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: 'Audio Waveforms',
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Home(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
const Home({super.key});
|
||||
|
||||
@override
|
||||
State<Home> createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
late final RecorderController recorderController;
|
||||
|
||||
String? path;
|
||||
String? musicFile;
|
||||
bool isRecording = false;
|
||||
bool isRecordingCompleted = false;
|
||||
bool isLoading = true;
|
||||
late Directory appDirectory;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getDir();
|
||||
_initialiseControllers();
|
||||
}
|
||||
|
||||
void _getDir() async {
|
||||
appDirectory = await getApplicationDocumentsDirectory();
|
||||
path = "${appDirectory.path}/recording.m4a";
|
||||
isLoading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _initialiseControllers() {
|
||||
recorderController = RecorderController()
|
||||
..androidEncoder = AndroidEncoder.aac
|
||||
..androidOutputFormat = AndroidOutputFormat.mpeg4
|
||||
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
|
||||
..sampleRate = 44100;
|
||||
}
|
||||
|
||||
void _pickFile() async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result != null) {
|
||||
musicFile = result.files.single.path;
|
||||
setState(() {});
|
||||
} else {
|
||||
debugPrint("File not picked");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
recorderController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF252331),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF252331),
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
shadowColor: Colors.grey,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
scale: 1.5,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Simform',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: 4,
|
||||
itemBuilder: (_, index) {
|
||||
return WaveBubble(
|
||||
index: index + 1,
|
||||
isSender: index.isOdd,
|
||||
width: MediaQuery.of(context).size.width / 2,
|
||||
appDirectory: appDirectory,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (isRecordingCompleted)
|
||||
WaveBubble(
|
||||
path: path,
|
||||
isSender: true,
|
||||
appDirectory: appDirectory,
|
||||
),
|
||||
if (musicFile != null)
|
||||
WaveBubble(
|
||||
path: musicFile,
|
||||
isSender: true,
|
||||
appDirectory: appDirectory,
|
||||
),
|
||||
SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: isRecording
|
||||
? AudioWaveforms(
|
||||
enableGesture: true,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width / 2,
|
||||
50),
|
||||
recorderController: recorderController,
|
||||
waveStyle: const WaveStyle(
|
||||
waveColor: Colors.white,
|
||||
extendWaveform: true,
|
||||
showMiddleLine: false,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
color: const Color(0xFF1E1B26),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 18),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 15),
|
||||
)
|
||||
: Container(
|
||||
width:
|
||||
MediaQuery.of(context).size.width / 1.7,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E1B26),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 18),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 15),
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Type Something...",
|
||||
hintStyle: const TextStyle(
|
||||
color: Colors.white54),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(top: 16),
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _pickFile,
|
||||
icon: Icon(Icons.adaptive.share),
|
||||
color: Colors.white54,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _refreshWave,
|
||||
icon: Icon(
|
||||
isRecording ? Icons.refresh : Icons.send,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
onPressed: _startOrStopRecording,
|
||||
icon: Icon(isRecording ? Icons.stop : Icons.mic),
|
||||
color: Colors.white,
|
||||
iconSize: 28,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _startOrStopRecording() async {
|
||||
try {
|
||||
if (isRecording) {
|
||||
recorderController.reset();
|
||||
|
||||
path = await recorderController.stop(false);
|
||||
|
||||
if (path != null) {
|
||||
isRecordingCompleted = true;
|
||||
debugPrint(path);
|
||||
debugPrint("Recorded file size: ${File(path!).lengthSync()}");
|
||||
}
|
||||
} else {
|
||||
await recorderController.record(path: path); // Path is optional
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
} finally {
|
||||
if (recorderController.hasPermission) {
|
||||
setState(() {
|
||||
isRecording = !isRecording;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshWave() {
|
||||
if (isRecording) recorderController.refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -83,9 +83,10 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_directChat == null ||
|
||||
_directChat!.maxFlameCounter == 0 ||
|
||||
_flameCounter >= (_directChat!.maxFlameCounter + 1) ||
|
||||
_directChat!.lastFlameCounterChange!
|
||||
.isBefore(DateTime.now().subtract(const Duration(days: 5)))) {
|
||||
.isBefore(DateTime.now().subtract(const Duration(days: 4)))) {
|
||||
return Container();
|
||||
}
|
||||
return BetterListTile(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
audio_waveforms:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_waveforms
|
||||
sha256: "658fef41bbab299184b65ba2fd749e8ec658c1f7d54a21f7cf97fa96b173b4ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
avatar_maker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ environment:
|
|||
sdk: ^3.6.0
|
||||
|
||||
dependencies:
|
||||
audio_waveforms: ^1.3.0
|
||||
avatar_maker: ^0.4.0
|
||||
background_downloader: ^9.2.2
|
||||
cached_network_image: ^3.4.1
|
||||
|
|
|
|||
Loading…
Reference in a new issue