mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 13:08:42 +00:00
implementing voice messages #251
This commit is contained in:
parent
3706a36cf9
commit
95c9db86d6
42 changed files with 1252 additions and 379 deletions
|
|
@ -219,6 +219,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
|
||||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
.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}}\"",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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() &
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ enum MediaType {
|
||||||
image,
|
image,
|
||||||
video,
|
video,
|
||||||
gif,
|
gif,
|
||||||
|
audio,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UploadState {
|
enum UploadState {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.';
|
||||||
|
|
|
||||||
|
|
@ -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.';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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=');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:intl/intl.dart' hide TextDirection;
|
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
|
||||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
|
||||||
import 'package:twonly/src/views/components/better_text.dart';
|
|
||||||
|
|
||||||
class ChatTextEntry extends StatelessWidget {
|
|
||||||
const ChatTextEntry({
|
|
||||||
required this.message,
|
|
||||||
required this.nextMessage,
|
|
||||||
required this.prevMessage,
|
|
||||||
required this.borderRadius,
|
|
||||||
required this.userIdToContact,
|
|
||||||
required this.minWidth,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Message message;
|
|
||||||
final Message? nextMessage;
|
|
||||||
final Message? prevMessage;
|
|
||||||
final Map<int, Contact>? userIdToContact;
|
|
||||||
final BorderRadius borderRadius;
|
|
||||||
final double minWidth;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var text = message.content ?? '';
|
|
||||||
var textColor = Colors.white;
|
|
||||||
|
|
||||||
if (EmojiAnimation.supported(text)) {
|
|
||||||
return Container(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 100,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 4,
|
|
||||||
horizontal: 10,
|
|
||||||
),
|
|
||||||
child: EmojiAnimation(emoji: text),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayTime = !combineTextMessageWithNext(message, nextMessage);
|
|
||||||
var displayUserName = '';
|
|
||||||
if (message.senderId != null &&
|
|
||||||
userIdToContact != null &&
|
|
||||||
userIdToContact![message.senderId] != null) {
|
|
||||||
if (prevMessage == null) {
|
|
||||||
displayUserName =
|
|
||||||
getContactDisplayName(userIdToContact![message.senderId]!);
|
|
||||||
} else {
|
|
||||||
if (!combineTextMessageWithNext(prevMessage!, message)) {
|
|
||||||
displayUserName =
|
|
||||||
getContactDisplayName(userIdToContact![message.senderId]!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var spacerWidth = minWidth - measureTextWidth(text) - 53;
|
|
||||||
if (spacerWidth < 0) spacerWidth = 0;
|
|
||||||
|
|
||||||
Color? color;
|
|
||||||
var expanded = false;
|
|
||||||
if (message.quotesMessageId == null) {
|
|
||||||
color = getMessageColor(message);
|
|
||||||
}
|
|
||||||
if (message.isDeletedFromSender) {
|
|
||||||
color = context.color.surfaceBright;
|
|
||||||
displayTime = false;
|
|
||||||
} else if (measureTextWidth(text) > 270) {
|
|
||||||
expanded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.isDeletedFromSender) {
|
|
||||||
text = context.lang.messageWasDeleted;
|
|
||||||
color = isDarkMode(context) ? Colors.black : Colors.grey;
|
|
||||||
if (isDarkMode(context)) {
|
|
||||||
textColor = const Color.fromARGB(255, 99, 99, 99);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
|
||||||
minWidth: minWidth,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color,
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (displayUserName != '')
|
|
||||||
Text(
|
|
||||||
displayUserName,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (expanded)
|
|
||||||
Expanded(
|
|
||||||
child: BetterText(text: text, textColor: textColor),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
BetterText(text: text, textColor: textColor),
|
|
||||||
SizedBox(
|
|
||||||
width: spacerWidth,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (displayTime || message.modifiedAt != null)
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentGeometry.centerRight,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 6),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (message.modifiedAt != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 5),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 10,
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.pencil,
|
|
||||||
color: Colors.white.withAlpha(150),
|
|
||||||
size: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
friendlyTime(
|
|
||||||
context,
|
|
||||||
(message.modifiedAt != null)
|
|
||||||
? message.modifiedAt!
|
|
||||||
: message.createdAt,
|
|
||||||
),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white.withAlpha(150),
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double measureTextWidth(
|
|
||||||
String text,
|
|
||||||
) {
|
|
||||||
final tp = TextPainter(
|
|
||||||
text: TextSpan(text: text, style: const TextStyle(fontSize: 17)),
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
maxLines: 1,
|
|
||||||
)..layout();
|
|
||||||
return tp.size.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
|
||||||
if (nextMessage != null && nextMessage.content != null) {
|
|
||||||
if (nextMessage.senderId == message.senderId) {
|
|
||||||
if (nextMessage.type == MessageType.text &&
|
|
||||||
message.type == MessageType.text) {
|
|
||||||
if (!EmojiAnimation.supported(nextMessage.content!)) {
|
|
||||||
final diff =
|
|
||||||
nextMessage.createdAt.difference(message.createdAt).inMinutes;
|
|
||||||
if (diff <= 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String friendlyTime(BuildContext context, DateTime dt) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final diff = now.difference(dt);
|
|
||||||
|
|
||||||
if (diff.inMinutes >= 0 && diff.inMinutes < 60) {
|
|
||||||
final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes;
|
|
||||||
if (minutes <= 1) {
|
|
||||||
return context.lang.now;
|
|
||||||
}
|
|
||||||
return '$minutes ${context.lang.minutesShort}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine 24h vs 12h from system/local settings
|
|
||||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
|
||||||
|
|
||||||
if (!use24Hour) {
|
|
||||||
// 12-hour format with locale-aware AM/PM
|
|
||||||
final format = DateFormat.jm(Localizations.localeOf(context).toString());
|
|
||||||
return format.format(dt);
|
|
||||||
} else {
|
|
||||||
// 24-hour HH:mm, locale-aware
|
|
||||||
final format = DateFormat.Hm(Localizations.localeOf(context).toString());
|
|
||||||
return format.format(dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart';
|
||||||
|
import 'package:twonly/src/views/components/better_text.dart';
|
||||||
|
|
||||||
|
class ChatAudioEntry extends StatelessWidget {
|
||||||
|
const ChatAudioEntry({
|
||||||
|
required this.message,
|
||||||
|
required this.nextMessage,
|
||||||
|
required this.mediaService,
|
||||||
|
required this.prevMessage,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.userIdToContact,
|
||||||
|
required this.minWidth,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
final MediaFileService mediaService;
|
||||||
|
final Message? nextMessage;
|
||||||
|
final Message? prevMessage;
|
||||||
|
final Map<int, Contact>? userIdToContact;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final double minWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!mediaService.tempPath.existsSync() &&
|
||||||
|
!mediaService.originalPath.existsSync()) {
|
||||||
|
return Container(); // media file was purged
|
||||||
|
}
|
||||||
|
final info = getBubbleInfo(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
nextMessage,
|
||||||
|
prevMessage,
|
||||||
|
userIdToContact,
|
||||||
|
minWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
minWidth: 250,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: info.color,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (info.displayUserName != '')
|
||||||
|
Text(
|
||||||
|
info.displayUserName,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (info.text != '')
|
||||||
|
Expanded(
|
||||||
|
child: BetterText(text: info.text, textColor: info.textColor),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
if (mediaService.mediaFile.downloadState ==
|
||||||
|
DownloadState.ready ||
|
||||||
|
mediaService.mediaFile.downloadState == null)
|
||||||
|
mediaService.tempPath.existsSync()
|
||||||
|
? InChatAudioPlayer(
|
||||||
|
path: mediaService.tempPath.path,
|
||||||
|
message: message,
|
||||||
|
)
|
||||||
|
: (mediaService.originalPath.existsSync())
|
||||||
|
? InChatAudioPlayer(
|
||||||
|
path: mediaService.originalPath.path,
|
||||||
|
message: message,
|
||||||
|
)
|
||||||
|
: Container()
|
||||||
|
else
|
||||||
|
MessageSendStateIcon([message], [mediaService.mediaFile]),
|
||||||
|
],
|
||||||
|
if (info.displayTime || message.modifiedAt != null)
|
||||||
|
FriendlyMessageTime(message: message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InChatAudioPlayer extends StatefulWidget {
|
||||||
|
const InChatAudioPlayer({
|
||||||
|
required this.path,
|
||||||
|
required this.message,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InChatAudioPlayer> createState() => _InChatAudioPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
||||||
|
final PlayerController _playerController = PlayerController();
|
||||||
|
int _displayDuration = 0;
|
||||||
|
int _maxDuration = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_playerController
|
||||||
|
..preparePlayer(path: widget.path)
|
||||||
|
..setFinishMode(finishMode: FinishMode.pause);
|
||||||
|
|
||||||
|
_playerController.onCompletion.listen((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isPlaying = false;
|
||||||
|
_playerController.seekTo(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_playerController.onCurrentDurationChanged.listen((duration) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_displayDuration = _maxDuration - duration;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
initAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_playerController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initAsync() async {
|
||||||
|
_displayDuration = await _playerController.getDuration(DurationType.max);
|
||||||
|
_maxDuration = _displayDuration;
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPlaying = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (_isPlaying) {
|
||||||
|
_playerController.pausePlayer();
|
||||||
|
} else {
|
||||||
|
_playerController.startPlayer();
|
||||||
|
if (widget.message.senderId != null &&
|
||||||
|
widget.message.openedAt == null) {
|
||||||
|
notifyContactAboutOpeningMessage(
|
||||||
|
widget.message.senderId!,
|
||||||
|
[widget.message.messageId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isPlaying = !_isPlaying;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: _isPlaying ? 2 : 0,
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: FaIcon(
|
||||||
|
_isPlaying ? FontAwesomeIcons.pause : FontAwesomeIcons.play,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
formatMsToMinSec(_displayDuration),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
AudioFileWaveforms(
|
||||||
|
playerController: _playerController,
|
||||||
|
size: const Size(150, 40),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatMsToMinSec(int milliseconds) {
|
||||||
|
final d = Duration(milliseconds: milliseconds);
|
||||||
|
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||||
|
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||||
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
|
import 'package:twonly/src/views/components/better_text.dart';
|
||||||
|
|
||||||
|
class ChatTextEntry extends StatelessWidget {
|
||||||
|
const ChatTextEntry({
|
||||||
|
required this.message,
|
||||||
|
required this.nextMessage,
|
||||||
|
required this.prevMessage,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.userIdToContact,
|
||||||
|
required this.minWidth,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
final Message? nextMessage;
|
||||||
|
final Message? prevMessage;
|
||||||
|
final Map<int, Contact>? userIdToContact;
|
||||||
|
final BorderRadius borderRadius;
|
||||||
|
final double minWidth;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final text = message.content ?? '';
|
||||||
|
|
||||||
|
if (EmojiAnimation.supported(text)) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 100,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 4,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
child: EmojiAnimation(emoji: text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final info = getBubbleInfo(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
nextMessage,
|
||||||
|
prevMessage,
|
||||||
|
userIdToContact,
|
||||||
|
minWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
minWidth: minWidth,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: info.color,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (info.displayUserName != '')
|
||||||
|
Text(
|
||||||
|
info.displayUserName,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (info.expanded)
|
||||||
|
Expanded(
|
||||||
|
child: BetterText(text: text, textColor: info.textColor),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
BetterText(text: text, textColor: info.textColor),
|
||||||
|
SizedBox(
|
||||||
|
width: info.spacerWidth,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (info.displayTime || message.modifiedAt != null)
|
||||||
|
FriendlyMessageTime(message: message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/src/views/chats/chat_messages_components/entries/common.dart
Normal file
101
lib/src/views/chats/chat_messages_components/entries/common.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||||
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
|
|
||||||
|
class BubbleInfo {
|
||||||
|
late String text;
|
||||||
|
late Color textColor;
|
||||||
|
late bool displayTime;
|
||||||
|
late String displayUserName;
|
||||||
|
late Color color;
|
||||||
|
late bool expanded;
|
||||||
|
late double spacerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
BubbleInfo getBubbleInfo(
|
||||||
|
BuildContext context,
|
||||||
|
Message message,
|
||||||
|
Message? nextMessage,
|
||||||
|
Message? prevMessage,
|
||||||
|
Map<int, Contact>? userIdToContact,
|
||||||
|
double minWidth,
|
||||||
|
) {
|
||||||
|
final info = BubbleInfo()
|
||||||
|
..text = message.content ?? ''
|
||||||
|
..textColor = Colors.white
|
||||||
|
..color = getMessageColor(message)
|
||||||
|
..displayTime = !combineTextMessageWithNext(message, nextMessage)
|
||||||
|
..displayUserName = '';
|
||||||
|
|
||||||
|
if (message.senderId != null &&
|
||||||
|
userIdToContact != null &&
|
||||||
|
userIdToContact[message.senderId] != null) {
|
||||||
|
if (prevMessage == null) {
|
||||||
|
info.displayUserName =
|
||||||
|
getContactDisplayName(userIdToContact[message.senderId]!);
|
||||||
|
} else {
|
||||||
|
if (!combineTextMessageWithNext(prevMessage, message)) {
|
||||||
|
info.displayUserName =
|
||||||
|
getContactDisplayName(userIdToContact[message.senderId]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.spacerWidth = minWidth - measureTextWidth(info.text) - 53;
|
||||||
|
if (info.spacerWidth < 0) info.spacerWidth = 0;
|
||||||
|
|
||||||
|
info.expanded = false;
|
||||||
|
if (message.quotesMessageId == null) {
|
||||||
|
info.color = getMessageColor(message);
|
||||||
|
}
|
||||||
|
if (message.isDeletedFromSender) {
|
||||||
|
info
|
||||||
|
..color = context.color.surfaceBright
|
||||||
|
..displayTime = false;
|
||||||
|
} else if (measureTextWidth(info.text) > 270) {
|
||||||
|
info.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isDeletedFromSender) {
|
||||||
|
info
|
||||||
|
..text = context.lang.messageWasDeleted
|
||||||
|
..color = isDarkMode(context) ? Colors.black : Colors.grey;
|
||||||
|
if (isDarkMode(context)) {
|
||||||
|
info.textColor = const Color.fromARGB(255, 99, 99, 99);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
double measureTextWidth(
|
||||||
|
String text,
|
||||||
|
) {
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: TextSpan(text: text, style: const TextStyle(fontSize: 17)),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
maxLines: 1,
|
||||||
|
)..layout();
|
||||||
|
return tp.size.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
||||||
|
if (nextMessage != null && nextMessage.content != null) {
|
||||||
|
if (nextMessage.senderId == message.senderId) {
|
||||||
|
if (nextMessage.type == MessageType.text &&
|
||||||
|
message.type == MessageType.text) {
|
||||||
|
if (!EmojiAnimation.supported(nextMessage.content!)) {
|
||||||
|
final diff =
|
||||||
|
nextMessage.createdAt.difference(message.createdAt).inMinutes;
|
||||||
|
if (diff <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:intl/intl.dart' show DateFormat;
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class FriendlyMessageTime extends StatelessWidget {
|
||||||
|
const FriendlyMessageTime({required this.message, super.key});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: AlignmentGeometry.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (message.modifiedAt != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 10,
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.pencil,
|
||||||
|
color: Colors.white.withAlpha(150),
|
||||||
|
size: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
friendlyTime(
|
||||||
|
context,
|
||||||
|
(message.modifiedAt != null)
|
||||||
|
? message.modifiedAt!
|
||||||
|
: message.createdAt,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white.withAlpha(150),
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String friendlyTime(BuildContext context, DateTime dt) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(dt);
|
||||||
|
|
||||||
|
if (diff.inMinutes >= 0 && diff.inMinutes < 60) {
|
||||||
|
final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes;
|
||||||
|
if (minutes <= 1) {
|
||||||
|
return context.lang.now;
|
||||||
|
}
|
||||||
|
return '$minutes ${context.lang.minutesShort}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine 24h vs 12h from system/local settings
|
||||||
|
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||||
|
|
||||||
|
if (!use24Hour) {
|
||||||
|
// 12-hour format with locale-aware AM/PM
|
||||||
|
final format = DateFormat.jm(Localizations.localeOf(context).toString());
|
||||||
|
return format.format(dt);
|
||||||
|
} else {
|
||||||
|
// 24-hour HH:mm, locale-aware
|
||||||
|
final format = DateFormat.Hm(Localizations.localeOf(context).toString());
|
||||||
|
return format.format(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import '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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
242
lib/src/views/chats/chat_messages_components/test
Normal file
242
lib/src/views/chats/chat_messages_components/test
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||||
|
import 'package:audio_waveforms_example/chat_bubble.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const MyApp());
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const MaterialApp(
|
||||||
|
title: 'Audio Waveforms',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Home(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Home extends StatefulWidget {
|
||||||
|
const Home({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Home> createState() => _HomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeState extends State<Home> {
|
||||||
|
late final RecorderController recorderController;
|
||||||
|
|
||||||
|
String? path;
|
||||||
|
String? musicFile;
|
||||||
|
bool isRecording = false;
|
||||||
|
bool isRecordingCompleted = false;
|
||||||
|
bool isLoading = true;
|
||||||
|
late Directory appDirectory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getDir();
|
||||||
|
_initialiseControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _getDir() async {
|
||||||
|
appDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
path = "${appDirectory.path}/recording.m4a";
|
||||||
|
isLoading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initialiseControllers() {
|
||||||
|
recorderController = RecorderController()
|
||||||
|
..androidEncoder = AndroidEncoder.aac
|
||||||
|
..androidOutputFormat = AndroidOutputFormat.mpeg4
|
||||||
|
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
|
||||||
|
..sampleRate = 44100;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pickFile() async {
|
||||||
|
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||||
|
if (result != null) {
|
||||||
|
musicFile = result.files.single.path;
|
||||||
|
setState(() {});
|
||||||
|
} else {
|
||||||
|
debugPrint("File not picked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
recorderController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFF252331),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: const Color(0xFF252331),
|
||||||
|
elevation: 1,
|
||||||
|
centerTitle: true,
|
||||||
|
shadowColor: Colors.grey,
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
scale: 1.5,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Text(
|
||||||
|
'Simform',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: isLoading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 4,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return WaveBubble(
|
||||||
|
index: index + 1,
|
||||||
|
isSender: index.isOdd,
|
||||||
|
width: MediaQuery.of(context).size.width / 2,
|
||||||
|
appDirectory: appDirectory,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isRecordingCompleted)
|
||||||
|
WaveBubble(
|
||||||
|
path: path,
|
||||||
|
isSender: true,
|
||||||
|
appDirectory: appDirectory,
|
||||||
|
),
|
||||||
|
if (musicFile != null)
|
||||||
|
WaveBubble(
|
||||||
|
path: musicFile,
|
||||||
|
isSender: true,
|
||||||
|
appDirectory: appDirectory,
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: isRecording
|
||||||
|
? AudioWaveforms(
|
||||||
|
enableGesture: true,
|
||||||
|
size: Size(
|
||||||
|
MediaQuery.of(context).size.width / 2,
|
||||||
|
50),
|
||||||
|
recorderController: recorderController,
|
||||||
|
waveStyle: const WaveStyle(
|
||||||
|
waveColor: Colors.white,
|
||||||
|
extendWaveform: true,
|
||||||
|
showMiddleLine: false,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
color: const Color(0xFF1E1B26),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(left: 18),
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
width:
|
||||||
|
MediaQuery.of(context).size.width / 1.7,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E1B26),
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(left: 18),
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15),
|
||||||
|
child: TextField(
|
||||||
|
readOnly: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Type Something...",
|
||||||
|
hintStyle: const TextStyle(
|
||||||
|
color: Colors.white54),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.only(top: 16),
|
||||||
|
border: InputBorder.none,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: _pickFile,
|
||||||
|
icon: Icon(Icons.adaptive.share),
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _refreshWave,
|
||||||
|
icon: Icon(
|
||||||
|
isRecording ? Icons.refresh : Icons.send,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _startOrStopRecording,
|
||||||
|
icon: Icon(isRecording ? Icons.stop : Icons.mic),
|
||||||
|
color: Colors.white,
|
||||||
|
iconSize: 28,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startOrStopRecording() async {
|
||||||
|
try {
|
||||||
|
if (isRecording) {
|
||||||
|
recorderController.reset();
|
||||||
|
|
||||||
|
path = await recorderController.stop(false);
|
||||||
|
|
||||||
|
if (path != null) {
|
||||||
|
isRecordingCompleted = true;
|
||||||
|
debugPrint(path);
|
||||||
|
debugPrint("Recorded file size: ${File(path!).lengthSync()}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await recorderController.record(path: path); // Path is optional
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
} finally {
|
||||||
|
if (recorderController.hasPermission) {
|
||||||
|
setState(() {
|
||||||
|
isRecording = !isRecording;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refreshWave() {
|
||||||
|
if (isRecording) recorderController.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,9 +83,10 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
||||||
@override
|
@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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue