implementing voice messages #251

This commit is contained in:
otsmr 2025-11-07 00:31:46 +01:00
parent 3706a36cf9
commit 95c9db86d6
42 changed files with 1252 additions and 379 deletions

View file

@ -219,6 +219,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.twonly: "hat ein twonly{inGroup} gesendet.", .twonly: "hat ein twonly{inGroup} gesendet.",
.video: "hat ein Video{inGroup} gesendet.", .video: "hat ein Video{inGroup} gesendet.",
.image: "hat ein Bild{inGroup} gesendet.", .image: "hat ein Bild{inGroup} gesendet.",
.audio: "hat eine Sprachnachricht{inGroup} gesendet.",
.contactRequest: "möchte sich mit dir vernetzen.", .contactRequest: "möchte sich mit dir vernetzen.",
.acceptRequest: "ist jetzt mit dir vernetzt.", .acceptRequest: "ist jetzt mit dir vernetzt.",
.storedMediaFile: "hat dein Bild gespeichert.", .storedMediaFile: "hat dein Bild gespeichert.",
@ -228,6 +229,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.", .reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
.reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
.response: "hat dir{inGroup} geantwortet.", .response: "hat dir{inGroup} geantwortet.",
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
] ]
@ -237,6 +239,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.twonly: "sent a twonly{inGroup}.", .twonly: "sent a twonly{inGroup}.",
.video: "sent a video{inGroup}.", .video: "sent a video{inGroup}.",
.image: "sent a image{inGroup}.", .image: "sent a image{inGroup}.",
.audio: "sent a voice message{inGroup}.",
.contactRequest: "wants to connect with you.", .contactRequest: "wants to connect with you.",
.acceptRequest: "is now connected with you.", .acceptRequest: "is now connected with you.",
.storedMediaFile: "has stored your image.", .storedMediaFile: "has stored your image.",
@ -246,6 +249,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.reactionToVideo: "has reacted with {{content}} to your video.", .reactionToVideo: "has reacted with {{content}} to your video.",
.reactionToText: "has reacted with {{content}} to your text.", .reactionToText: "has reacted with {{content}} to your text.",
.reactionToImage: "has reacted with {{content}} to your image.", .reactionToImage: "has reacted with {{content}} to your image.",
.reactionToAudio: "has reacted with {{content}} to your voice message.",
.response: "has responded{inGroup}.", .response: "has responded{inGroup}.",
.addedToGroup: "has added you to \"{{content}}\"", .addedToGroup: "has added you to \"{{content}}\"",
] ]

View file

@ -37,7 +37,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
case reactionToVideo // = 11 case reactionToVideo // = 11
case reactionToText // = 12 case reactionToText // = 12
case reactionToImage // = 13 case reactionToImage // = 13
case addedToGroup // = 14 case reactionToAudio // = 14
case addedToGroup // = 15
case audio // = 16
case UNRECOGNIZED(Int) case UNRECOGNIZED(Int)
init() { init() {
@ -60,7 +62,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
case 11: self = .reactionToVideo case 11: self = .reactionToVideo
case 12: self = .reactionToText case 12: self = .reactionToText
case 13: self = .reactionToImage case 13: self = .reactionToImage
case 14: self = .addedToGroup case 14: self = .reactionToAudio
case 15: self = .addedToGroup
case 16: self = .audio
default: self = .UNRECOGNIZED(rawValue) default: self = .UNRECOGNIZED(rawValue)
} }
} }
@ -81,7 +85,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
case .reactionToVideo: return 11 case .reactionToVideo: return 11
case .reactionToText: return 12 case .reactionToText: return 12
case .reactionToImage: return 13 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 case .UNRECOGNIZED(let i): return i
} }
} }
@ -102,7 +108,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
.reactionToVideo, .reactionToVideo,
.reactionToText, .reactionToText,
.reactionToImage, .reactionToImage,
.reactionToAudio,
.addedToGroup, .addedToGroup,
.audio,
] ]
} }
@ -218,7 +226,7 @@ struct PushKey: Sendable {
// MARK: - Code below here is support for the SwiftProtobuf runtime. // MARK: - Code below here is support for the SwiftProtobuf runtime.
extension PushKind: SwiftProtobuf._ProtoNameProviding { 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 { extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {

View file

@ -1,4 +1,6 @@
PODS: PODS:
- audio_waveforms (0.0.1):
- Flutter
- background_downloader (0.0.1): - background_downloader (0.0.1):
- Flutter - Flutter
- camera_avfoundation (0.0.1): - camera_avfoundation (0.0.1):
@ -247,6 +249,7 @@ PODS:
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- background_downloader (from `.symlinks/plugins/background_downloader/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
@ -307,6 +310,8 @@ SPEC REPOS:
- SwiftProtobuf - SwiftProtobuf
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
background_downloader: background_downloader:
:path: ".symlinks/plugins/background_downloader/ios" :path: ".symlinks/plugins/background_downloader/ios"
camera_avfoundation: camera_avfoundation:
@ -369,6 +374,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd

View file

@ -89,6 +89,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
..where( ..where(
(t) => (t) =>
t.uploadState.equals(UploadState.initialized.name) | t.uploadState.equals(UploadState.initialized.name) |
t.uploadState.equals(UploadState.uploadLimitReached.name) |
t.uploadState.equals(UploadState.preprocessing.name), t.uploadState.equals(UploadState.preprocessing.name),
)) ))
.get(); .get();

View file

@ -50,6 +50,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
mediaFiles.downloadState mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name) .equals(DownloadState.reuploadRequested.name)
.not() & .not() &
mediaFiles.type.equals(MediaType.audio.name).not() &
messages.openedAt.isNull() & messages.openedAt.isNull() &
messages.groupId.equals(groupId) & messages.groupId.equals(groupId) &
messages.mediaId.isNotNull() & messages.mediaId.isNotNull() &

View file

@ -5,6 +5,7 @@ enum MediaType {
image, image,
video, video,
gif, gif,
audio,
} }
enum UploadState { enum UploadState {

View file

@ -648,6 +648,7 @@
"appOutdatedBtn": "Jetzt aktualisieren.", "appOutdatedBtn": "Jetzt aktualisieren.",
"@appOutdatedBtn": {}, "@appOutdatedBtn": {},
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.",
"@doubleClickToReopen": {}, "@doubleClickToReopen": {},
"retransmissionRequested": "Wird erneut versucht.", "retransmissionRequested": "Wird erneut versucht.",
"@retransmissionRequested": {}, "@retransmissionRequested": {},
@ -784,6 +785,7 @@
"notificationTwonly": "hat ein twonly{inGroup} gesendet.", "notificationTwonly": "hat ein twonly{inGroup} gesendet.",
"notificationVideo": "hat ein Video{inGroup} gesendet.", "notificationVideo": "hat ein Video{inGroup} gesendet.",
"notificationImage": "hat ein Bild{inGroup} gesendet.", "notificationImage": "hat ein Bild{inGroup} gesendet.",
"notificationAudio": "hat eine Sprachnachricht{inGroup} gesendet.",
"notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.", "notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.",
"notificationContactRequest": "möchte sich mit dir vernetzen.", "notificationContactRequest": "möchte sich mit dir vernetzen.",
"notificationAcceptRequest": "ist jetzt mit dir vernetzt.", "notificationAcceptRequest": "ist jetzt mit dir vernetzt.",
@ -793,6 +795,7 @@
"notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.", "notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.",
"notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.", "notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.",
"notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.",
"notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.",
"notificationResponse": "hat dir{inGroup} geantwortet.", "notificationResponse": "hat dir{inGroup} geantwortet.",
"notificationTitleUnknownUser": "Jemand", "notificationTitleUnknownUser": "Jemand",
"notificationCategoryMessageTitle": "Nachrichten", "notificationCategoryMessageTitle": "Nachrichten",

View file

@ -495,6 +495,7 @@
"appOutdated": "Your version of twonly is out of date.", "appOutdated": "Your version of twonly is out of date.",
"appOutdatedBtn": "Update Now", "appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again", "doubleClickToReopen": "Double-click\nto open again",
"uploadLimitReached": "The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.",
"retransmissionRequested": "Retransmission requested", "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!", "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", "openChangeLog": "Open changelog automatically",
@ -562,6 +563,7 @@
"notificationTwonly": "sent a twonly{inGroup}.", "notificationTwonly": "sent a twonly{inGroup}.",
"notificationVideo": "sent a video{inGroup}.", "notificationVideo": "sent a video{inGroup}.",
"notificationImage": "sent a image{inGroup}.", "notificationImage": "sent a image{inGroup}.",
"notificationAudio": "sent a voice message{inGroup}.",
"notificationAddedToGroup": "has added you to \"{groupname}\"", "notificationAddedToGroup": "has added you to \"{groupname}\"",
"notificationContactRequest": "wants to connect with you.", "notificationContactRequest": "wants to connect with you.",
"notificationAcceptRequest": "is now connected with you.", "notificationAcceptRequest": "is now connected with you.",
@ -571,6 +573,7 @@
"notificationReactionToVideo": "has reacted with {reaction} to your video.", "notificationReactionToVideo": "has reacted with {reaction} to your video.",
"notificationReactionToText": "has reacted with {reaction} to your message.", "notificationReactionToText": "has reacted with {reaction} to your message.",
"notificationReactionToImage": "has reacted with {reaction} to your image.", "notificationReactionToImage": "has reacted with {reaction} to your image.",
"notificationReactionToAudio": "has reacted with {reaction} to your audio message.",
"notificationResponse": "has responded{inGroup}.", "notificationResponse": "has responded{inGroup}.",
"notificationTitleUnknownUser": "Someone", "notificationTitleUnknownUser": "Someone",
"notificationCategoryMessageTitle": "Messages", "notificationCategoryMessageTitle": "Messages",

View file

@ -2072,6 +2072,12 @@ abstract class AppLocalizations {
/// **'Double-click\nto open again'** /// **'Double-click\nto open again'**
String get doubleClickToReopen; 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. /// No description provided for @retransmissionRequested.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2474,6 +2480,12 @@ abstract class AppLocalizations {
/// **'sent a image{inGroup}.'** /// **'sent a image{inGroup}.'**
String notificationImage(Object 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. /// No description provided for @notificationAddedToGroup.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2528,6 +2540,12 @@ abstract class AppLocalizations {
/// **'has reacted with {reaction} to your image.'** /// **'has reacted with {reaction} to your image.'**
String notificationReactionToImage(Object reaction); 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. /// No description provided for @notificationResponse.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -1098,6 +1098,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get doubleClickToReopen => 'Doppelklicken zum\nerneuten Öffnen.'; String get doubleClickToReopen => 'Doppelklicken zum\nerneuten Öffnen.';
@override
String get uploadLimitReached =>
'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.';
@override @override
String get retransmissionRequested => 'Wird erneut versucht.'; String get retransmissionRequested => 'Wird erneut versucht.';
@ -1352,6 +1356,11 @@ class AppLocalizationsDe extends AppLocalizations {
return 'hat ein Bild$inGroup gesendet.'; return 'hat ein Bild$inGroup gesendet.';
} }
@override
String notificationAudio(Object inGroup) {
return 'hat eine Sprachnachricht$inGroup gesendet.';
}
@override @override
String notificationAddedToGroup(Object groupname) { String notificationAddedToGroup(Object groupname) {
return 'hat dich zu \"$groupname\" hinzugefügt.'; return 'hat dich zu \"$groupname\" hinzugefügt.';
@ -1387,6 +1396,11 @@ class AppLocalizationsDe extends AppLocalizations {
return 'hat mit $reaction auf dein Bild reagiert.'; return 'hat mit $reaction auf dein Bild reagiert.';
} }
@override
String notificationReactionToAudio(Object reaction) {
return 'hat mit $reaction auf deine Sprachnachricht reagiert.';
}
@override @override
String notificationResponse(Object inGroup) { String notificationResponse(Object inGroup) {
return 'hat dir$inGroup geantwortet.'; return 'hat dir$inGroup geantwortet.';

View file

@ -1091,6 +1091,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get doubleClickToReopen => 'Double-click\nto open again'; 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 @override
String get retransmissionRequested => 'Retransmission requested'; String get retransmissionRequested => 'Retransmission requested';
@ -1344,6 +1348,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'sent a image$inGroup.'; return 'sent a image$inGroup.';
} }
@override
String notificationAudio(Object inGroup) {
return 'sent a voice message$inGroup.';
}
@override @override
String notificationAddedToGroup(Object groupname) { String notificationAddedToGroup(Object groupname) {
return 'has added you to \"$groupname\"'; return 'has added you to \"$groupname\"';
@ -1379,6 +1388,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'has reacted with $reaction to your image.'; return 'has reacted with $reaction to your image.';
} }
@override
String notificationReactionToAudio(Object reaction) {
return 'has reacted with $reaction to your audio message.';
}
@override @override
String notificationResponse(Object inGroup) { String notificationResponse(Object inGroup) {
return 'has responded$inGroup.'; return 'has responded$inGroup.';

View file

@ -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 IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE');
static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO'); 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 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> [ static const $core.List<EncryptedContent_Media_Type> values = <EncryptedContent_Media_Type> [
REUPLOAD, REUPLOAD,
IMAGE, IMAGE,
VIDEO, VIDEO,
GIF, GIF,
AUDIO,
]; ];
static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -264,6 +264,7 @@ const EncryptedContent_Media_Type$json = {
{'1': 'IMAGE', '2': 1}, {'1': 'IMAGE', '2': 1},
{'1': 'VIDEO', '2': 2}, {'1': 'VIDEO', '2': 2},
{'1': 'GIF', '2': 3}, {'1': 'GIF', '2': 3},
{'1': 'AUDIO', '2': 4},
], ],
}; };
@ -409,7 +410,7 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo' 'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo'
'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE' 'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE'
'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH' 'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH'
'CgVfdGV4dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' 'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW'
'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD' 'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD'
'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG' 'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG'
'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1' 'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1'
@ -417,31 +418,31 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI' 'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI'
'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2' 'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2'
'5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu' '5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu'
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRV' 'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV'
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0' 'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k'
'SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl' 'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2'
'9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEK' 'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp'
'C01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYX' 'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn'
'RlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQi' 'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl'
'NgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAh' 'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE'
'p4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250' 'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD'
'YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEg' 'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg'
'oKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRD' 'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk'
'b250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGA' 'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0'
'IgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNl' 'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD'
'cm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZR' 'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY'
'ILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFgoUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3Vz' 'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl'
'ZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3' 'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA'
'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ' 'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg'
'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' 'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS'
'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' 'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf'
'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' 'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG'
'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' 'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW'
'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG' 'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi'
'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf' 'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl'
'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW' 'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW'
'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l' 'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz'
'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX' 'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW'
'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi' 'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz'
'bGljS2V5'); 'ZW5kR3JvdXBQdWJsaWNLZXk=');

View file

@ -28,7 +28,9 @@ class PushKind extends $pb.ProtobufEnum {
static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo'); static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo');
static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText'); static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText');
static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage'); 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> [ static const $core.List<PushKind> values = <PushKind> [
reaction, reaction,
@ -45,7 +47,9 @@ class PushKind extends $pb.ProtobufEnum {
reactionToVideo, reactionToVideo,
reactionToText, reactionToText,
reactionToImage, reactionToImage,
reactionToAudio,
addedToGroup, addedToGroup,
audio,
]; ];
static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -31,7 +31,9 @@ const PushKind$json = {
{'1': 'reactionToVideo', '2': 11}, {'1': 'reactionToVideo', '2': 11},
{'1': 'reactionToText', '2': 12}, {'1': 'reactionToText', '2': 12},
{'1': 'reactionToImage', '2': 13}, {'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' 'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw'
'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ' 'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ'
'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0' 'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0'
'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEAoMYWRkZWRUb0dyb3VwEA4='); 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEwoPcmVhY3Rpb25Ub0F1ZGlvEA4SEAoMYWRkZWRUb0'
'dyb3VwEA8SCQoFYXVkaW8QEA==');
@$core.Deprecated('Use encryptedPushNotificationDescriptor instead') @$core.Deprecated('Use encryptedPushNotificationDescriptor instead')
const EncryptedPushNotification$json = { const EncryptedPushNotification$json = {

View file

@ -107,6 +107,7 @@ message EncryptedContent {
IMAGE = 1; IMAGE = 1;
VIDEO = 2; VIDEO = 2;
GIF = 3; GIF = 3;
AUDIO = 4;
} }
string senderMessageId = 1; string senderMessageId = 1;

View file

@ -22,7 +22,9 @@ enum PushKind {
reactionToVideo = 11; reactionToVideo = 11;
reactionToText = 12; reactionToText = 12;
reactionToImage = 13; reactionToImage = 13;
addedToGroup = 14; reactionToAudio = 14;
addedToGroup = 15;
audio = 16;
}; };
message PushNotification { message PushNotification {

View file

@ -62,6 +62,8 @@ Future<void> handleMedia(
mediaType = MediaType.video; mediaType = MediaType.video;
case EncryptedContent_Media_Type.GIF: case EncryptedContent_Media_Type.GIF:
mediaType = MediaType.gif; mediaType = MediaType.gif;
case EncryptedContent_Media_Type.AUDIO:
mediaType = MediaType.audio;
} }
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(

View file

@ -30,38 +30,52 @@ Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
enum DownloadMediaTypes { enum DownloadMediaTypes {
video, video,
image, image,
audio,
} }
Map<String, List<String>> defaultAutoDownloadOptions = { Map<String, List<String>> defaultAutoDownloadOptions = {
ConnectivityResult.mobile.name: [], ConnectivityResult.mobile.name: [
DownloadMediaTypes.audio.name,
],
ConnectivityResult.wifi.name: [ ConnectivityResult.wifi.name: [
DownloadMediaTypes.video.name, DownloadMediaTypes.video.name,
DownloadMediaTypes.image.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 connectivityResult = await Connectivity().checkConnectivity();
final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
if (connectivityResult.contains(ConnectivityResult.mobile)) { if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (isVideo) { if (type == MediaType.video) {
if (options[ConnectivityResult.mobile.name]! if (options[ConnectivityResult.mobile.name]!
.contains(DownloadMediaTypes.video.name)) { .contains(DownloadMediaTypes.video.name)) {
return true; return true;
} else if (type == MediaType.audio) {
if (options[ConnectivityResult.mobile.name]!
.contains(DownloadMediaTypes.audio.name)) {
return true;
} }
} else if (options[ConnectivityResult.mobile.name]! } else if (options[ConnectivityResult.mobile.name]!
.contains(DownloadMediaTypes.image.name)) { .contains(DownloadMediaTypes.image.name)) {
return true; return true;
} }
} }
}
if (connectivityResult.contains(ConnectivityResult.wifi)) { if (connectivityResult.contains(ConnectivityResult.wifi)) {
if (isVideo) { if (type == MediaType.video) {
if (options[ConnectivityResult.wifi.name]! if (options[ConnectivityResult.wifi.name]!
.contains(DownloadMediaTypes.video.name)) { .contains(DownloadMediaTypes.video.name)) {
return true; return true;
} }
} else if (type == MediaType.audio) {
if (options[ConnectivityResult.wifi.name]!
.contains(DownloadMediaTypes.audio.name)) {
return true;
}
} else if (options[ConnectivityResult.wifi.name]! } else if (options[ConnectivityResult.wifi.name]!
.contains(DownloadMediaTypes.image.name)) { .contains(DownloadMediaTypes.image.name)) {
return true; return true;
@ -110,8 +124,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
return; return;
} }
if (!force && if (!force && !await isAllowedToDownload(media.type)) {
!await isAllowedToDownload(isVideo: media.type == MediaType.video)) {
Log.warn( Log.warn(
'Download blocked for ${media.mediaId} because of network state.', 'Download blocked for ${media.mediaId} because of network state.',
); );

View file

@ -58,8 +58,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
final mediaId = update.task.taskId.replaceAll('upload_', ''); final mediaId = update.task.taskId.replaceAll('upload_', '');
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
if (update.status == TaskStatus.enqueued || if (update.status == TaskStatus.running) {
update.status == TaskStatus.running) {
// Ignore these updates // Ignore these updates
return; return;
} }
@ -103,25 +102,34 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
Log.error( Log.error(
'Got HTTP error ${update.responseStatusCode} for $mediaId', 'Got HTTP error ${update.responseStatusCode} for $mediaId',
); );
}
if (update.responseStatusCode == 429) { if (update.status == TaskStatus.notFound) {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaId, mediaId,
const MediaFilesCompanion( const MediaFilesCompanion(
uploadState: Value(UploadState.uploadLimitReached), uploadState: Value(UploadState.uploadLimitReached),
), ),
); );
Log.info(
'Background upload failed for $mediaId with status ${update.responseStatusCode}. Not trying again.',
);
return; return;
} }
}
Log.info( Log.info(
'Background upload failed for $mediaId with status ${update.status}. Trying again.', 'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
); );
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); final mediaService = await MediaFileService.fromMedia(media);
await mediaService.setUploadState(UploadState.uploaded); await mediaService.setUploadState(UploadState.uploaded);
// In all other cases just try the upload again... // In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService); await startBackgroundMediaUpload(mediaService);
} }
}

View file

@ -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); await _uploadUploadRequest(mediaService);
} }
} }
@ -180,11 +181,16 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final downloadToken = getRandomUint8List(32); final downloadToken = getRandomUint8List(32);
var type = EncryptedContent_Media_Type.IMAGE; late EncryptedContent_Media_Type type;
if (media.mediaFile.type == MediaType.video) { switch (media.mediaFile.type) {
type = EncryptedContent_Media_Type.VIDEO; case MediaType.audio:
} else if (media.mediaFile.type == MediaType.gif) { type = EncryptedContent_Media_Type.AUDIO;
case MediaType.image:
type = EncryptedContent_Media_Type.IMAGE;
case MediaType.gif:
type = EncryptedContent_Media_Type.GIF; type = EncryptedContent_Media_Type.GIF;
case MediaType.video:
type = EncryptedContent_Media_Type.VIDEO;
} }
final notEncryptedContent = EncryptedContent( final notEncryptedContent = EncryptedContent(

View file

@ -78,6 +78,9 @@ class MediaFileService {
// Message was not yet opened, so do not remove it. // Message was not yet opened, so do not remove it.
delete = false; delete = false;
} }
if (service.mediaFile.type == MediaType.audio) {
delete = false; // do not delete voice messages
}
} }
} }
} }
@ -162,6 +165,7 @@ class MediaFileService {
} }
switch (mediaFile.type) { switch (mediaFile.type) {
case MediaType.gif: case MediaType.gif:
case MediaType.audio:
case MediaType.image: case MediaType.image:
// all images are already compress.. // all images are already compress..
break; break;
@ -181,6 +185,7 @@ class MediaFileService {
await compressImage(originalPath, tempPath); await compressImage(originalPath, tempPath);
case MediaType.video: case MediaType.video:
await compressAndOverlayVideo(this); await compressAndOverlayVideo(this);
case MediaType.audio:
case MediaType.gif: case MediaType.gif:
originalPath.copySync(tempPath.path); originalPath.copySync(tempPath.path);
} }
@ -267,6 +272,8 @@ class MediaFileService {
extension = 'mp4'; extension = 'mp4';
case MediaType.gif: case MediaType.gif:
extension = 'gif'; extension = 'gif';
case MediaType.audio:
extension = 'm4a';
} }
} }
final mediaBaseDir = final mediaBaseDir =

View file

@ -249,6 +249,7 @@ String getPushNotificationText(PushNotification pushNotification) {
PushKind.twonly.name: lang.notificationTwonly(inGroup), PushKind.twonly.name: lang.notificationTwonly(inGroup),
PushKind.video.name: lang.notificationVideo(inGroup), PushKind.video.name: lang.notificationVideo(inGroup),
PushKind.image.name: lang.notificationImage(inGroup), PushKind.image.name: lang.notificationImage(inGroup),
PushKind.video.name: lang.notificationAudio(inGroup),
PushKind.contactRequest.name: lang.notificationContactRequest, PushKind.contactRequest.name: lang.notificationContactRequest,
PushKind.acceptRequest.name: lang.notificationAcceptRequest, PushKind.acceptRequest.name: lang.notificationAcceptRequest,
PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile,
@ -256,6 +257,8 @@ String getPushNotificationText(PushNotification pushNotification) {
PushKind.reopenedMedia.name: lang.notificationReopenedMedia, PushKind.reopenedMedia.name: lang.notificationReopenedMedia,
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name:
lang.notificationReactionToVideo(pushNotification.additionalContent), lang.notificationReactionToVideo(pushNotification.additionalContent),
PushKind.reactionToAudio.name:
lang.notificationReactionToAudio(pushNotification.additionalContent),
PushKind.reactionToText.name: PushKind.reactionToText.name:
lang.notificationReactionToText(pushNotification.additionalContent), lang.notificationReactionToText(pushNotification.additionalContent),
PushKind.reactionToImage.name: PushKind.reactionToImage.name:

View file

@ -220,6 +220,8 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
switch (media.type) { switch (media.type) {
case MediaType.image: case MediaType.image:
kind = PushKind.reactionToImage; kind = PushKind.reactionToImage;
case MediaType.audio:
kind = PushKind.reactionToAudio;
case MediaType.video: case MediaType.video:
kind = PushKind.reactionToVideo; kind = PushKind.reactionToVideo;
case MediaType.gif: case MediaType.gif:
@ -241,13 +243,16 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
} }
if (content.hasMedia()) { if (content.hasMedia()) {
switch (content.media.type) { switch (content.media.type) {
case EncryptedContent_Media_Type.REUPLOAD:
return null;
case EncryptedContent_Media_Type.IMAGE: case EncryptedContent_Media_Type.IMAGE:
kind = PushKind.image; kind = PushKind.image;
case EncryptedContent_Media_Type.VIDEO: case EncryptedContent_Media_Type.VIDEO:
kind = PushKind.video; kind = PushKind.video;
// ignore: no_default_cases case EncryptedContent_Media_Type.GIF:
default: kind = PushKind.image;
return null; case EncryptedContent_Media_Type.AUDIO:
kind = PushKind.audio;
} }
if (content.media.requiresAuthentication) { if (content.media.requiresAuthentication) {
kind = PushKind.twonly; kind = PushKind.twonly;

View file

@ -293,6 +293,8 @@ Color getMessageColorFromType(
} else { } else {
if (mediaFile.type == MediaType.video) { if (mediaFile.type == MediaType.video) {
color = const Color.fromARGB(255, 243, 33, 208); color = const Color.fromARGB(255, 243, 33, 208);
} else if (mediaFile.type == MediaType.audio) {
color = const Color.fromARGB(255, 252, 149, 85);
} else { } else {
color = Colors.redAccent; color = Colors.redAccent;
} }

View file

@ -176,6 +176,7 @@ class _UserListItem extends State<GroupListItem> {
_previewMessages.where((x) => x.type == MessageType.media).toList(); _previewMessages.where((x) => x.type == MessageType.media).toList();
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
if (mediaFile?.type != MediaType.audio) {
if (mediaFile?.downloadState == null) return; if (mediaFile?.downloadState == null) return;
if (mediaFile!.downloadState! == DownloadState.pending) { if (mediaFile!.downloadState! == DownloadState.pending) {
await startDownloadMedia(mediaFile, true); await startDownloadMedia(mediaFile, true);
@ -194,6 +195,7 @@ class _UserListItem extends State<GroupListItem> {
return; return;
} }
} }
}
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(
context, context,

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.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/model/memory_item.model.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.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_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/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/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';

View file

@ -2,14 +2,17 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart' import 'package:twonly/src/database/tables/messages.table.dart'
hide MessageActions; hide MessageActions;
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.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_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_actions.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.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'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
@ -136,6 +139,16 @@ class _ChatListEntryState extends State<ChatListEntry> {
) )
: (mediaService == null) : (mediaService == null)
? null ? null
: (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( : ChatMediaEntry(
message: widget.message, message: widget.message,
group: widget.group, group: widget.group,

View file

@ -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);
}
}

View file

@ -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';
}

View file

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

View 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;
}

View file

@ -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);
}
}

View file

@ -1,8 +1,15 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/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/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';
@ -25,10 +32,14 @@ class MessageInput extends StatefulWidget {
State<MessageInput> createState() => _MessageInputState(); State<MessageInput> createState() => _MessageInputState();
} }
enum RecordingState { none, recording, finished }
class _MessageInputState extends State<MessageInput> { class _MessageInputState extends State<MessageInput> {
late final TextEditingController _textFieldController; late final TextEditingController _textFieldController;
late final RecorderController recorderController;
final bool isApple = Platform.isIOS; final bool isApple = Platform.isIOS;
bool _emojiShowing = false; bool _emojiShowing = false;
RecordingState _recordingState = RecordingState.none;
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_textFieldController.text == '') return; if (_textFieldController.text == '') return;
@ -48,6 +59,7 @@ class _MessageInputState extends State<MessageInput> {
void initState() { void initState() {
_textFieldController = TextEditingController(); _textFieldController = TextEditingController();
widget.textFieldFocus.addListener(_handleTextFocusChange); widget.textFieldFocus.addListener(_handleTextFocusChange);
_initializeControllers();
super.initState(); super.initState();
} }
@ -58,6 +70,14 @@ class _MessageInputState extends State<MessageInput> {
super.dispose(); super.dispose();
} }
void _initializeControllers() {
recorderController = RecorderController()
..androidEncoder = AndroidEncoder.aac
..androidOutputFormat = AndroidOutputFormat.mpeg4
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
..sampleRate = 44100;
}
void _handleTextFocusChange() { void _handleTextFocusChange() {
if (widget.textFieldFocus.hasFocus) { if (widget.textFieldFocus.hasFocus) {
setState(() { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -89,6 +136,7 @@ class _MessageInputState extends State<MessageInput> {
), ),
child: Row( child: Row(
children: [ children: [
if (_recordingState != RecordingState.recording)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
@ -119,7 +167,30 @@ class _MessageInputState extends State<MessageInput> {
), ),
), ),
Expanded( Expanded(
child: TextField( 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, controller: _textFieldController,
focusNode: widget.textFieldFocus, focusNode: widget.textFieldFocus,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
@ -139,6 +210,90 @@ class _MessageInputState extends State<MessageInput> {
), ),
), ),
), ),
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,
),
);
},
onLongPressCancel: () async {
final path = await recorderController.stop();
if (path == null) return;
if (File(path).existsSync()) {
File(path).deleteSync();
}
setState(() {
_recordingState = RecordingState.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,
),
),
),
],
),
),
], ],
), ),
), ),

View file

@ -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/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
enum MessageSendState { enum MessageSendState {
received, received,
@ -85,6 +86,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
final kindsAlreadyShown = HashSet<MessageType>(); final kindsAlreadyShown = HashSet<MessageType>();
var hasLoader = false; var hasLoader = false;
GestureTapCallback? onTap;
for (final message in widget.messages) { for (final message in widget.messages) {
if (icons.length == 2) break; if (icons.length == 2) break;
@ -147,7 +149,27 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
if (mediaFile != null) { if (mediaFile != null) {
if (mediaFile.uploadState == UploadState.uploadLimitReached) { 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) { if (mediaFile.uploadState == UploadState.preprocessing) {
text = 'Wird verarbeitet'; text = 'Wird verarbeitet';
@ -251,7 +273,9 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
); );
} }
return Row( return GestureDetector(
onTap: onTap,
child: Row(
mainAxisAlignment: widget.mainAxisAlignment, mainAxisAlignment: widget.mainAxisAlignment,
children: [ children: [
icon, icon,
@ -265,6 +289,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
], ],
),
); );
} }
} }

View file

@ -175,9 +175,16 @@ class _ResponsePreviewState extends State<ResponsePreview> {
} }
} }
if (_message!.type == MessageType.media && _mediaService != null) { if (_message!.type == MessageType.media && _mediaService != null) {
subtitle = _mediaService!.mediaFile.type == MediaType.video switch (_mediaService!.mediaFile.type) {
? context.lang.video case MediaType.image:
: context.lang.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) { 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( SizedBox(
height: widget.showBorder ? 100 : 210, height: widget.showBorder ? 100 : 210,
child: Image.file( child: Image.file(

View 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();
}
}

View file

@ -83,9 +83,10 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_directChat == null || if (_directChat == null ||
_directChat!.maxFlameCounter == 0 ||
_flameCounter >= (_directChat!.maxFlameCounter + 1) || _flameCounter >= (_directChat!.maxFlameCounter + 1) ||
_directChat!.lastFlameCounterChange! _directChat!.lastFlameCounterChange!
.isBefore(DateTime.now().subtract(const Duration(days: 5)))) { .isBefore(DateTime.now().subtract(const Duration(days: 4)))) {
return Container(); return Container();
} }
return BetterListTile( return BetterListTile(

View file

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" 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: avatar_maker:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -9,6 +9,7 @@ environment:
sdk: ^3.6.0 sdk: ^3.6.0
dependencies: dependencies:
audio_waveforms: ^1.3.0
avatar_maker: ^0.4.0 avatar_maker: ^0.4.0
background_downloader: ^9.2.2 background_downloader: ^9.2.2
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1