diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 718417c..9fb06e1 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -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}}\"", ] diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index d3dee0c..88baecf 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -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 { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3c00a85..57db739 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index b5da4a1..03fae40 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -89,6 +89,7 @@ class MediaFilesDao extends DatabaseAccessor ..where( (t) => t.uploadState.equals(UploadState.initialized.name) | + t.uploadState.equals(UploadState.uploadLimitReached.name) | t.uploadState.equals(UploadState.preprocessing.name), )) .get(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 993ea08..3576abe 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -50,6 +50,7 @@ class MessagesDao extends DatabaseAccessor 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() & diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 585b49d..6651485 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -5,6 +5,7 @@ enum MediaType { image, video, gif, + audio, } enum UploadState { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 182be3d..f14986a 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -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", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c014f78..08f553c 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -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", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index f329ac9..140bc39 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index bc98dea..2d8c516 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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.'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index ff1a8e0..9834524 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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.'; diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart index 4651437..11a6701 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -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 values = [ REUPLOAD, IMAGE, VIDEO, GIF, + AUDIO, ]; static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index a0fd2af..08c49e4 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -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='); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart index e2d05ee..2a2fbf7 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart @@ -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 values = [ 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); diff --git a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart index 9782873..4d576a1 100644 --- a/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart @@ -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 = { diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 301ac2e..61f6364 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -107,6 +107,7 @@ message EncryptedContent { IMAGE = 1; VIDEO = 2; GIF = 3; + AUDIO = 4; } string senderMessageId = 1; diff --git a/lib/src/model/protobuf/client/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto index c30d715..5e74e9c 100644 --- a/lib/src/model/protobuf/client/push_notification.proto +++ b/lib/src/model/protobuf/client/push_notification.proto @@ -22,7 +22,9 @@ enum PushKind { reactionToVideo = 11; reactionToText = 12; reactionToImage = 13; - addedToGroup = 14; + reactionToAudio = 14; + addedToGroup = 15; + audio = 16; }; message PushNotification { diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 10108b6..616086d 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -62,6 +62,8 @@ Future 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( diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 7c57699..ec8f659 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -30,38 +30,52 @@ Future tryDownloadAllMediaFiles({bool force = false}) async { enum DownloadMediaTypes { video, image, + audio, } Map> defaultAutoDownloadOptions = { - ConnectivityResult.mobile.name: [], + ConnectivityResult.mobile.name: [ + DownloadMediaTypes.audio.name, + ], ConnectivityResult.wifi.name: [ DownloadMediaTypes.video.name, DownloadMediaTypes.image.name, + DownloadMediaTypes.audio.name, ], }; -Future isAllowedToDownload({required bool isVideo}) async { +Future 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 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.', ); diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index d4fc7c1..8d1283e 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -58,8 +58,7 @@ Future 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 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); + } } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 52b3f53..d4a6783 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -115,7 +115,8 @@ Future 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 _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( diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 5c09e10..27e2ea1 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -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 = diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 935ea22..ac5a889 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -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: diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 033f2c3..64509a8 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -220,6 +220,8 @@ Future 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 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; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b121035..c41c306 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -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; } diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 4b1ce6c..6f1939a 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -176,22 +176,24 @@ class _UserListItem extends State { _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; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index abd8a04..4526e8e 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -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'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 0c48305..496d97f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.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 { ) : (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), ], diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart deleted file mode 100644 index 03940ab..0000000 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ /dev/null @@ -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? 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); - } -} diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart new file mode 100644 index 0000000..24267fb --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart @@ -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? 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 createState() => _InChatAudioPlayerState(); +} + +class _InChatAudioPlayerState extends State { + 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 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'; +} diff --git a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart b/lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart similarity index 100% rename from lib/src/views/chats/chat_messages_components/chat_date_chip.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart similarity index 100% rename from lib/src/views/chats/chat_messages_components/chat_media_entry.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart new file mode 100644 index 0000000..89c336e --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -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? 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), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/common.dart b/lib/src/views/chats/chat_messages_components/entries/common.dart new file mode 100644 index 0000000..da56997 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/common.dart @@ -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? 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; +} diff --git a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart new file mode 100644 index 0000000..1d793e0 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -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); + } +} diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index a0768dd..ec24342 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -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 createState() => _MessageInputState(); } +enum RecordingState { none, recording, finished } + class _MessageInputState extends State { late final TextEditingController _textFieldController; + late final RecorderController recorderController; final bool isApple = Platform.isIOS; bool _emojiShowing = false; + RecordingState _recordingState = RecordingState.none; Future _sendMessage() async { if (_textFieldController.text == '') return; @@ -48,6 +59,7 @@ class _MessageInputState extends State { void initState() { _textFieldController = TextEditingController(); widget.textFieldFocus.addListener(_handleTextFocusChange); + _initializeControllers(); super.initState(); } @@ -58,6 +70,14 @@ class _MessageInputState extends State { 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 { } } + Future _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 { ), 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, + ), + ), + ), + ], ), ), - ), ], ), ), diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index cb378a9..e147dbc 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -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 { final kindsAlreadyShown = HashSet(); var hasLoader = false; + GestureTapCallback? onTap; for (final message in widget.messages) { if (icons.length == 2) break; @@ -147,7 +149,27 @@ class _MessageSendStateIconState extends State { 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 { ); } - 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), + ], + ), ); } } diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index a0b96da..1c27f90 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -175,9 +175,16 @@ class _ResponsePreviewState extends State { } } 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 { ], ), ), - if (_mediaService != null) + if (_mediaService != null && + _mediaService!.mediaFile.type != MediaType.audio) SizedBox( height: widget.showBorder ? 100 : 210, child: Image.file( diff --git a/lib/src/views/chats/chat_messages_components/test b/lib/src/views/chats/chat_messages_components/test new file mode 100644 index 0000000..db5158b --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/test @@ -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 createState() => _HomeState(); +} + +class _HomeState extends State { + 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(); + } +} \ No newline at end of file diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index 4c863b7..ee193e7 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -83,9 +83,10 @@ class _MaxFlameListTitleState extends State { @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( diff --git a/pubspec.lock b/pubspec.lock index b1d0c98..ff7558f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 352eb50..0489c4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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