diff --git a/.gitmodules b/.gitmodules index a3642d1..41a0a1b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "dependencies/flutter_secure_storage"] - path = dependencies/flutter_secure_storage - url = https://github.com/juliansteenbakker/flutter_secure_storage [submodule "dependencies/flutter_zxing"] path = dependencies/flutter_zxing url = https://github.com/khoren93/flutter_zxing.git -[submodule "dependencies/flutter-pie-menu"] - path = dependencies/flutter-pie-menu - url = https://github.com/otsmr/flutter-pie-menu.git diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..fca9f99 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: macos + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5121631..6827338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.0.62 + +- Support for groups with multiple administrators +- Edit and delete messages +- Create images using volume buttons +- New and improved emoji picker +- Removing audio after recording is possible +- Edited image is now embedded into the video +- Video max length increased to 60 seconds +- Switched to FFmpeg for improved video compression +- New context menu and other UI enhancements +- Client-to-client protocol migrated to Protocol Buffers (Protobuf) +- Database identifiers converted to UUIDs and the database schema completely redesigned +- Improved reliability of client-to-client messaging +- Multiple bug fixes + + ## 0.0.61 - Improving image editor when changing colors diff --git a/README.md b/README.md index 33187be..b6229df 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ This repository contains the complete source code of the [twonly](https://twonly - Offer a Snapchat™ like experience - End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll) +- twonly is Open Source and can be downloaded directly from GitHub +- Developed by humans not by AI or Vibe Coding - No email or phone number required to register - Privacy friendly - Everything is stored on the device +- The backend is hosted exclusively in Europe -## In work +## Planned -- For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM -- For Android: Reproducible Builds + Publishing on Github/F-Droid +- For Android: Optional support for [UnifiedPush](https://unifiedpush.org/) +- For Android: Reproducible Builds - Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata -- Maybe: Switching from the Signal Protocol to [MLS](https://openmls.tech/). ## Security Issues If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send @@ -33,8 +35,6 @@ guarantee a bounty currently :/ Some dependencies are downloaded directly from the source as there are some new changes which are not yet published on pub.dev or because they require some special installation. -- `flutter_secure_storage`: We need the 10.0.0-beta version, but this version has some issues which are fixed but [not yet published](https://github.com/juliansteenbakker/flutter_secure_storage/issues/866): - ```bash git submodule update --init --recursive diff --git a/android/app/build.gradle b/android/app/build.gradle index a500868..bb0c654 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -55,6 +55,9 @@ android { debug { applicationIdSuffix ".testing" } + // profile { + // applicationIdSuffix ".STOP" + // } release { signingConfig signingConfigs.release } diff --git a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt index ce8739d..bb90f69 100644 --- a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt +++ b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt @@ -1,5 +1,21 @@ package eu.twonly import io.flutter.embedding.android.FlutterFragmentActivity +import android.view.KeyEvent +import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink +import android.view.KeyEvent.KEYCODE_VOLUME_DOWN +import android.view.KeyEvent.KEYCODE_VOLUME_UP -class MainActivity: FlutterFragmentActivity() +class MainActivity : FlutterFragmentActivity() { + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) { + eventSink!!.success(true) + return true + } + if (keyCode == KEYCODE_VOLUME_UP && eventSink != null) { + eventSink!!.success(false) + return true + } + return super.onKeyDown(keyCode, event) + } +} diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index e807f77..8606e90 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -6,4 +6,6 @@ + + diff --git a/build.yaml b/build.yaml index 71f1ccd..01b6605 100644 --- a/build.yaml +++ b/build.yaml @@ -10,4 +10,5 @@ targets: drift_dev: options: databases: - twonly_database: lib/src/database/twonly_database.dart \ No newline at end of file + twonly_db: lib/src/database/twonly.db.dart + twonly_database: lib/src/database/twonly_database_old.dart \ No newline at end of file diff --git a/dependencies/flutter-pie-menu b/dependencies/flutter-pie-menu deleted file mode 160000 index 22df3f2..0000000 --- a/dependencies/flutter-pie-menu +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 22df3f2ab9ad71db60526668578a5309b3cc84ef diff --git a/dependencies/flutter_secure_storage b/dependencies/flutter_secure_storage deleted file mode 160000 index 71b75a3..0000000 --- a/dependencies/flutter_secure_storage +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71b75a36f35f2ce945998e20c6c6aa1820babfc6 diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 57de2d5..9fb06e1 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -35,6 +35,7 @@ class NotificationService: UNNotificationServiceExtension { bestAttemptContent.body = data!.body bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) } else { + NSLog("Could not decrypt message. Show default.") bestAttemptContent.title = "\(bestAttemptContent.title)" } @@ -81,8 +82,9 @@ func getPushNotificationData(pushData: String) -> ( key: pushKey.key, pushData: pushData) if pushNotification != nil { pushUser = tryPushUser - if pushNotification!.messageID <= pushUser!.lastMessageID { - return ("blocked", "blocked", 0) + if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID) + { + //return ("blocked", "blocked", 0) } break } @@ -107,11 +109,12 @@ func getPushNotificationData(pushData: String) -> ( } else if pushUser != nil { return ( pushUser!.displayName, - getPushNotificationText(pushNotification: pushNotification), pushUser!.userID + getPushNotificationText(pushNotification: pushNotification).0, pushUser!.userID ) } else { + let content = getPushNotificationText(pushNotification: pushNotification) return ( - "", getPushNotificationTextWithoutUserId(pushKind: pushNotification.kind), 1 + content.1, content.0, 1 ) } @@ -125,6 +128,16 @@ func getPushNotificationData(pushData: String) -> ( } } +func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool { + guard uuid1.count >= 8, uuid2.count >= 8 else { return true } + let hex1 = String(uuid1.prefix(8)) + let hex2 = String(uuid2.prefix(8)) + guard let timestamp1 = UInt32(hex1, radix: 16), + let timestamp2 = UInt32(hex2, radix: 16) + else { return true } + return timestamp1 > timestamp2 +} + func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNotification? { do { @@ -152,8 +165,8 @@ func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNo func getPushUsers() -> [PushUser]? { // Retrieve the data from secure storage (Keychain) - guard let pushUsersB64 = readFromKeychain(key: "receiving_push_keys") else { - NSLog("No data found for key: receiving_push_keys") + guard let pushUsersB64 = readFromKeychain(key: "push_keys_receiving") else { + NSLog("No data found for key: push_keys_receiving") return nil } guard let pushUsersBytes = Data(base64Encoded: pushUsersB64) else { @@ -192,100 +205,66 @@ func readFromKeychain(key: String) -> String? { return nil } -func getPushNotificationText(pushNotification: PushNotification) -> String { +func getPushNotificationText(pushNotification: PushNotification) -> (String, String) { let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language var pushNotificationText: [PushKind: String] = [:] + var title = "Someone" // Define the messages based on the system language if systemLanguage.contains("de") { // German + title = "Jemand" pushNotificationText = [ - .text: "hat dir eine Nachricht gesendet.", - .twonly: "hat dir ein twonly gesendet.", - .video: "hat dir ein Video gesendet.", - .image: "hat dir ein Bild gesendet.", + .text: "hat eine Nachricht{inGroup} gesendet.", + .twonly: "hat ein twonly{inGroup} gesendet.", + .video: "hat ein Video{inGroup} gesendet.", + .image: "hat ein Bild{inGroup} gesendet.", + .audio: "hat eine Sprachnachricht{inGroup} gesendet.", .contactRequest: "möchte sich mit dir vernetzen.", .acceptRequest: "ist jetzt mit dir vernetzt.", .storedMediaFile: "hat dein Bild gespeichert.", .reaction: "hat auf dein Bild reagiert.", .testNotification: "Das ist eine Testbenachrichtigung.", .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "hat mit {{reaction}} auf dein Video reagiert.", - .reactionToText: "hat mit {{reaction}} auf deinen Text reagiert.", - .reactionToImage: "hat mit {{reaction}} auf dein Bild reagiert.", - .response: "hat dir geantwortet.", + .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", + .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", + .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", + .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.", + .response: "hat dir{inGroup} geantwortet.", + .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", ] } else { // Default to English pushNotificationText = [ - .text: "has sent you a message.", - .twonly: "has sent you a twonly.", - .video: "has sent you a video.", - .image: "has sent you an image.", + .text: "sent a message{inGroup}.", + .twonly: "sent a twonly{inGroup}.", + .video: "sent a video{inGroup}.", + .image: "sent a image{inGroup}.", + .audio: "sent a voice message{inGroup}.", .contactRequest: "wants to connect with you.", .acceptRequest: "is now connected with you.", .storedMediaFile: "has stored your image.", .reaction: "has reacted to your image.", .testNotification: "This is a test notification.", .reopenedMedia: "has reopened your image.", - .reactionToVideo: "has reacted with {{reaction}} to your video.", - .reactionToText: "has reacted with {{reaction}} to your text.", - .reactionToImage: "has reacted with {{reaction}} to your image.", - .response: "has responded.", + .reactionToVideo: "has reacted with {{content}} to your video.", + .reactionToText: "has reacted with {{content}} to your text.", + .reactionToImage: "has reacted with {{content}} to your image.", + .reactionToAudio: "has reacted with {{content}} to your voice message.", + .response: "has responded{inGroup}.", + .addedToGroup: "has added you to \"{{content}}\"", ] } var content = pushNotificationText[pushNotification.kind] ?? "" - if pushNotification.hasReactionContent { - content.replace("{{reaction}}", with: pushNotification.reactionContent) + if pushNotification.hasAdditionalContent { + content.replace("{{content}}", with: pushNotification.additionalContent) + content.replace("{inGroup}", with: " in {inGroup}") + content.replace("{inGroup}", with: pushNotification.additionalContent) + } else { + content.replace("{inGroup}", with: "") } // Return the corresponding message or an empty string if not found - return content -} - -func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String { - let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language - - var pushNotificationText: [PushKind: String] = [:] - - // Define the messages based on the system language - if systemLanguage.contains("de") { // German - pushNotificationText = [ - .text: "Du hast eine Nachricht erhalten.", - .twonly: "Du hast ein twonly erhalten.", - .video: "Du hast ein Video erhalten.", - .image: "Du hast ein Bild erhalten.", - .contactRequest: "Du hast eine Kontaktanfrage erhalten.", - .acceptRequest: "Deine Kontaktanfrage wurde angenommen.", - .storedMediaFile: "Dein Bild wurde gespeichert.", - .reaction: "Du hast eine Reaktion auf dein Bild erhalten.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "Du hast eine Reaktion auf dein Video erhalten.", - .reactionToText: "Du hast eine Reaktion auf deinen Text erhalten.", - .reactionToImage: "Du hast eine Reaktion auf dein Bild erhalten.", - .response: "Du hast eine Antwort erhalten.", - ] - } else { // Default to English - pushNotificationText = [ - .text: "You got a message.", - .twonly: "You got a twonly.", - .video: "You got a video.", - .image: "You got an image.", - .contactRequest: "You got a contact request.", - .acceptRequest: "Your contact request has been accepted.", - .storedMediaFile: "Your image has been saved.", - .reaction: "You got a reaction to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "You got a reaction to your video.", - .reactionToText: "You got a reaction to your text.", - .reactionToImage: "You got a reaction to your image.", - .response: "You got a response.", - ] - } - - // Return the corresponding message or an empty string if not found - return pushNotificationText[pushKind] ?? "" + return (content, title) } diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift index be89a9d..88baecf 100644 --- a/ios/NotificationService/push_notification.pb.swift +++ b/ios/NotificationService/push_notification.pb.swift @@ -37,6 +37,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case reactionToVideo // = 11 case reactionToText // = 12 case reactionToImage // = 13 + case reactionToAudio // = 14 + case addedToGroup // = 15 + case audio // = 16 case UNRECOGNIZED(Int) init() { @@ -59,6 +62,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case 11: self = .reactionToVideo case 12: self = .reactionToText case 13: self = .reactionToImage + case 14: self = .reactionToAudio + case 15: self = .addedToGroup + case 16: self = .audio default: self = .UNRECOGNIZED(rawValue) } } @@ -79,6 +85,9 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { case .reactionToVideo: return 11 case .reactionToText: return 12 case .reactionToImage: return 13 + case .reactionToAudio: return 14 + case .addedToGroup: return 15 + case .audio: return 16 case .UNRECOGNIZED(let i): return i } } @@ -99,11 +108,14 @@ enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { .reactionToVideo, .reactionToText, .reactionToImage, + .reactionToAudio, + .addedToGroup, + .audio, ] } -struct EncryptedPushNotification: @unchecked Sendable { +struct EncryptedPushNotification: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -128,8 +140,8 @@ struct PushNotification: Sendable { var kind: PushKind = .reaction - var messageID: Int64 { - get {return _messageID ?? 0} + var messageID: String { + get {return _messageID ?? String()} set {_messageID = newValue} } /// Returns true if `messageID` has been explicitly set. @@ -137,21 +149,21 @@ struct PushNotification: Sendable { /// Clears the value of `messageID`. Subsequent reads from it will return its default value. mutating func clearMessageID() {self._messageID = nil} - var reactionContent: String { - get {return _reactionContent ?? String()} - set {_reactionContent = newValue} + var additionalContent: String { + get {return _additionalContent ?? String()} + set {_additionalContent = newValue} } - /// Returns true if `reactionContent` has been explicitly set. - var hasReactionContent: Bool {return self._reactionContent != nil} - /// Clears the value of `reactionContent`. Subsequent reads from it will return its default value. - mutating func clearReactionContent() {self._reactionContent = nil} + /// Returns true if `additionalContent` has been explicitly set. + var hasAdditionalContent: Bool {return self._additionalContent != nil} + /// Clears the value of `additionalContent`. Subsequent reads from it will return its default value. + mutating func clearAdditionalContent() {self._additionalContent = nil} var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _messageID: Int64? = nil - fileprivate var _reactionContent: String? = nil + fileprivate var _messageID: String? = nil + fileprivate var _additionalContent: String? = nil } struct PushUsers: Sendable { @@ -177,8 +189,8 @@ struct PushUser: Sendable { var blocked: Bool = false - var lastMessageID: Int64 { - get {return _lastMessageID ?? 0} + var lastMessageID: String { + get {return _lastMessageID ?? String()} set {_lastMessageID = newValue} } /// Returns true if `lastMessageID` has been explicitly set. @@ -192,10 +204,10 @@ struct PushUser: Sendable { init() {} - fileprivate var _lastMessageID: Int64? = nil + fileprivate var _lastMessageID: String? = nil } -struct PushKey: @unchecked Sendable { +struct PushKey: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -214,32 +226,12 @@ struct PushKey: @unchecked Sendable { // MARK: - Code below here is support for the SwiftProtobuf runtime. extension PushKind: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "reaction"), - 1: .same(proto: "response"), - 2: .same(proto: "text"), - 3: .same(proto: "video"), - 4: .same(proto: "twonly"), - 5: .same(proto: "image"), - 6: .same(proto: "contactRequest"), - 7: .same(proto: "acceptRequest"), - 8: .same(proto: "storedMediaFile"), - 9: .same(proto: "testNotification"), - 10: .same(proto: "reopenedMedia"), - 11: .same(proto: "reactionToVideo"), - 12: .same(proto: "reactionToText"), - 13: .same(proto: "reactionToImage"), - ] + 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 { static let protoMessageName: String = "EncryptedPushNotification" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "keyId"), - 2: .same(proto: "nonce"), - 3: .same(proto: "ciphertext"), - 4: .same(proto: "mac"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}keyId\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -284,11 +276,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushNotification" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "kind"), - 2: .same(proto: "messageId"), - 3: .same(proto: "reactionContent"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}additionalContent\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -297,8 +285,8 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }() - case 2: try { try decoder.decodeSingularInt64Field(value: &self._messageID) }() - case 3: try { try decoder.decodeSingularStringField(value: &self._reactionContent) }() + case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._additionalContent) }() default: break } } @@ -313,9 +301,9 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1) } try { if let v = self._messageID { - try visitor.visitSingularInt64Field(value: v, fieldNumber: 2) + try visitor.visitSingularStringField(value: v, fieldNumber: 2) } }() - try { if let v = self._reactionContent { + try { if let v = self._additionalContent { try visitor.visitSingularStringField(value: v, fieldNumber: 3) } }() try unknownFields.traverse(visitor: &visitor) @@ -324,7 +312,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme static func ==(lhs: PushNotification, rhs: PushNotification) -> Bool { if lhs.kind != rhs.kind {return false} if lhs._messageID != rhs._messageID {return false} - if lhs._reactionContent != rhs._reactionContent {return false} + if lhs._additionalContent != rhs._additionalContent {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -332,9 +320,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUsers" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "users"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}users\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -364,13 +350,7 @@ extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushUser" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "userId"), - 2: .same(proto: "displayName"), - 3: .same(proto: "blocked"), - 4: .same(proto: "lastMessageId"), - 5: .same(proto: "pushKeys"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}userId\0\u{1}displayName\0\u{1}blocked\0\u{1}lastMessageId\0\u{1}pushKeys\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -381,7 +361,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }() case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }() case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }() - case 4: try { try decoder.decodeSingularInt64Field(value: &self._lastMessageID) }() + case 4: try { try decoder.decodeSingularStringField(value: &self._lastMessageID) }() case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }() default: break } @@ -403,7 +383,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3) } try { if let v = self._lastMessageID { - try visitor.visitSingularInt64Field(value: v, fieldNumber: 4) + try visitor.visitSingularStringField(value: v, fieldNumber: 4) } }() if !self.pushKeys.isEmpty { try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5) @@ -424,11 +404,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "PushKey" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .same(proto: "key"), - 3: .same(proto: "createdAtUnixTimestamp"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{1}createdAtUnixTimestamp\0") mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 42f70db..57db739 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - audio_waveforms (0.0.1): + - Flutter - background_downloader (0.0.1): - Flutter - camera_avfoundation (0.0.1): @@ -9,6 +11,13 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter + - emoji_picker_flutter (0.0.1): + - Flutter + - ffmpeg_kit_flutter_new (1.0.0): + - ffmpeg_kit_flutter_new/full-gpl (= 1.0.0) + - Flutter + - ffmpeg_kit_flutter_new/full-gpl (1.0.0): + - Flutter - Firebase (12.4.0): - Firebase/Core (= 12.4.0) - Firebase/Core (12.4.0): @@ -77,6 +86,8 @@ PODS: - flutter_secure_storage_darwin (10.0.0): - Flutter - FlutterMacOS + - flutter_volume_controller (0.0.1): + - Flutter - flutter_zxing (0.0.1): - Flutter - gal (1.0.0): @@ -233,21 +244,19 @@ PODS: - SwiftProtobuf (1.32.0) - url_launcher_ios (0.0.1): - Flutter - - video_compress (0.3.0): - - Flutter - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS - - video_thumbnail (0.0.1): - - Flutter - - libwebp DEPENDENCIES: + - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) + - ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`) - Firebase - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) @@ -259,6 +268,7 @@ DEPENDENCIES: - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_zxing (from `.symlinks/plugins/flutter_zxing/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - GoogleUtilities @@ -275,9 +285,7 @@ DEPENDENCIES: - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - SwiftProtobuf - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_compress (from `.symlinks/plugins/video_compress/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) SPEC REPOS: trunk: @@ -302,6 +310,8 @@ SPEC REPOS: - SwiftProtobuf EXTERNAL SOURCES: + audio_waveforms: + :path: ".symlinks/plugins/audio_waveforms/ios" background_downloader: :path: ".symlinks/plugins/background_downloader/ios" camera_avfoundation: @@ -312,6 +322,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cryptography_flutter_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" + ffmpeg_kit_flutter_new: + :path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" firebase_messaging: @@ -326,6 +340,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage_darwin: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + flutter_volume_controller: + :path: ".symlinks/plugins/flutter_volume_controller/ios" flutter_zxing: :path: ".symlinks/plugins/flutter_zxing/ios" gal: @@ -354,19 +370,18 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - video_compress: - :path: ".symlinks/plugins/video_compress/ios" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" - video_thumbnail: - :path: ".symlinks/plugins/video_thumbnail/ios" SPEC CHECKSUMS: + audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc + ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_messaging: 82c70650c426a0a14873e1acdb9ec2b443c4e8b4 @@ -380,20 +395,21 @@ SPEC CHECKSUMS: flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468 + flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93 gal: baecd024ebfd13c441269ca7404792a7152fde89 GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1 GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf @@ -401,15 +417,13 @@ SPEC CHECKSUMS: SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 SwiftProtobuf: 81e341191afbddd64aa031bd12862dccfab2f639 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - video_compress: f2133a07762889d67f0711ac831faa26f956980e - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b - video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a PODFILE CHECKSUM: 47470fbd5b59affa461eaf943ac57acce81e0ee8 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3b02435..2997989 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -263,8 +263,8 @@ D21FCEAC2D9F2B750088701D /* Embed Foundation Extensions */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - A3027D78D4FF6E79C9EFD470 /* [CP] Copy Pods Resources */, 32D7521D6B8F508A844DBC22 /* [CP] Embed Pods Frameworks */, + A7154597C13DDED7C7F2355C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -485,7 +485,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A3027D78D4FF6E79C9EFD470 /* [CP] Copy Pods Resources */ = { + A7154597C13DDED7C7F2355C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( diff --git a/lib/app.dart b/lib/app.dart index 36c9c2d..74fd9b2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,12 +6,16 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart'; +import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; +import 'package:twonly/src/views/updates/62_database_migration.view.dart'; class App extends StatefulWidget { const App({super.key}); @@ -35,8 +39,8 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); }; - globalCallbackUpdatePlan = (String planId) async { - await context.read().updatePlan(planId); + globalCallbackUpdatePlan = (SubscriptionPlan plan) async { + await context.read().updatePlan(plan); }; unawaited(initAsync()); @@ -44,22 +48,11 @@ class _AppState extends State with WidgetsBindingObserver { Future setUserPlan() async { final user = await getUser(); - globalBestFriendUserId = -1; if (user != null && mounted) { - if (user.myBestFriendContactId != null) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(user.myBestFriendContactId!) - .getSingleOrNull(); - if (contact != null) { - if (contact.alsoBestFriend) { - globalBestFriendUserId = user.myBestFriendContactId ?? 0; - } - } - } if (mounted) { - await context - .read() - .updatePlan(user.subscriptionPlan); + await context.read().updatePlan( + planFromString(user.subscriptionPlan), + ); } } } @@ -68,8 +61,6 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); await apiService.connect(force: true); await apiService.listenToNetworkChanges(); - // call this function so invalid media files are get purged - await retryMediaUpload(true); } @override @@ -84,7 +75,6 @@ class _AppState extends State with WidgetsBindingObserver { } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; - unawaited(handleUploadWhenAppGoesBackground()); } } @@ -92,7 +82,7 @@ class _AppState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); globalCallbackConnectionState = ({required bool isConnected}) {}; - globalCallbackUpdatePlan = (String planId) {}; + globalCallbackUpdatePlan = (SubscriptionPlan planId) {}; super.dispose(); } @@ -131,6 +121,8 @@ class _AppState extends State with WidgetsBindingObserver { colorScheme: ColorScheme.fromSeed( brightness: Brightness.dark, seedColor: const Color(0xFF57CC99), + surface: const Color.fromARGB(255, 20, 18, 23), + surfaceContainer: const Color.fromARGB(255, 33, 30, 39), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), @@ -159,36 +151,85 @@ class AppMainWidget extends StatefulWidget { } class _AppMainWidgetState extends State { - Future userCreated = isUserCreated(); - bool showOnboarding = true; + bool _isUserCreated = false; + bool _showDatabaseMigration = false; + bool _showOnboarding = true; + bool _isLoaded = false; + + (Future?, bool) _proofOfWork = (null, false); + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + _isUserCreated = await isUserCreated(); + + if (_isUserCreated) { + if (gUser.appVersion < 62) { + _showDatabaseMigration = true; + } + } + + if (!_isUserCreated && !_showDatabaseMigration) { + // This means the user is in the onboarding screen, so start with the Proof of Work. + + final (proof, disabled) = await apiService.getProofOfWork(); + if (proof != null) { + Log.info('Starting with proof of work calculation.'); + // Starting with the proof of work. + _proofOfWork = + (calculatePoW(proof.prefix, proof.difficulty.toInt()), false); + } else { + _proofOfWork = (null, disabled); + } + } + + setState(() { + _isLoaded = true; + }); + } @override Widget build(BuildContext context) { + if (!_isLoaded) { + return Center(child: Container()); + } + + late Widget child; + + if (_showDatabaseMigration) { + child = const DatabaseMigrationView(); + } else if (_isUserCreated) { + if (gUser.twonlySafeBackup == null) { + child = TwonlyIdentityBackupView( + callBack: () { + setState(() {}); + }, + ); + } else { + child = HomeView( + initialPage: widget.initialPage, + ); + } + } else if (_showOnboarding) { + child = OnboardingView( + callbackOnSuccess: () => setState(() { + _showOnboarding = false; + }), + ); + } else { + child = RegisterView( + callbackOnSuccess: initAsync, + proofOfWork: _proofOfWork, + ); + } + return Stack( children: [ - FutureBuilder( - future: userCreated, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center(child: Container()); - } else if (snapshot.data!) { - return HomeView( - initialPage: widget.initialPage, - ); - } else if (showOnboarding) { - return OnboardingView( - callbackOnSuccess: () => setState(() { - showOnboarding = false; - }), - ); - } - return RegisterView( - callbackOnSuccess: () => setState(() { - userCreated = isUserCreated(); - }), - ); - }, - ), + child, const AppOutdated(), ], ); diff --git a/lib/globals.dart b/lib/globals.dart index b5c0a9c..5a9acbf 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,14 +1,22 @@ +import 'dart:ui'; + import 'package:camera/camera.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; late ApiService apiService; // uses for background notification -late TwonlyDatabase twonlyDB; +late TwonlyDB twonlyDB; List gCameras = []; +// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called, +// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart +late UserData gUser; + // The following global function can be called from anywhere to update // the UI when something changed. The callbacks will be set by // App widget. @@ -19,7 +27,9 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({ }) {}; void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackNewDeviceRegistered = () {}; -void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; +void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = + (SubscriptionPlan plan) {}; + +Map globalUserDataChangedCallBack = {}; bool globalIsAppInBackground = true; -int globalBestFriendUserId = -1; diff --git a/lib/main.dart b/lib/main.dart index f2fb4a4..607db29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,25 +1,30 @@ +// ignore_for_file: unused_import + import 'dart:async'; +import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/app.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/api.service.dart'; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/fcm.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'app.dart'; - void main() async { WidgetsFlutterBinding.ensureInitialized(); await initFCMService(); @@ -28,9 +33,7 @@ void main() async { final user = await getUser(); if (user != null) { - if (user.isDemoUser) { - await deleteLocalUserData(); - } + gUser = user; } final settingsController = SettingsChangeProvider(); @@ -42,21 +45,26 @@ void main() async { gCameras = await availableCameras(); + // try { + // File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite')) + // .deleteSync(); + // } catch (e) {} + // await updateUserdata((u) { + // u.appVersion = 0; + // return u; + // }); + apiService = ApiService(); - twonlyDB = TwonlyDatabase(); - - await twonlyDB.messagesDao.resetPendingDownloadState(); - await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); - await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); - await twonlyDB.signalDao.purgeOutDatedPreKeys(); - - // Purge media files in the background - unawaited(purgeReceivedMediaFiles()); - unawaited(purgeSendMediaFiles()); - - unawaited(performTwonlySafeBackup()); + twonlyDB = TwonlyDB(); await initFileDownloader(); + unawaited(finishStartedPreprocessing()); + + unawaited(MediaFileService.purgeTempFolder()); + unawaited(createPushAvatars()); + await twonlyDB.messagesDao.purgeMessageTable(); + + unawaited(performTwonlySafeBackup()); runApp( MultiProvider( diff --git a/lib/src/constants/secure_storage_keys.dart b/lib/src/constants/secure_storage_keys.dart index 8e2eac5..43da069 100644 --- a/lib/src/constants/secure_storage_keys.dart +++ b/lib/src/constants/secure_storage_keys.dart @@ -6,6 +6,6 @@ class SecureStorageKeys { static const String userData = 'userData'; static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash'; - static const String receivingPushKeys = 'receiving_push_keys'; - static const String sendingPushKeys = 'sending_push_keys'; + static const String receivingPushKeys = 'push_keys_receiving'; + static const String sendingPushKeys = 'push_keys_sending'; } diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart new file mode 100644 index 0000000..0d4b6a8 --- /dev/null +++ b/lib/src/database/daos/contacts.dao.dart @@ -0,0 +1,171 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/database/twonly_database_old.dart' as old; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'contacts.dao.g.dart'; + +@DriftAccessor(tables: [Contacts]) +class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + ContactsDao(super.db); + + Future insertContact(ContactsCompanion contact) async { + try { + return await into(contacts).insert(contact); + } catch (e) { + Log.error(e); + return null; + } + } + + Future insertOnConflictUpdate(ContactsCompanion contact) async { + try { + return await into(contacts).insertOnConflictUpdate(contact); + } catch (e) { + Log.error(e); + return 0; + } + } + + SingleOrNullSelectable getContactByUserId(int userId) { + return select(contacts)..where((t) => t.userId.equals(userId)); + } + + Future getContactById(int userId) async { + return (select(contacts)..where((t) => t.userId.equals(userId))) + .getSingleOrNull(); + } + + Future> getContactsByUsername(String username) async { + return (select(contacts)..where((t) => t.username.equals(username))).get(); + } + + Future deleteContactByUserId(int userId) { + return (delete(contacts)..where((t) => t.userId.equals(userId))).go(); + } + + Future updateContact( + int userId, + ContactsCompanion updatedValues, + ) async { + await (update(contacts)..where((c) => c.userId.equals(userId))) + .write(updatedValues); + if (updatedValues.blocked.present || + updatedValues.displayName.present || + updatedValues.nickName.present) { + final contact = await getContactByUserId(userId).getSingleOrNull(); + if (contact != null) { + await updatePushUser(contact); + final group = await twonlyDB.groupsDao.getDirectChat(userId); + if (group != null) { + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); + } + } + } + } + + Stream> watchNotAcceptedContacts() { + return (select(contacts) + ..where( + (t) => + t.accepted.equals(false) & + t.blocked.equals(false) & + t.deletedByUser.equals(false), + )) + .watch(); + } + + Stream watchContact(int userid) { + return (select(contacts)..where((t) => t.userId.equals(userid))) + .watchSingleOrNull(); + } + + Future> getAllNotBlockedContacts() { + return select(contacts).get(); + } + + Stream watchContactsBlocked() { + final count = contacts.userId.count(); + final query = selectOnly(contacts) + ..where(contacts.blocked.equals(true)) + ..addColumns([count]); + return query.map((row) => row.read(count)).watchSingle(); + } + + Stream watchContactsRequested() { + final count = contacts.requested.count(distinct: true); + final query = selectOnly(contacts) + ..where( + contacts.requested.equals(true) & + contacts.accepted.equals(false) & + contacts.deletedByUser.equals(false) & + contacts.blocked.equals(false), + ) + ..addColumns([count]); + return query.map((row) => row.read(count)).watchSingle(); + } + + Stream> watchAllAcceptedContacts() { + return (select(contacts) + ..where((t) => t.blocked.equals(false) & t.accepted.equals(true))) + .watch(); + } + + Stream> watchAllContacts() { + return select(contacts).watch(); + } +} + +String getContactDisplayName(Contact user, {int? maxLength}) { + var name = user.username; + if (user.nickName != null && user.nickName != '') { + name = user.nickName!; + } else if (user.displayName != null) { + name = user.displayName!; + } + if (user.accountDeleted) { + name = applyStrikethrough(name); + } + if (maxLength != null) { + name = substringBy(name, maxLength); + } + return name; +} + +String substringBy(String string, int maxLength) { + if (string.length > maxLength) { + return '${string.substring(0, maxLength - 3)}...'; + } + return string; +} + +String getContactDisplayNameOld(old.Contact user) { + var name = user.username; + if (user.nickName != null && user.nickName != '') { + name = user.nickName!; + } else if (user.displayName != null) { + name = user.displayName!; + } + if (user.deleted) { + name = applyStrikethrough(name); + } + if (name.length > 12) { + return '${name.substring(0, 12)}...'; + } + return name; +} + +String applyStrikethrough(String text) { + return text.split('').map((char) => '$char\u0336').join(); +} diff --git a/lib/src/database/daos/contacts_dao.g.dart b/lib/src/database/daos/contacts.dao.g.dart similarity index 59% rename from lib/src/database/daos/contacts_dao.g.dart rename to lib/src/database/daos/contacts.dao.g.dart index 7f5fb6b..626cccb 100644 --- a/lib/src/database/daos/contacts_dao.g.dart +++ b/lib/src/database/daos/contacts.dao.g.dart @@ -1,8 +1,8 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'contacts_dao.dart'; +part of 'contacts.dao.dart'; // ignore_for_file: type=lint -mixin _$ContactsDaoMixin on DatabaseAccessor { +mixin _$ContactsDaoMixin on DatabaseAccessor { $ContactsTable get contacts => attachedDatabase.contacts; } diff --git a/lib/src/database/daos/contacts_dao.dart b/lib/src/database/daos/contacts_dao.dart deleted file mode 100644 index f97918f..0000000 --- a/lib/src/database/daos/contacts_dao.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; - -part 'contacts_dao.g.dart'; - -@DriftAccessor(tables: [Contacts]) -class ContactsDao extends DatabaseAccessor - with _$ContactsDaoMixin { - // this constructor is required so that the main database can create an instance - // of this object. - // ignore: matching_super_parameters - ContactsDao(super.db); - - Future insertContact(ContactsCompanion contact) async { - try { - return await into(contacts).insert(contact); - } catch (e) { - return 0; - } - } - - Future incFlameCounter( - int contactId, - bool received, - DateTime timestamp, - ) async { - final contact = (await (select(contacts) - ..where((t) => t.userId.equals(contactId))) - .get()) - .first; - - final totalMediaCounter = contact.totalMediaCounter + 1; - var flameCounter = contact.flameCounter; - - if (contact.lastMessageReceived != null && - contact.lastMessageSend != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (contact.lastMessageSend!.isBefore(twoDaysAgo) || - contact.lastMessageReceived!.isBefore(twoDaysAgo)) { - flameCounter = 0; - } - } - - var lastMessageSend = const Value.absent(); - var lastMessageReceived = const Value.absent(); - var lastFlameCounterChange = const Value.absent(); - - if (contact.lastFlameCounterChange != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - - if (contact.lastFlameCounterChange!.isBefore(startOfToday)) { - // last flame update was yesterday. check if it can be updated. - var updateFlame = false; - if (received) { - if (contact.lastMessageSend != null && - contact.lastMessageSend!.isAfter(startOfToday)) { - // today a message was already send -> update flame - updateFlame = true; - } - } else if (contact.lastMessageReceived != null && - contact.lastMessageReceived!.isAfter(startOfToday)) { - // today a message was already received -> update flame - updateFlame = true; - } - if (updateFlame) { - flameCounter += 1; - lastFlameCounterChange = Value(timestamp); - } - } - } else { - // There where no message until no... - lastFlameCounterChange = Value(timestamp); - } - - if (received) { - lastMessageReceived = Value(timestamp); - } else { - lastMessageSend = Value(timestamp); - } - - return (update(contacts)..where((t) => t.userId.equals(contactId))).write( - ContactsCompanion( - totalMediaCounter: Value(totalMediaCounter), - lastFlameCounterChange: lastFlameCounterChange, - lastMessageReceived: lastMessageReceived, - lastMessageSend: lastMessageSend, - flameCounter: Value(flameCounter), - ), - ); - } - - SingleOrNullSelectable getContactByUserId(int userId) { - return select(contacts)..where((t) => t.userId.equals(userId)); - } - - Future deleteContactByUserId(int userId) { - return (delete(contacts)..where((t) => t.userId.equals(userId))).go(); - } - - Future updateContact( - int userId, - ContactsCompanion updatedValues, - ) async { - await (update(contacts)..where((c) => c.userId.equals(userId))) - .write(updatedValues); - if (updatedValues.blocked.present || - updatedValues.displayName.present || - updatedValues.nickName.present) { - final contact = await getContactByUserId(userId).getSingleOrNull(); - if (contact != null) { - await updatePushUser(contact); - } - } - } - - Stream> watchNotAcceptedContacts() { - return (select(contacts) - ..where( - (t) => - t.accepted.equals(false) & - t.archived.equals(false) & - t.blocked.equals(false), - )) - .watch(); - // return (select(contacts)).watch(); - } - - Stream watchContact(int userid) { - return (select(contacts)..where((t) => t.userId.equals(userid))) - .watchSingleOrNull(); - } - - Stream> watchContactsForShareView() { - return (select(contacts) - ..where( - (t) => - t.accepted.equals(true) & - t.blocked.equals(false) & - t.deleted.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } - - Stream> watchContactsForStartNewChat() { - return (select(contacts) - ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } - - Stream> watchContactsForChatList() { - return (select(contacts) - ..where( - (t) => - t.accepted.equals(true) & - t.blocked.equals(false) & - t.archived.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .watch(); - } - - Future> getAllNotBlockedContacts() { - return (select(contacts) - ..where((t) => t.blocked.equals(false)) - ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) - .get(); - } - - Stream watchContactsBlocked() { - final count = contacts.userId.count(); - final query = selectOnly(contacts) - ..where(contacts.blocked.equals(true)) - ..addColumns([count]); - return query.map((row) => row.read(count)).watchSingle(); - } - - Stream watchContactsRequested() { - final count = contacts.requested.count(distinct: true); - final query = selectOnly(contacts) - ..where( - contacts.requested.equals(true) & contacts.accepted.equals(true).not(), - ) - ..addColumns([count]); - return query.map((row) => row.read(count)).watchSingle(); - } - - Stream> watchAllContacts() { - return select(contacts).watch(); - } - - Future modifyFlameCounterForTesting() async { - await update(contacts).write( - ContactsCompanion( - lastFlameCounterChange: Value(DateTime.now()), - flameCounter: const Value(1337), - lastFlameSync: const Value(null), - ), - ); - } - - Stream watchFlameCounter(int userId) { - return (select(contacts) - ..where( - (u) => - u.userId.equals(userId) & - u.lastMessageReceived.isNotNull() & - u.lastMessageSend.isNotNull(), - )) - .watchSingle() - .asyncMap(getFlameCounterFromContact); - } -} - -String getContactDisplayName(Contact user) { - var name = user.username; - if (user.nickName != null && user.nickName != '') { - name = user.nickName!; - } else if (user.displayName != null) { - name = user.displayName!; - } - if (user.deleted) { - name = applyStrikethrough(name); - } - if (name.length > 12) { - return '${name.substring(0, 12)}...'; - } - return name; -} - -String applyStrikethrough(String text) { - return text.split('').map((char) => '$char\u0336').join(); -} - -int getFlameCounterFromContact(Contact contact) { - if (contact.lastMessageSend == null || contact.lastMessageReceived == null) { - return 0; - } - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (contact.lastMessageSend!.isAfter(twoDaysAgo) && - contact.lastMessageReceived!.isAfter(twoDaysAgo)) { - return contact.flameCounter + 1; - } else { - return 0; - } -} diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart new file mode 100644 index 0000000..85ce7ec --- /dev/null +++ b/lib/src/database/daos/groups.dao.dart @@ -0,0 +1,395 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +part 'groups.dao.g.dart'; + +@DriftAccessor( + tables: [ + Groups, + GroupMembers, + GroupHistories, + ], +) +class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + GroupsDao(super.db); + + Future isContactInGroup(int contactId, String groupId) async { + final entry = await (select(groupMembers)..where( + // ignore: require_trailing_commas + (t) => t.contactId.equals(contactId) & t.groupId.equals(groupId))) + .getSingleOrNull(); + return entry != null; + } + + Future deleteGroup(String groupId) async { + await (delete(groups)..where((t) => t.groupId.equals(groupId))).go(); + } + + Future updateGroup( + String groupId, + GroupsCompanion updates, + ) async { + await (update(groups)..where((c) => c.groupId.equals(groupId))) + .write(updates); + } + + Future> getGroupNonLeftMembers(String groupId) async { + return (select(groupMembers) + ..where( + (t) => + t.groupId.equals(groupId) & + (t.memberState.equals(MemberState.leftGroup.name).not() | + t.memberState.isNull()), + )) + .get(); + } + + Future> getAllGroupMembers(String groupId) async { + return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + .get(); + } + + Future getGroupMemberByPublicKey(Uint8List publicKey) async { + return (select(groupMembers) + ..where((t) => t.groupPublicKey.equals(publicKey))) + .getSingleOrNull(); + } + + Future createNewGroup(GroupsCompanion group) async { + return _insertGroup(group); + } + + Future insertOrUpdateGroupMember(GroupMembersCompanion members) async { + await into(groupMembers).insertOnConflictUpdate(members); + } + + Future insertGroupAction(GroupHistoriesCompanion action) async { + var insertAction = action; + if (!action.groupHistoryId.present) { + insertAction = action.copyWith( + groupHistoryId: Value(uuid.v4()), + ); + } + await into(groupHistories).insert(insertAction); + } + + Stream> watchGroupActions(String groupId) { + return (select(groupHistories) + ..where((t) => t.groupId.equals(groupId)) + ..orderBy([(t) => OrderingTerm.asc(t.actionAt)])) + .watch(); + } + + Future updateMember( + String groupId, + int contactId, + GroupMembersCompanion updates, + ) async { + await (update(groupMembers) + ..where( + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), + )) + .write(updates); + } + + Future removeMember(String groupId, int contactId) async { + await (delete(groupMembers) + ..where( + (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), + )) + .go(); + } + + Future createNewDirectChat( + int contactId, + GroupsCompanion group, + ) async { + final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId); + final insertGroup = group.copyWith( + groupId: Value(groupIdDirectChat), + isDirectChat: const Value(true), + isGroupAdmin: const Value(true), + joinedGroup: const Value(true), + ); + + final result = await _insertGroup(insertGroup); + if (result != null) { + await into(groupMembers).insert( + GroupMembersCompanion( + groupId: Value(result.groupId), + contactId: Value( + contactId, + ), + ), + ); + } + return result; + } + + Future _insertGroup(GroupsCompanion group) async { + try { + await into(groups).insert(group); + return await (select(groups) + ..where((t) => t.groupId.equals(group.groupId.value))) + .getSingle(); + } catch (e) { + Log.error('Could not insert group: $e'); + return null; + } + } + + Future> getGroupContact(String groupId) async { + final query = (select(contacts).join([ + leftOuterJoin( + groupMembers, + groupMembers.contactId.equalsExp(contacts.userId), + useColumns: false, + ), + ]) + ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) + ..where(groupMembers.groupId.equals(groupId))); + return query.map((row) => row.readTable(contacts)).get(); + } + + Stream> watchGroupContact(String groupId) { + final query = (select(contacts).join([ + leftOuterJoin( + groupMembers, + groupMembers.contactId.equalsExp(contacts.userId), + useColumns: false, + ), + ]) + ..orderBy([OrderingTerm.desc(groupMembers.lastMessage)]) + ..where(groupMembers.groupId.equals(groupId))); + return query.map((row) => row.readTable(contacts)).watch(); + } + + Stream> watchGroupMembers(String groupId) { + final query = + (select(groupMembers)..where((t) => t.groupId.equals(groupId))).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(groupMembers.contactId), + ), + ]); + return query + .map((row) => (row.readTable(contacts), row.readTable(groupMembers))) + .watch(); + } + + Stream> watchGroupsForShareImage() { + return (select(groups) + ..where( + (g) => g.leftGroup.equals(false) & g.deletedContent.equals(false), + )) + .watch(); + } + + Stream watchGroup(String groupId) { + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .watchSingleOrNull(); + } + + Stream watchDirectChat(int contactId) { + final groupId = getUUIDforDirectChat(contactId, gUser.userId); + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .watchSingleOrNull(); + } + + Stream> watchGroupsForChatList() { + return (select(groups) + ..where((t) => t.deletedContent.equals(false)) + ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) + .watch(); + } + + Stream> watchGroupsForStartNewChat() { + return (select(groups) + ..where((t) => t.isDirectChat.equals(false)) + ..orderBy([(t) => OrderingTerm.asc(t.groupName)])) + .watch(); + } + + Future getGroup(String groupId) { + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .getSingleOrNull(); + } + + Stream watchFlameCounter(String groupId) { + return (select(groups) + ..where( + (u) => + u.groupId.equals(groupId) & + u.lastMessageReceived.isNotNull() & + u.lastMessageSend.isNotNull(), + )) + .watchSingleOrNull() + .asyncMap(getFlameCounterFromGroup); + } + + Future> getAllDirectChats() { + return (select(groups)..where((t) => t.isDirectChat.equals(true))).get(); + } + + Future> getAllNotJoinedGroups() { + return (select(groups) + ..where( + (t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false), + )) + .get(); + } + + Future> getAllGroupMemberWithoutPublicKey() { + final query = + ((select(groups)..where((t) => t.isDirectChat.equals(false))).join([ + leftOuterJoin( + groupMembers, + groupMembers.groupId.equalsExp(groups.groupId), + ), + ]) + ..where(groupMembers.groupPublicKey.isNull())); + return query.map((row) => row.readTable(groupMembers)).get(); + } + + Future getDirectChat(int userId) async { + final query = + ((select(groups)..where((t) => t.isDirectChat.equals(true))).join([ + leftOuterJoin( + groupMembers, + groupMembers.groupId.equalsExp(groups.groupId), + ), + ]) + ..where(groupMembers.contactId.equals(userId))); + + return query.map((row) => row.readTable(groups)).getSingleOrNull(); + } + + Future incFlameCounter( + String groupId, + bool received, + DateTime timestamp, + ) async { + final group = await (select(groups) + ..where((t) => t.groupId.equals(groupId))) + .getSingle(); + + final totalMediaCounter = group.totalMediaCounter + (received ? 0 : 1); + var flameCounter = group.flameCounter; + var maxFlameCounter = group.maxFlameCounter; + var maxFlameCounterFrom = group.maxFlameCounterFrom; + + if (group.lastMessageReceived != null && group.lastMessageSend != null) { + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + if (group.lastMessageSend!.isBefore(twoDaysAgo) || + group.lastMessageReceived!.isBefore(twoDaysAgo)) { + flameCounter = 0; + } + } + + var lastMessageSend = const Value.absent(); + var lastMessageReceived = const Value.absent(); + var lastFlameCounterChange = const Value.absent(); + + if (group.lastFlameCounterChange != null) { + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + + if (group.lastFlameCounterChange!.isBefore(startOfToday)) { + // last flame update was yesterday. check if it can be updated. + var updateFlame = false; + if (received) { + if (group.lastMessageSend != null && + group.lastMessageSend!.isAfter(startOfToday)) { + // today a message was already send -> update flame + updateFlame = true; + } + } else if (group.lastMessageReceived != null && + group.lastMessageReceived!.isAfter(startOfToday)) { + // today a message was already received -> update flame + updateFlame = true; + } + if (updateFlame) { + flameCounter += 1; + lastFlameCounterChange = Value(timestamp); + // Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days + if ((flameCounter + 1) >= maxFlameCounter || + maxFlameCounterFrom == null || + maxFlameCounterFrom + .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { + maxFlameCounter = flameCounter + 1; + maxFlameCounterFrom = DateTime.now(); + } + } + } + } else { + // There where no message until no... + lastFlameCounterChange = Value(timestamp); + } + + if (received) { + lastMessageReceived = Value(timestamp); + } else { + lastMessageSend = Value(timestamp); + } + + await (update(groups)..where((t) => t.groupId.equals(groupId))).write( + GroupsCompanion( + totalMediaCounter: Value(totalMediaCounter), + lastFlameCounterChange: lastFlameCounterChange, + lastMessageReceived: lastMessageReceived, + lastMessageSend: lastMessageSend, + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: Value(maxFlameCounterFrom), + ), + ); + } + + Stream watchSumTotalMediaCounter() { + final query = selectOnly(groups) + ..addColumns([groups.totalMediaCounter.sum()]); + return query.watch().map((rows) { + final expr = rows.first.read(groups.totalMediaCounter.sum()); + return expr ?? 0; + }); + } + + Future increaseLastMessageExchange( + String groupId, + DateTime newLastMessage, + ) async { + await (update(groups) + ..where( + (t) => + t.groupId.equals(groupId) & + (t.lastMessageExchange.isSmallerThanValue(newLastMessage)), + )) + .write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); + } +} + +int getFlameCounterFromGroup(Group? group) { + if (group == null) return 0; + if (group.lastMessageSend == null || group.lastMessageReceived == null) { + return 0; + } + final now = DateTime.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + if (group.lastMessageSend!.isAfter(twoDaysAgo) && + group.lastMessageReceived!.isAfter(twoDaysAgo)) { + return group.flameCounter + 1; + } else { + return 0; + } +} diff --git a/lib/src/database/daos/groups.dao.g.dart b/lib/src/database/daos/groups.dao.g.dart new file mode 100644 index 0000000..4f873ec --- /dev/null +++ b/lib/src/database/daos/groups.dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'groups.dao.dart'; + +// ignore_for_file: type=lint +mixin _$GroupsDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; + $ContactsTable get contacts => attachedDatabase.contacts; + $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; + $GroupHistoriesTable get groupHistories => attachedDatabase.groupHistories; +} diff --git a/lib/src/database/daos/media_uploads_dao.dart b/lib/src/database/daos/media_uploads_dao.dart deleted file mode 100644 index 97135b3..0000000 --- a/lib/src/database/daos/media_uploads_dao.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'media_uploads_dao.g.dart'; - -@DriftAccessor(tables: [MediaUploads]) -class MediaUploadsDao extends DatabaseAccessor - with _$MediaUploadsDaoMixin { - // ignore: matching_super_parameters - MediaUploadsDao(super.db); - - Future> getMediaUploadsForRetry() { - return (select(mediaUploads) - ..where( - (t) => t.state.equals(UploadState.receiverNotified.name).not(), - )) - .get(); - } - - Future updateMediaUpload( - int mediaUploadId, - MediaUploadsCompanion updatedValues, - ) { - return (update(mediaUploads) - ..where((c) => c.mediaUploadId.equals(mediaUploadId))) - .write(updatedValues); - } - - Future insertMediaUpload(MediaUploadsCompanion values) async { - try { - return await into(mediaUploads).insert(values); - } catch (e) { - Log.error('Error while inserting media upload: $e'); - return null; - } - } - - Future deleteMediaUpload(int mediaUploadId) { - return (delete(mediaUploads) - ..where((t) => t.mediaUploadId.equals(mediaUploadId))) - .go(); - } - - SingleOrNullSelectable getMediaUploadById(int mediaUploadId) { - return select(mediaUploads) - ..where((t) => t.mediaUploadId.equals(mediaUploadId)); - } -} diff --git a/lib/src/database/daos/media_uploads_dao.g.dart b/lib/src/database/daos/media_uploads_dao.g.dart deleted file mode 100644 index c2aa995..0000000 --- a/lib/src/database/daos/media_uploads_dao.g.dart +++ /dev/null @@ -1,8 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'media_uploads_dao.dart'; - -// ignore_for_file: type=lint -mixin _$MediaUploadsDaoMixin on DatabaseAccessor { - $MediaUploadsTable get mediaUploads => attachedDatabase.mediaUploads; -} diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart new file mode 100644 index 0000000..b1e09d9 --- /dev/null +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -0,0 +1,123 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'mediafiles.dao.g.dart'; + +@DriftAccessor(tables: [MediaFiles]) +class MediaFilesDao extends DatabaseAccessor + with _$MediaFilesDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + MediaFilesDao(super.db); + + Future insertMedia(MediaFilesCompanion mediaFile) async { + try { + var insertMediaFile = mediaFile; + + if (insertMediaFile.mediaId == const Value.absent()) { + insertMediaFile = mediaFile.copyWith( + mediaId: Value(uuid.v7()), + ); + } + + final rowId = await into(mediaFiles).insert(insertMediaFile); + + return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) + .getSingle(); + } catch (e) { + Log.error('Could not insert media file: $e'); + return null; + } + } + + Future deleteMediaFile(String mediaId) async { + await (delete(mediaFiles) + ..where( + (t) => t.mediaId.equals(mediaId), + )) + .go(); + } + + Future updateMedia( + String mediaId, + MediaFilesCompanion updates, + ) async { + await (update(mediaFiles)..where((c) => c.mediaId.equals(mediaId))) + .write(updates); + } + + Future updateAllMediaFiles( + MediaFilesCompanion updates, + ) async { + await update(mediaFiles).write(updates); + } + + Future getMediaFileById(String mediaId) async { + return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) + .getSingleOrNull(); + } + + Future getDraftMediaFile() async { + final medias = await (select(mediaFiles) + ..where((t) => t.isDraftMedia.equals(true))) + .get(); + if (medias.isEmpty) { + return null; + } + return medias.first; + } + + Stream watchMedia(String mediaId) { + return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) + .watchSingleOrNull(); + } + + Future resetPendingDownloadState() async { + await (update(mediaFiles) + ..where( + (c) => c.downloadState.equals( + DownloadState.downloading.name, + ), + )) + .write( + const MediaFilesCompanion( + downloadState: Value(DownloadState.pending), + ), + ); + } + + Future> getAllMediaFilesPendingDownload() async { + return (select(mediaFiles) + ..where( + (t) => + t.downloadState.equals(DownloadState.pending.name) | + t.downloadState.equals(DownloadState.downloading.name), + )) + .get(); + } + + Future> getAllMediaFilesPendingUpload() async { + return (select(mediaFiles) + ..where( + (t) => (t.uploadState.equals(UploadState.initialized.name) | + t.uploadState.equals(UploadState.uploadLimitReached.name) | + t.uploadState.equals(UploadState.preprocessing.name)), + )) + .get(); + } + + Stream> watchAllStoredMediaFiles() { + return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); + } + + Stream> watchNewestMediaFiles() { + return (select(mediaFiles) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(100)) + .watch(); + } +} diff --git a/lib/src/database/daos/mediafiles.dao.g.dart b/lib/src/database/daos/mediafiles.dao.g.dart new file mode 100644 index 0000000..0157e2d --- /dev/null +++ b/lib/src/database/daos/mediafiles.dao.g.dart @@ -0,0 +1,8 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mediafiles.dao.dart'; + +// ignore_for_file: type=lint +mixin _$MediaFilesDaoMixin on DatabaseAccessor { + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; +} diff --git a/lib/src/database/daos/message_retransmissions.dao.dart b/lib/src/database/daos/message_retransmissions.dao.dart deleted file mode 100644 index b4a7934..0000000 --- a/lib/src/database/daos/message_retransmissions.dao.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/message_retransmissions.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'message_retransmissions.dao.g.dart'; - -@DriftAccessor(tables: [MessageRetransmissions]) -class MessageRetransmissionDao extends DatabaseAccessor - with _$MessageRetransmissionDaoMixin { - // this constructor is required so that the main database can create an instance - // of this object. - // ignore: matching_super_parameters - MessageRetransmissionDao(super.db); - - Future insertRetransmission( - MessageRetransmissionsCompanion message, - ) async { - try { - return await into(messageRetransmissions).insert(message); - } catch (e) { - Log.error('Error while inserting message for retransmission: $e'); - return null; - } - } - - Future purgeOldRetransmissions() async { - // delete entries older than two weeks - await (delete(messageRetransmissions) - ..where( - (t) => (t.acknowledgeByServerAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 25), - ), - )), - )) - .go(); - } - - Future> getRetransmitAbleMessages() async { - final countDeleted = await (delete(messageRetransmissions) - ..where( - (t) => - t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull(), - )) - .go(); - - if (countDeleted > 0) { - Log.info('Deleted $countDeleted faulty retransmissions'); - } - - return (await (select(messageRetransmissions) - ..where((t) => t.acknowledgeByServerAt.isNull())) - .get()) - .map((msg) => msg.retransmissionId) - .toList(); - } - - SingleOrNullSelectable getRetransmissionById( - int retransmissionId, - ) { - return select(messageRetransmissions) - ..where((t) => t.retransmissionId.equals(retransmissionId)); - } - - Stream> watchAllMessages() { - return (select(messageRetransmissions) - ..orderBy([(t) => OrderingTerm.asc(t.retransmissionId)])) - .watch(); - } - - Future updateRetransmission( - int retransmissionId, - MessageRetransmissionsCompanion updatedValues, - ) { - return (update(messageRetransmissions) - ..where((c) => c.retransmissionId.equals(retransmissionId))) - .write(updatedValues); - } - - Future resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async { - return ((update(messageRetransmissions)) - ..where( - (m) => - m.contactId.equals(fromUserId) & - m.encryptedHash.equals(encryptedHash), - )) - .write( - const MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(null), - ), - ); - } - - Future getRetransmissionFromHash( - int fromUserId, - Uint8List encryptedHash, - ) async { - return ((select(messageRetransmissions)) - ..where( - (m) => - m.contactId.equals(fromUserId) & - m.encryptedHash.equals(encryptedHash), - )) - .getSingleOrNull(); - } - - Future deleteRetransmissionById(int retransmissionId) { - return (delete(messageRetransmissions) - ..where((t) => t.retransmissionId.equals(retransmissionId))) - .go(); - } - - Future clearRetransmissionTable() { - return delete(messageRetransmissions).go(); - } - - Future deleteRetransmissionByMessageId(int messageId) { - return (delete(messageRetransmissions) - ..where((t) => t.messageId.equals(messageId))) - .go(); - } -} diff --git a/lib/src/database/daos/message_retransmissions.dao.g.dart b/lib/src/database/daos/message_retransmissions.dao.g.dart deleted file mode 100644 index cd7ca29..0000000 --- a/lib/src/database/daos/message_retransmissions.dao.g.dart +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'message_retransmissions.dao.dart'; - -// ignore_for_file: type=lint -mixin _$MessageRetransmissionDaoMixin on DatabaseAccessor { - $ContactsTable get contacts => attachedDatabase.contacts; - $MessagesTable get messages => attachedDatabase.messages; - $MessageRetransmissionsTable get messageRetransmissions => - attachedDatabase.messageRetransmissions; -} diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart new file mode 100644 index 0000000..3576abe --- /dev/null +++ b/lib/src/database/daos/messages.dao.dart @@ -0,0 +1,550 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.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/reactions.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'messages.dao.g.dart'; + +@DriftAccessor( + tables: [ + Messages, + Contacts, + MediaFiles, + Reactions, + MessageHistories, + GroupMembers, + MessageActions, + Groups, + ], +) +class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + MessagesDao(super.db); + + Stream> watchMessageNotOpened(String groupId) { + return (select(messages) + ..where( + (t) => + t.openedAt.isNull() & + t.groupId.equals(groupId) & + t.isDeletedFromSender.equals(false), + ) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + + Stream> watchMediaNotOpened(String groupId) { + final query = select(messages).join([ + leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)), + ]) + ..where( + mediaFiles.downloadState + .equals(DownloadState.reuploadRequested.name) + .not() & + mediaFiles.type.equals(MediaType.audio.name).not() & + messages.openedAt.isNull() & + messages.groupId.equals(groupId) & + messages.mediaId.isNotNull() & + messages.senderId.isNotNull() & + messages.type.equals(MessageType.media.name), + ); + return query.map((row) => row.readTable(messages)).watch(); + } + + Stream watchLastMessage(String groupId) { + return (select(messages) + ..where((t) => t.groupId.equals(groupId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) + ..limit(1)) + .watchSingleOrNull(); + } + + Stream> watchByGroupId(String groupId) { + return ((select(messages) + ..where( + (t) => + t.groupId.equals(groupId) & + (t.isDeletedFromSender.equals(true) | + ((t.type.equals(MessageType.text.name) & + t.content.isNotNull()) | + (t.type.equals(MessageType.media.name) & + t.mediaId.isNotNull()))), + )) + ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + .watch(); + } + + Stream> watchMembersByGroupId(String groupId) { + final query = (select(groupMembers).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(groupMembers.contactId), + ), + ]) + ..where(groupMembers.groupId.equals(groupId))); + return query + .map((row) => (row.readTable(groupMembers), row.readTable(contacts))) + .watch(); + } + + Stream> watchMessageActionChanges(String messageId) { + return (select(messageActions)..where((t) => t.messageId.equals(messageId))) + .watch(); + } + + Stream watchMessageById(String messageId) { + return (select(messages)..where((t) => t.messageId.equals(messageId))) + .watchSingleOrNull(); + } + + Future purgeMessageTable() async { + final allGroups = await select(groups).get(); + + for (final group in allGroups) { + final deletionTime = DateTime.now().subtract( + Duration( + milliseconds: group.deleteMessagesAfterMilliseconds, + ), + ); + final affected = await (delete(messages) + ..where( + (m) => + m.groupId.equals(group.groupId) & + // m.messageId.equals(lastMessage.messageId).not() & + (m.mediaStored.equals(true) & + m.isDeletedFromSender.equals(true) | + m.mediaStored.equals(false)) & + (m.openedByAll.isSmallerThanValue(deletionTime) | + (m.isDeletedFromSender.equals(true) & + m.createdAt.isSmallerThanValue(deletionTime))), + )) + .go(); + Log.info('Deleted $affected messages.'); + } + } + + // Future> getAllMessagesPendingDownloading() { + // return (select(messages) + // ..where( + // (t) => + // t.downloadState.equals(DownloadState.downloaded.index).not() & + // t.messageOtherId.isNotNull() & + // t.errorWhileSending.equals(false) & + // t.kind.equals(MessageKind.media.name), + // )) + // .get(); + // } + + // Future> getAllNonACKMessagesFromUser() { + // return (select(messages) + // ..where( + // (t) => + // t.acknowledgeByUser.equals(false) & + // t.messageOtherId.isNull() & + // t.errorWhileSending.equals(false) & + // t.sendAt.isBiggerThanValue( + // DateTime.now().subtract(const Duration(minutes: 10)), + // ), + // )) + // .get(); + // } + + // Stream> getAllStoredMediaFiles() { + // return (select(messages) + // ..where((t) => t.mediaStored.equals(true)) + // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) + // .watch(); + // } + + // Future> getAllMessagesPendingUpload() { + // return (select(messages) + // ..where( + // (t) => + // t.acknowledgeByServer.equals(false) & + // t.messageOtherId.isNull() & + // t.mediaUploadId.isNotNull() & + // t.downloadState.equals(DownloadState.pending.index) & + // t.errorWhileSending.equals(false) & + // t.kind.equals(MessageKind.media.name), + // )) + // .get(); + // } + + Future openedAllTextMessages(String groupId) { + final updates = MessagesCompanion(openedAt: Value(DateTime.now())); + return (update(messages) + ..where( + (t) => + t.groupId.equals(groupId) & + t.senderId.isNotNull() & + t.openedAt.isNull() & + t.type.equals(MessageType.text.name), + )) + .write(updates); + } + + Future handleMessageDeletion( + int? contactId, + String messageId, + DateTime timestamp, + ) async { + final msg = await getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.senderId != contactId) { + Log.error('Message does not exists or contact is not owner.'); + return; + } + if (msg.mediaId != null && contactId != null) { + // contactId -> When a image is send to multiple and one message is delete the image should be still available... + await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) + .go(); + + final mediaService = await MediaFileService.fromMediaId(msg.mediaId!); + if (mediaService != null) { + mediaService.fullMediaRemoval(); + } + } + await (delete(messageHistories) + ..where((t) => t.messageId.equals(messageId))) + .go(); + + await (update(messages) + ..where( + (t) => t.messageId.equals(messageId), + )) + .write( + const MessagesCompanion( + isDeletedFromSender: Value(true), + content: Value(null), + mediaId: Value(null), + ), + ); + } + + Future handleTextEdit( + int? contactId, + String messageId, + String text, + DateTime timestamp, + ) async { + final msg = await getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.content == null || msg.senderId != contactId) { + return; + } + await into(messageHistories).insert( + MessageHistoriesCompanion( + messageId: Value(messageId), + content: Value(msg.content), + createdAt: Value(timestamp), + ), + ); + await (update(messages) + ..where( + (t) => t.messageId.equals(messageId), + )) + .write( + MessagesCompanion( + content: Value(text), + modifiedAt: Value(timestamp), + ), + ); + } + + Future handleMessageOpened( + int contactId, + String messageId, + DateTime timestamp, + ) async { + await into(messageActions).insertOnConflictUpdate( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.openedAt), + actionAt: Value(timestamp), + ), + ); + // Directly show as message opened as soon as one person has opened it + final openedByAll = + await haveAllMembers(messageId, MessageActionType.openedAt) + ? DateTime.now() + : null; + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion( + openedAt: Value(DateTime.now()), + openedByAll: Value(openedByAll), + ), + ); + } + + Future handleMessageAckByServer( + int contactId, + String messageId, + DateTime timestamp, + ) async { + await into(messageActions).insertOnConflictUpdate( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.ackByServerAt), + actionAt: Value(timestamp), + ), + ); + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(ackByServer: Value(DateTime.now())), + ); + } + + Future haveAllMembers( + String messageId, + MessageActionType action, + ) async { + final message = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (message == null) return true; + final members = + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); + + final actions = await (select(messageActions) + ..where( + (t) => t.type.equals(action.name) & t.messageId.equals(messageId), + )) + .get(); + + return members.length == actions.length; + } + + // Future updateMessageByOtherUser( + // int userId, + // int messageId, + // MessagesCompanion updatedValues, + // ) { + // return (update(messages) + // ..where( + // (c) => c.contactId.equals(userId) & c.messageId.equals(messageId), + // )) + // .write(updatedValues); + // } + + // Future updateMessageByOtherMessageId( + // int userId, + // int messageOtherId, + // MessagesCompanion updatedValues, + // ) { + // return (update(messages) + // ..where( + // (c) => + // c.contactId.equals(userId) & + // c.messageOtherId.equals(messageOtherId), + // )) + // .write(updatedValues); + // } + + Future updateMessageId( + String messageId, + MessagesCompanion updatedValues, + ) async { + await (update(messages)..where((c) => c.messageId.equals(messageId))) + .write(updatedValues); + } + + Future updateMessagesByMediaId( + String mediaId, + MessagesCompanion updatedValues, + ) { + return (update(messages)..where((c) => c.mediaId.equals(mediaId))) + .write(updatedValues); + } + + Future insertMessage(MessagesCompanion message) async { + try { + var insertMessage = message; + + if (message.messageId == const Value.absent()) { + insertMessage = message.copyWith( + messageId: Value(uuid.v7()), + ); + } + + final rowId = await into(messages).insert(insertMessage); + + await twonlyDB.groupsDao.updateGroup( + message.groupId.value, + GroupsCompanion( + lastMessageExchange: Value(DateTime.now()), + archived: const Value(false), + deletedContent: const Value(false), + ), + ); + + if (message.senderId.present) { + await twonlyDB.groupsDao.updateMember( + message.groupId.value, + message.senderId.value!, + GroupMembersCompanion( + lastMessage: Value(DateTime.now()), + ), + ); + } + + return await (select(messages)..where((t) => t.rowId.equals(rowId))) + .getSingle(); + } catch (e) { + Log.error('Could not insert message: $e'); + return null; + } + } + + Future getLastMessageAction(String messageId) async { + return (((select(messageActions) + ..where( + (t) => t.messageId.equals(messageId), + )) + ..orderBy([(t) => OrderingTerm.desc(t.actionAt)])) + ..limit(1)) + .getSingleOrNull(); + } + + Stream>> watchLastOpenedMessagePerContact( + String groupId, + ) { + const sql = ''' + SELECT m.*, c.* + FROM ( + SELECT ma.contact_id, ma.message_id, + ROW_NUMBER() OVER (PARTITION BY ma.contact_id + ORDER BY ma.action_at DESC, ma.message_id DESC) AS rn + FROM message_actions ma + WHERE ma.type = 'openedAt' + ) last_open + JOIN messages m ON m.message_id = last_open.message_id + JOIN contacts c ON c.user_id = last_open.contact_id + WHERE last_open.rn = 1 AND m.group_id = ?; + '''; + + return customSelect( + sql, + variables: [Variable.withString(groupId)], + readsFrom: {messages, messageActions, contacts}, + ).watch().map((rows) async { + final res = <(Message, Contact)>[]; + for (final row in rows) { + final message = await messages.mapFromRow(row); + final contact = await contacts.mapFromRow(row); + res.add((message, contact)); + } + return res; + }); + } + + // Future deleteMessagesByContactId(int contactId) { + // return (delete(messages) + // ..where( + // (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false), + // )) + // .go(); + // } + + // Future deleteMessagesByContactIdAndOtherMessageId( + // int contactId, + // int messageOtherId, + // ) { + // return (delete(messages) + // ..where( + // (t) => + // t.contactId.equals(contactId) & + // t.messageOtherId.equals(messageOtherId), + // )) + // .go(); + // } + + Future deleteMessagesById(String messageId) { + return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); + } + + Future deleteMessagesByGroupId(String groupId) { + return (delete(messages)..where((t) => t.groupId.equals(groupId))).go(); + } + + // Future deleteAllMessagesByContactId(int contactId) { + // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); + // } + + // Future containsOtherMessageId( + // int fromUserId, + // int messageOtherId, + // ) async { + // final query = select(messages) + // ..where( + // (t) => + // t.messageOtherId.equals(messageOtherId) & + // t.contactId.equals(fromUserId), + // ); + // final entry = await query.get(); + // return entry.isNotEmpty; + // } + + SingleOrNullSelectable getMessageById(String messageId) { + return select(messages)..where((t) => t.messageId.equals(messageId)); + } + + Future> getMessagesByMediaId(String mediaId) async { + return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get(); + } + + Stream> watchMessageActions(String messageId) { + final query = (select(messageActions).join([ + leftOuterJoin( + contacts, + contacts.userId.equalsExp(messageActions.contactId), + ), + ]) + ..where(messageActions.messageId.equals(messageId))); + return query + .map((row) => (row.readTable(messageActions), row.readTable(contacts))) + .watch(); + } + + Stream> watchMessageHistory(String messageId) { + return (select(messageHistories) + ..where((t) => t.messageId.equals(messageId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + + // Future> getMessagesByMediaUploadId(int mediaUploadId) async { + // return (select(messages) + // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) + // .get(); + // } + + // SingleOrNullSelectable getMessageByOtherMessageId( + // int fromUserId, + // int messageId, + // ) { + // return select(messages) + // ..where( + // (t) => + // t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId), + // ); + // } + + // SingleOrNullSelectable getMessageByIdAndContactId( + // int fromUserId, + // int messageId, + // ) { + // return select(messages) + // ..where( + // (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId), + // ); + // } +} diff --git a/lib/src/database/daos/messages.dao.g.dart b/lib/src/database/daos/messages.dao.g.dart new file mode 100644 index 0000000..feb9283 --- /dev/null +++ b/lib/src/database/daos/messages.dao.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'messages.dao.dart'; + +// ignore_for_file: type=lint +mixin _$MessagesDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; + $ContactsTable get contacts => attachedDatabase.contacts; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $ReactionsTable get reactions => attachedDatabase.reactions; + $MessageHistoriesTable get messageHistories => + attachedDatabase.messageHistories; + $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; + $MessageActionsTable get messageActions => attachedDatabase.messageActions; +} diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart deleted file mode 100644 index 7bb05e3..0000000 --- a/lib/src/database/daos/messages_dao.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/log.dart'; - -part 'messages_dao.g.dart'; - -@DriftAccessor(tables: [Messages, Contacts]) -class MessagesDao extends DatabaseAccessor - with _$MessagesDaoMixin { - // this constructor is required so that the main database can create an instance - // of this object. - // ignore: matching_super_parameters - MessagesDao(super.db); - - Stream> watchMessageNotOpened(int contactId) { - return (select(messages) - ..where( - (t) => - t.openedAt.isNull() & - t.contactId.equals(contactId) & - t.errorWhileSending.equals(false), - ) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - .watch(); - } - - Stream> watchMediaMessageNotOpened(int contactId) { - return (select(messages) - ..where( - (t) => - t.openedAt.isNull() & - t.contactId.equals(contactId) & - t.errorWhileSending.equals(false) & - t.messageOtherId.isNotNull() & - t.kind.equals(MessageKind.media.name), - ) - ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - .watch(); - } - - Stream> watchLastMessage(int contactId) { - return (select(messages) - ..where((t) => t.contactId.equals(contactId)) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) - ..limit(1)) - .watch(); - } - - Stream> watchAllMessagesFrom(int contactId) { - return (select(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.contentJson.isNotNull() & - (t.openedAt.isNull() | - t.mediaStored.equals(true) | - t.openedAt.isBiggerThanValue( - DateTime.now().subtract(const Duration(days: 1)), - )), - ) - ..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) - .watch(); - } - - Future removeOldMessages() { - return (update(messages) - ..where( - (t) => - (t.openedAt.isSmallerThanValue( - DateTime.now().subtract(const Duration(days: 1)), - ) | - (t.sendAt.isSmallerThanValue( - DateTime.now().subtract(const Duration(days: 3)), - ) & - t.errorWhileSending.equals(true))) & - t.kind.equals(MessageKind.textMessage.name), - )) - .write(const MessagesCompanion(contentJson: Value(null))); - } - - Future handleMediaFilesOlderThan30Days() { - /// media files will be deleted by the server after 30 days, so delete them here also - return (update(messages) - ..where( - (t) => (t.kind.equals(MessageKind.media.name) & - t.openedAt.isNull() & - t.messageOtherId.isNull() & - (t.sendAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 30), - ), - ))), - )) - .write(const MessagesCompanion(errorWhileSending: Value(true))); - } - - Future> getAllMessagesPendingDownloading() { - return (select(messages) - ..where( - (t) => - t.downloadState.equals(DownloadState.downloaded.index).not() & - t.messageOtherId.isNotNull() & - t.errorWhileSending.equals(false) & - t.kind.equals(MessageKind.media.name), - )) - .get(); - } - - Future> getAllNonACKMessagesFromUser() { - return (select(messages) - ..where( - (t) => - t.acknowledgeByUser.equals(false) & - t.messageOtherId.isNull() & - t.errorWhileSending.equals(false) & - t.sendAt.isBiggerThanValue( - DateTime.now().subtract(const Duration(minutes: 10)), - ), - )) - .get(); - } - - Stream> getAllStoredMediaFiles() { - return (select(messages) - ..where((t) => t.mediaStored.equals(true)) - ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - .watch(); - } - - Future> getAllMessagesPendingUpload() { - return (select(messages) - ..where( - (t) => - t.acknowledgeByServer.equals(false) & - t.messageOtherId.isNull() & - t.mediaUploadId.isNotNull() & - t.downloadState.equals(DownloadState.pending.index) & - t.errorWhileSending.equals(false) & - t.kind.equals(MessageKind.media.name), - )) - .get(); - } - - Future openedAllNonMediaMessages(int contactId) { - final updates = MessagesCompanion(openedAt: Value(DateTime.now())); - return (update(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId.isNotNull() & - t.openedAt.isNull() & - t.kind.equals(MessageKind.media.name).not(), - )) - .write(updates); - } - - Future resetPendingDownloadState() { - // All media files in the downloading state are reset to the pending state - // When the app is used in mobile network, they will not be downloaded at the start - // if they are not yet downloaded... - const updates = - MessagesCompanion(downloadState: Value(DownloadState.pending)); - return (update(messages) - ..where( - (t) => - t.messageOtherId.isNotNull() & - t.downloadState.equals(DownloadState.downloading.index) & - t.kind.equals(MessageKind.media.name), - )) - .write(updates); - } - - Future openedAllNonMediaMessagesFromOtherUser(int contactId) { - final updates = MessagesCompanion(openedAt: Value(DateTime.now())); - return (update(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId - .isNull() & // only mark messages open that where send - t.openedAt.isNull() & - t.kind.equals(MessageKind.media.name).not(), - )) - .write(updates); - } - - Future updateMessageByOtherUser( - int userId, - int messageId, - MessagesCompanion updatedValues, - ) { - return (update(messages) - ..where( - (c) => c.contactId.equals(userId) & c.messageId.equals(messageId), - )) - .write(updatedValues); - } - - Future updateMessageByOtherMessageId( - int userId, - int messageOtherId, - MessagesCompanion updatedValues, - ) { - return (update(messages) - ..where( - (c) => - c.contactId.equals(userId) & - c.messageOtherId.equals(messageOtherId), - )) - .write(updatedValues); - } - - Future updateMessageByMessageId( - int messageId, - MessagesCompanion updatedValues, - ) { - return (update(messages)..where((c) => c.messageId.equals(messageId))) - .write(updatedValues); - } - - Future insertMessage(MessagesCompanion message) async { - try { - await (update(contacts) - ..where( - (c) => c.userId.equals(message.contactId.value), - )) - .write(ContactsCompanion(lastMessageExchange: Value(DateTime.now()))); - - return await into(messages).insert(message); - } catch (e) { - Log.error('Error while inserting message: $e'); - return null; - } - } - - Future deleteMessagesByContactId(int contactId) { - return (delete(messages) - ..where( - (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false), - )) - .go(); - } - - Future deleteMessagesByContactIdAndOtherMessageId( - int contactId, - int messageOtherId, - ) { - return (delete(messages) - ..where( - (t) => - t.contactId.equals(contactId) & - t.messageOtherId.equals(messageOtherId), - )) - .go(); - } - - Future deleteMessagesByMessageId(int messageId) { - return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); - } - - Future deleteAllMessagesByContactId(int contactId) { - return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); - } - - Future containsOtherMessageId( - int fromUserId, - int messageOtherId, - ) async { - final query = select(messages) - ..where( - (t) => - t.messageOtherId.equals(messageOtherId) & - t.contactId.equals(fromUserId), - ); - final entry = await query.get(); - return entry.isNotEmpty; - } - - SingleOrNullSelectable getMessageByMessageId(int messageId) { - return select(messages)..where((t) => t.messageId.equals(messageId)); - } - - Future> getMessagesByMediaUploadId(int mediaUploadId) async { - return (select(messages) - ..where((t) => t.mediaUploadId.equals(mediaUploadId))) - .get(); - } - - SingleOrNullSelectable getMessageByOtherMessageId( - int fromUserId, - int messageId, - ) { - return select(messages) - ..where( - (t) => - t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId), - ); - } - - SingleOrNullSelectable getMessageByIdAndContactId( - int fromUserId, - int messageId, - ) { - return select(messages) - ..where( - (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId), - ); - } -} diff --git a/lib/src/database/daos/messages_dao.g.dart b/lib/src/database/daos/messages_dao.g.dart deleted file mode 100644 index 1967aec..0000000 --- a/lib/src/database/daos/messages_dao.g.dart +++ /dev/null @@ -1,9 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'messages_dao.dart'; - -// ignore_for_file: type=lint -mixin _$MessagesDaoMixin on DatabaseAccessor { - $ContactsTable get contacts => attachedDatabase.contacts; - $MessagesTable get messages => attachedDatabase.messages; -} diff --git a/lib/src/database/daos/reactions.dao.dart b/lib/src/database/daos/reactions.dao.dart new file mode 100644 index 0000000..59ef34a --- /dev/null +++ b/lib/src/database/daos/reactions.dao.dart @@ -0,0 +1,129 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/reactions.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +part 'reactions.dao.g.dart'; + +@DriftAccessor(tables: [Reactions, Contacts]) +class ReactionsDao extends DatabaseAccessor with _$ReactionsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + ReactionsDao(super.db); + + Future updateReaction( + int contactId, + String messageId, + String groupId, + String emoji, + bool remove, + ) async { + if (!isEmoji(emoji)) { + Log.error('Did not update reaction as it is not an emoji!'); + return; + } + final msg = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (msg == null || msg.groupId != groupId) return; + + try { + if (remove) { + await (delete(reactions) + ..where( + (t) => + t.senderId.equals(contactId) & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) + .go(); + } else { + await into(reactions).insertOnConflictUpdate( + ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: Value(contactId), + ), + ); + } + } catch (e) { + Log.error(e); + } + } + + Future updateMyReaction( + String messageId, + String emoji, + bool remove, + ) async { + if (!isEmoji(emoji)) { + Log.error('Did not update reaction as it is not an emoji!'); + return; + } + final msg = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (msg == null) return; + + try { + await (delete(reactions) + ..where( + (t) => + t.senderId.isNull() & + t.messageId.equals(messageId) & + t.emoji.equals(emoji), + )) + .go(); + if (!remove) { + await into(reactions).insert( + ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: const Value(null), + ), + ); + } + } catch (e) { + Log.error(e); + } + } + + Stream> watchReactions(String messageId) { + return (select(reactions) + ..where((t) => t.messageId.equals(messageId)) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .watch(); + } + + Stream watchLastReactions(String groupId) { + final query = (select(reactions) + ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) + .join( + [ + innerJoin( + messages, + messages.messageId.equalsExp(reactions.messageId), + useColumns: false, + ), + ], + ) + ..where(messages.groupId.equals(groupId)) + // ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) + ..limit(1); + return query.map((row) => row.readTable(reactions)).watchSingleOrNull(); + } + + Stream> watchReactionWithContacts( + String messageId, + ) { + final query = (select(reactions)).join( + [leftOuterJoin(contacts, contacts.userId.equalsExp(reactions.senderId))], + )..where(reactions.messageId.equals(messageId)); + + return query + .map((row) => (row.readTable(reactions), row.readTableOrNull(contacts))) + .watch(); + } +} diff --git a/lib/src/database/daos/reactions.dao.g.dart b/lib/src/database/daos/reactions.dao.g.dart new file mode 100644 index 0000000..26ac0da --- /dev/null +++ b/lib/src/database/daos/reactions.dao.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reactions.dao.dart'; + +// ignore_for_file: type=lint +mixin _$ReactionsDaoMixin on DatabaseAccessor { + $GroupsTable get groups => attachedDatabase.groups; + $ContactsTable get contacts => attachedDatabase.contacts; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $ReactionsTable get reactions => attachedDatabase.reactions; +} diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart new file mode 100644 index 0000000..8b44397 --- /dev/null +++ b/lib/src/database/daos/receipts.dao.dart @@ -0,0 +1,114 @@ +import 'package:drift/drift.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'receipts.dao.g.dart'; + +@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts]) +class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { + // this constructor is required so that the main database can create an instance + // of this object. + // ignore: matching_super_parameters + ReceiptsDao(super.db); + + Future confirmReceipt(String receiptId, int fromUserId) async { + final receipt = await (select(receipts) + ..where( + (t) => + t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId), + )) + .getSingleOrNull(); + + if (receipt == null) return; + + if (receipt.messageId != null) { + await into(messageActions).insert( + MessageActionsCompanion( + messageId: Value(receipt.messageId!), + contactId: Value(fromUserId), + type: const Value(MessageActionType.ackByUserAt), + ), + ); + } + + await (delete(receipts) + ..where( + (t) => + t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId), + )) + .go(); + } + + Future deleteReceipt(String receiptId) async { + await (delete(receipts) + ..where( + (t) => t.receiptId.equals(receiptId), + )) + .go(); + } + + Future insertReceipt(ReceiptsCompanion entry) async { + try { + var insertEntry = entry; + if (entry.receiptId == const Value.absent()) { + insertEntry = entry.copyWith( + receiptId: Value(uuid.v4()), + ); + } + final id = await into(receipts).insert(insertEntry); + return await (select(receipts)..where((t) => t.rowId.equals(id))) + .getSingle(); + } catch (e) { + Log.error(e); + return null; + } + } + + Future getReceiptById(String receiptId) async { + try { + return await (select(receipts) + ..where( + (t) => t.receiptId.equals(receiptId), + )) + .getSingleOrNull(); + } catch (e) { + Log.error(e); + return null; + } + } + + Future> getReceiptsNotAckByServer() async { + return (select(receipts) + ..where( + (t) => t.ackByServerAt.isNull(), + )) + .get(); + } + + Stream> watchAll() { + return select(receipts).watch(); + } + + Future updateReceipt( + String receiptId, + ReceiptsCompanion updates, + ) async { + await (update(receipts)..where((c) => c.receiptId.equals(receiptId))) + .write(updates); + } + + Future isDuplicated(String receiptId) async { + return await (select(receivedReceipts) + ..where((t) => t.receiptId.equals(receiptId))) + .getSingleOrNull() != + null; + } + + Future gotReceipt(String receiptId) async { + await into(receivedReceipts) + .insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId))); + } +} diff --git a/lib/src/database/daos/receipts.dao.g.dart b/lib/src/database/daos/receipts.dao.g.dart new file mode 100644 index 0000000..4230aa8 --- /dev/null +++ b/lib/src/database/daos/receipts.dao.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'receipts.dao.dart'; + +// ignore_for_file: type=lint +mixin _$ReceiptsDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; + $GroupsTable get groups => attachedDatabase.groups; + $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $MessagesTable get messages => attachedDatabase.messages; + $ReceiptsTable get receipts => attachedDatabase.receipts; + $MessageActionsTable get messageActions => attachedDatabase.messageActions; + $ReceivedReceiptsTable get receivedReceipts => + attachedDatabase.receivedReceipts; +} diff --git a/lib/src/database/daos/signal_dao.dart b/lib/src/database/daos/signal.dao.dart similarity index 92% rename from lib/src/database/daos/signal_dao.dart rename to lib/src/database/daos/signal.dao.dart index e8f4732..2b64aac 100644 --- a/lib/src/database/daos/signal_dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -1,11 +1,11 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; -part 'signal_dao.g.dart'; +part 'signal.dao.g.dart'; @DriftAccessor( tables: [ @@ -13,7 +13,7 @@ part 'signal_dao.g.dart'; SignalContactSignedPreKeys, ], ) -class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { +class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { // this constructor is required so that the main database can create an instance // of this object. // ignore: matching_super_parameters @@ -57,7 +57,7 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { tbl.preKeyId.equals(preKey.preKeyId), )) .go(); - Log.info('Using prekey ${preKey.preKeyId} for $contactId'); + Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId'); return preKey; } return null; @@ -68,6 +68,7 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { List preKeys, ) async { for (final preKey in preKeys) { + Log.info('[PREKEY] Inserting others ${preKey.preKeyId}'); try { await into(signalContactPreKeys).insert(preKey); } catch (e) { diff --git a/lib/src/database/daos/signal_dao.g.dart b/lib/src/database/daos/signal.dao.g.dart similarity index 67% rename from lib/src/database/daos/signal_dao.g.dart rename to lib/src/database/daos/signal.dao.g.dart index a3b77d6..a9eea13 100644 --- a/lib/src/database/daos/signal_dao.g.dart +++ b/lib/src/database/daos/signal.dao.g.dart @@ -1,9 +1,10 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'signal_dao.dart'; +part of 'signal.dao.dart'; // ignore_for_file: type=lint -mixin _$SignalDaoMixin on DatabaseAccessor { +mixin _$SignalDaoMixin on DatabaseAccessor { + $ContactsTable get contacts => attachedDatabase.contacts; $SignalContactPreKeysTable get signalContactPreKeys => attachedDatabase.signalContactPreKeys; $SignalContactSignedPreKeysTable get signalContactSignedPreKeys => diff --git a/lib/src/database/signal/connect_identity_key_store.dart b/lib/src/database/signal/connect_identity_key_store.dart index 47d4d88..2b36b13 100644 --- a/lib/src/database/signal/connect_identity_key_store.dart +++ b/lib/src/database/signal/connect_identity_key_store.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectIdentityKeyStore extends IdentityKeyStore { ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId); diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 6fb193b..60018c5 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; class ConnectPreKeyStore extends PreKeyStore { @@ -19,15 +19,18 @@ class ConnectPreKeyStore extends PreKeyStore { ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .get(); if (preKeyRecord.isEmpty) { - throw InvalidKeyIdException('No such preKey record! - $preKeyId'); + throw InvalidKeyIdException( + '[PREKEY] No such preKey record! - $preKeyId', + ); } - Log.info('Contact used preKey $preKeyId'); + Log.info('[PREKEY] Contact used my preKey $preKeyId'); final preKey = preKeyRecord.first.preKey; return PreKeyRecord.fromBuffer(preKey); } @override Future removePreKey(int preKeyId) async { + Log.info('[PREKEY] Removing $preKeyId from my own storage.'); await (twonlyDB.delete(twonlyDB.signalPreKeyStores) ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .go(); @@ -40,6 +43,7 @@ class ConnectPreKeyStore extends PreKeyStore { preKey: Value(record.serialize()), ); + Log.info('[PREKEY] Storing $preKeyId from my own storage.'); try { await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); } catch (e) { diff --git a/lib/src/database/signal/connect_sender_key_store.dart b/lib/src/database/signal/connect_sender_key_store.dart index e9b0ee2..69b25c7 100644 --- a/lib/src/database/signal/connect_sender_key_store.dart +++ b/lib/src/database/signal/connect_sender_key_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectSenderKeyStore extends SenderKeyStore { @override diff --git a/lib/src/database/signal/connect_session_store.dart b/lib/src/database/signal/connect_session_store.dart index d851600..bb738c4 100644 --- a/lib/src/database/signal/connect_session_store.dart +++ b/lib/src/database/signal/connect_session_store.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class ConnectSessionStore extends SessionStore { @override diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart new file mode 100644 index 0000000..a427fad --- /dev/null +++ b/lib/src/database/tables/contacts.table.dart @@ -0,0 +1,27 @@ +import 'package:drift/drift.dart'; + +class Contacts extends Table { + IntColumn get userId => integer()(); + + TextColumn get username => text()(); + TextColumn get displayName => text().nullable()(); + TextColumn get nickName => text().nullable()(); + BlobColumn get avatarSvgCompressed => blob().nullable()(); + + IntColumn get senderProfileCounter => + integer().withDefault(const Constant(0))(); + + BoolColumn get accepted => boolean().withDefault(const Constant(false))(); + BoolColumn get deletedByUser => + boolean().withDefault(const Constant(false))(); + BoolColumn get requested => boolean().withDefault(const Constant(false))(); + BoolColumn get blocked => boolean().withDefault(const Constant(false))(); + BoolColumn get verified => boolean().withDefault(const Constant(false))(); + BoolColumn get accountDeleted => + boolean().withDefault(const Constant(false))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {userId}; +} diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart new file mode 100644 index 0000000..32bbc1c --- /dev/null +++ b/lib/src/database/tables/groups.table.dart @@ -0,0 +1,113 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +const int defaultDeleteMessagesAfterMilliseconds = 1000 * 60 * 60 * 24; + +@DataClassName('Group') +class Groups extends Table { + TextColumn get groupId => text()(); + + BoolColumn get isGroupAdmin => boolean().withDefault(const Constant(false))(); + BoolColumn get isDirectChat => boolean().withDefault(const Constant(false))(); + BoolColumn get pinned => boolean().withDefault(const Constant(false))(); + BoolColumn get archived => boolean().withDefault(const Constant(false))(); + + BoolColumn get joinedGroup => boolean().withDefault(const Constant(false))(); + BoolColumn get leftGroup => boolean().withDefault(const Constant(false))(); + BoolColumn get deletedContent => + boolean().withDefault(const Constant(false))(); + + IntColumn get stateVersionId => integer().withDefault(const Constant(0))(); + + BlobColumn get stateEncryptionKey => blob().nullable()(); + BlobColumn get myGroupPrivateKey => blob().nullable()(); + + TextColumn get groupName => text()(); + + IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))(); + + BoolColumn get alsoBestFriend => + boolean().withDefault(const Constant(false))(); + + IntColumn get deleteMessagesAfterMilliseconds => integer() + .withDefault(const Constant(defaultDeleteMessagesAfterMilliseconds))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get lastMessageSend => dateTime().nullable()(); + DateTimeColumn get lastMessageReceived => dateTime().nullable()(); + DateTimeColumn get lastFlameCounterChange => dateTime().nullable()(); + DateTimeColumn get lastFlameSync => dateTime().nullable()(); + + IntColumn get flameCounter => integer().withDefault(const Constant(0))(); + + IntColumn get maxFlameCounter => integer().withDefault(const Constant(0))(); + DateTimeColumn get maxFlameCounterFrom => dateTime().nullable()(); + + DateTimeColumn get lastMessageExchange => + dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupId}; +} + +enum MemberState { normal, admin, leftGroup } + +@DataClassName('GroupMember') +class GroupMembers extends Table { + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); + + IntColumn get contactId => integer().references(Contacts, #userId)(); + TextColumn get memberState => textEnum().nullable()(); + BlobColumn get groupPublicKey => blob().nullable()(); + + DateTimeColumn get lastMessage => dateTime().nullable()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupId, contactId}; +} + +enum GroupActionType { + createdGroup, + removedMember, + addMember, + leftGroup, + promoteToAdmin, + demoteToMember, + updatedGroupName, + changeDisplayMaxTime, +} + +@DataClassName('GroupHistory') +class GroupHistories extends Table { + TextColumn get groupHistoryId => text()(); + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); + + IntColumn get contactId => + integer().nullable().references(Contacts, #userId)(); + + IntColumn get affectedContactId => + integer().nullable().references(Contacts, #userId)(); + + TextColumn get oldGroupName => text().nullable()(); + TextColumn get newGroupName => text().nullable()(); + + IntColumn get newDeleteMessagesAfterMilliseconds => integer().nullable()(); + + TextColumn get type => textEnum()(); + + DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {groupHistoryId}; +} + +GroupActionType? groupActionTypeFromString(String name) { + for (final v in GroupActionType.values) { + if (v.name == name) return v; + } + return null; +} diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart new file mode 100644 index 0000000..af129d8 --- /dev/null +++ b/lib/src/database/tables/mediafiles.table.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; +import 'package:drift/drift.dart'; + +enum MediaType { + image, + video, + gif, + audio, +} + +enum UploadState { + // Image/Video was taken. A database entry was created to track it... + initialized, + // Image was stored but not send + storedOnly, + // At this point the user is finished with editing, and the media file can be uploaded + preprocessing, + uploading, + backgroundUploadTaskStarted, + uploaded, + + uploadLimitReached, + // readyToUpload, + // uploadTaskStarted, + // receiverNotified, +} + +enum DownloadState { + pending, + downloading, + downloaded, + ready, + reuploadRequested +} + +@DataClassName('MediaFile') +class MediaFiles extends Table { + TextColumn get mediaId => text()(); + + TextColumn get type => textEnum()(); + + TextColumn get uploadState => textEnum().nullable()(); + TextColumn get downloadState => textEnum().nullable()(); + + BoolColumn get requiresAuthentication => + boolean().withDefault(const Constant(false))(); + + BoolColumn get reopenByContact => + boolean().withDefault(const Constant(false))(); + + BoolColumn get stored => boolean().withDefault(const Constant(false))(); + BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); + + TextColumn get reuploadRequestedBy => + text().map(IntListTypeConverter()).nullable()(); + + IntColumn get displayLimitInMilliseconds => integer().nullable()(); + BoolColumn get removeAudio => boolean().nullable()(); + + BlobColumn get downloadToken => blob().nullable()(); + BlobColumn get encryptionKey => blob().nullable()(); + BlobColumn get encryptionMac => blob().nullable()(); + BlobColumn get encryptionNonce => blob().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {mediaId}; +} + +class IntListTypeConverter extends TypeConverter, String> { + @override + List fromSql(String fromDb) { + return List.from(jsonDecode(fromDb) as Iterable); + } + + @override + String toSql(List value) { + return json.encode(value); + } +} diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart new file mode 100644 index 0000000..7d0f20c --- /dev/null +++ b/lib/src/database/tables/messages.table.dart @@ -0,0 +1,84 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; + +enum MessageType { media, text } + +@DataClassName('Message') +class Messages extends Table { + TextColumn get groupId => + text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); + TextColumn get messageId => text()(); + + // in case senderId is null, it was send by user itself + IntColumn get senderId => + integer().nullable().references(Contacts, #userId)(); + + TextColumn get type => textEnum()(); + + TextColumn get content => text().nullable()(); + TextColumn get mediaId => text() + .nullable() + .references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)(); + + BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); + + BlobColumn get downloadToken => blob().nullable()(); + + TextColumn get quotesMessageId => text().nullable()(); + + BoolColumn get isDeletedFromSender => + boolean().withDefault(const Constant(false))(); + + DateTimeColumn get openedAt => dateTime().nullable()(); + DateTimeColumn get openedByAll => dateTime().nullable()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get modifiedAt => dateTime().nullable()(); + DateTimeColumn get ackByUser => dateTime().nullable()(); + DateTimeColumn get ackByServer => dateTime().nullable()(); + + @override + Set get primaryKey => {messageId}; +} + +enum MessageActionType { + openedAt, + ackByUserAt, + ackByServerAt, +} + +@DataClassName('MessageAction') +class MessageActions extends Table { + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + IntColumn get contactId => + integer().references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + + TextColumn get type => textEnum()(); + + DateTimeColumn get actionAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId, contactId, type}; +} + +@DataClassName('MessageHistory') +class MessageHistories extends Table { + IntColumn get id => integer().autoIncrement()(); + + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + IntColumn get contactId => integer() + .nullable() + .references(Contacts, #contactId, onDelete: KeyAction.cascade)(); + + TextColumn get content => text().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {id}; +} diff --git a/lib/src/database/tables/reactions.table.dart b/lib/src/database/tables/reactions.table.dart new file mode 100644 index 0000000..5c7a671 --- /dev/null +++ b/lib/src/database/tables/reactions.table.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; + +@DataClassName('Reaction') +class Reactions extends Table { + TextColumn get messageId => + text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + TextColumn get emoji => text()(); + + // in case senderId is null, it was send by user itself + IntColumn get senderId => integer() + .nullable() + .references(Contacts, #userId, onDelete: KeyAction.cascade)(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId, senderId, emoji}; +} diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart new file mode 100644 index 0000000..77a76e4 --- /dev/null +++ b/lib/src/database/tables/receipts.table.dart @@ -0,0 +1,42 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; + +@DataClassName('Receipt') +class Receipts extends Table { + TextColumn get receiptId => text()(); + + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + + // in case a message is deleted, it should be also deleted from the receipts table + TextColumn get messageId => text() + .nullable() + .references(Messages, #messageId, onDelete: KeyAction.cascade)(); + + /// This is the protobuf 'Message' + BlobColumn get message => blob()(); + + BoolColumn get contactWillSendsReceipt => + boolean().withDefault(const Constant(true))(); + + DateTimeColumn get ackByServerAt => dateTime().nullable()(); + + IntColumn get retryCount => integer().withDefault(const Constant(0))(); + DateTimeColumn get lastRetry => dateTime().nullable()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {receiptId}; +} + +@DataClassName('ReceivedReceipt') +class ReceivedReceipts extends Table { + TextColumn get receiptId => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {receiptId}; +} diff --git a/lib/src/database/tables/signal_contact_prekey.table.dart b/lib/src/database/tables/signal_contact_prekey.table.dart new file mode 100644 index 0000000..d14f522 --- /dev/null +++ b/lib/src/database/tables/signal_contact_prekey.table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +@DataClassName('SignalContactPreKey') +class SignalContactPreKeys extends Table { + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + IntColumn get preKeyId => integer()(); + BlobColumn get preKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + @override + Set get primaryKey => {contactId, preKeyId}; +} diff --git a/lib/src/database/tables/signal_contact_signed_prekey.table.dart b/lib/src/database/tables/signal_contact_signed_prekey.table.dart new file mode 100644 index 0000000..5888abe --- /dev/null +++ b/lib/src/database/tables/signal_contact_signed_prekey.table.dart @@ -0,0 +1,14 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; + +@DataClassName('SignalContactSignedPreKey') +class SignalContactSignedPreKeys extends Table { + IntColumn get contactId => + integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); + IntColumn get signedPreKeyId => integer()(); + BlobColumn get signedPreKey => blob()(); + BlobColumn get signedPreKeySignature => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + @override + Set get primaryKey => {contactId}; +} diff --git a/lib/src/database/tables/signal_identity_key_store_table.dart b/lib/src/database/tables/signal_identity_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_identity_key_store_table.dart rename to lib/src/database/tables/signal_identity_key_store.table.dart diff --git a/lib/src/database/tables/signal_pre_key_store_table.dart b/lib/src/database/tables/signal_pre_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_pre_key_store_table.dart rename to lib/src/database/tables/signal_pre_key_store.table.dart diff --git a/lib/src/database/tables/signal_sender_key_store_table.dart b/lib/src/database/tables/signal_sender_key_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_sender_key_store_table.dart rename to lib/src/database/tables/signal_sender_key_store.table.dart diff --git a/lib/src/database/tables/signal_session_store_table.dart b/lib/src/database/tables/signal_session_store.table.dart similarity index 100% rename from lib/src/database/tables/signal_session_store_table.dart rename to lib/src/database/tables/signal_session_store.table.dart diff --git a/lib/src/database/tables/contacts_table.dart b/lib/src/database/tables_old/contacts_table.dart similarity index 100% rename from lib/src/database/tables/contacts_table.dart rename to lib/src/database/tables_old/contacts_table.dart diff --git a/lib/src/database/tables/media_uploads_table.dart b/lib/src/database/tables_old/media_uploads_table.dart similarity index 100% rename from lib/src/database/tables/media_uploads_table.dart rename to lib/src/database/tables_old/media_uploads_table.dart diff --git a/lib/src/database/tables/message_retransmissions.dart b/lib/src/database/tables_old/message_retransmissions.dart similarity index 85% rename from lib/src/database/tables/message_retransmissions.dart rename to lib/src/database/tables_old/message_retransmissions.dart index a746f98..113d775 100644 --- a/lib/src/database/tables/message_retransmissions.dart +++ b/lib/src/database/tables_old/message_retransmissions.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/messages_table.dart'; @DataClassName('MessageRetransmission') class MessageRetransmissions extends Table { diff --git a/lib/src/database/tables/messages_table.dart b/lib/src/database/tables_old/messages_table.dart similarity index 96% rename from lib/src/database/tables/messages_table.dart rename to lib/src/database/tables_old/messages_table.dart index 6a2af91..547857e 100644 --- a/lib/src/database/tables/messages_table.dart +++ b/lib/src/database/tables_old/messages_table.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; enum MessageKind { textMessage, diff --git a/lib/src/database/tables/signal_contact_prekey_table.dart b/lib/src/database/tables_old/signal_contact_prekey_table.dart similarity index 100% rename from lib/src/database/tables/signal_contact_prekey_table.dart rename to lib/src/database/tables_old/signal_contact_prekey_table.dart diff --git a/lib/src/database/tables/signal_contact_signed_prekey_table.dart b/lib/src/database/tables_old/signal_contact_signed_prekey_table.dart similarity index 100% rename from lib/src/database/tables/signal_contact_signed_prekey_table.dart rename to lib/src/database/tables_old/signal_contact_signed_prekey_table.dart diff --git a/lib/src/database/tables_old/signal_identity_key_store_table.dart b/lib/src/database/tables_old/signal_identity_key_store_table.dart new file mode 100644 index 0000000..1f7d380 --- /dev/null +++ b/lib/src/database/tables_old/signal_identity_key_store_table.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalIdentityKeyStore') +class SignalIdentityKeyStores extends Table { + IntColumn get deviceId => integer()(); + TextColumn get name => text()(); + BlobColumn get identityKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {deviceId, name}; +} diff --git a/lib/src/database/tables_old/signal_pre_key_store_table.dart b/lib/src/database/tables_old/signal_pre_key_store_table.dart new file mode 100644 index 0000000..eb74263 --- /dev/null +++ b/lib/src/database/tables_old/signal_pre_key_store_table.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalPreKeyStore') +class SignalPreKeyStores extends Table { + IntColumn get preKeyId => integer()(); + BlobColumn get preKey => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {preKeyId}; +} diff --git a/lib/src/database/tables_old/signal_sender_key_store_table.dart b/lib/src/database/tables_old/signal_sender_key_store_table.dart new file mode 100644 index 0000000..1c10183 --- /dev/null +++ b/lib/src/database/tables_old/signal_sender_key_store_table.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalSenderKeyStore') +class SignalSenderKeyStores extends Table { + TextColumn get senderKeyName => text()(); + BlobColumn get senderKey => blob()(); + + @override + Set get primaryKey => {senderKeyName}; +} diff --git a/lib/src/database/tables_old/signal_session_store_table.dart b/lib/src/database/tables_old/signal_session_store_table.dart new file mode 100644 index 0000000..e522700 --- /dev/null +++ b/lib/src/database/tables_old/signal_session_store_table.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; + +@DataClassName('SignalSessionStore') +class SignalSessionStores extends Table { + IntColumn get deviceId => integer()(); + TextColumn get name => text()(); + BlobColumn get sessionRecord => blob()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {deviceId, name}; +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart new file mode 100644 index 0000000..aa8390e --- /dev/null +++ b/lib/src/database/twonly.db.dart @@ -0,0 +1,129 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart' + show DriftNativeOptions, driftDatabase; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/daos/mediafiles.dao.dart'; +import 'package:twonly/src/database/daos/messages.dao.dart'; +import 'package:twonly/src/database/daos/reactions.dao.dart'; +import 'package:twonly/src/database/daos/receipts.dao.dart'; +import 'package:twonly/src/database/daos/signal.dao.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/tables/groups.table.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/reactions.table.dart'; +import 'package:twonly/src/database/tables/receipts.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart'; +import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart'; +import 'package:twonly/src/database/tables/signal_session_store.table.dart'; +import 'package:twonly/src/utils/log.dart'; + +part 'twonly.db.g.dart'; + +// You can then create a database class that includes this table +@DriftDatabase( + tables: [ + Contacts, + Messages, + MessageHistories, + MediaFiles, + Reactions, + Groups, + GroupMembers, + Receipts, + ReceivedReceipts, + SignalIdentityKeyStores, + SignalPreKeyStores, + SignalSenderKeyStores, + SignalSessionStores, + SignalContactPreKeys, + SignalContactSignedPreKeys, + MessageActions, + GroupHistories, + ], + daos: [ + MessagesDao, + ContactsDao, + SignalDao, + ReceiptsDao, + GroupsDao, + ReactionsDao, + MediaFilesDao, + ], +) +class TwonlyDB extends _$TwonlyDB { + TwonlyDB([QueryExecutor? e]) + : super( + e ?? _openConnection(), + ); + + // ignore: matching_super_parameters + TwonlyDB.forTesting(DatabaseConnection super.connection); + + @override + int get schemaVersion => 1; + + static QueryExecutor _openConnection() { + return driftDatabase( + name: 'twonly', + native: const DriftNativeOptions( + databaseDirectory: getApplicationSupportDirectory, + ), + ); + } + + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + // onUpgrade: stepByStep(), + ); + } + + void markUpdated() { + notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)}); + notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)}); + } + + Future printTableSizes() async { + final result = await customSelect( + 'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name', + ).get(); + + for (final row in result) { + final tableName = row.read('name'); + final tableSize = row.read('size'); + Log.info('Table: $tableName, Size: $tableSize bytes'); + } + } + + Future deleteDataForTwonlySafe() async { + // await delete(messages).go(); + // await delete(messageRetransmissions).go(); + // await delete(mediaUploads).go(); + // await update(contacts).write( + // const ContactsCompanion( + // avatarSvg: Value(null), + // myAvatarCounter: Value(0), + // ), + // ); + // await delete(signalContactPreKeys).go(); + // await delete(signalContactSignedPreKeys).go(); + // await (delete(signalPreKeyStores) + // ..where( + // (t) => (t.createdAt.isSmallerThanValue( + // DateTime.now().subtract( + // const Duration(days: 25), + // ), + // )), + // )) + // .go(); + } +} diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart new file mode 100644 index 0000000..0e7f60b --- /dev/null +++ b/lib/src/database/twonly.db.g.dart @@ -0,0 +1,14033 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'twonly.db.dart'; + +// ignore_for_file: type=lint +class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ContactsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _usernameMeta = + const VerificationMeta('username'); + @override + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _displayNameMeta = + const VerificationMeta('displayName'); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _nickNameMeta = + const VerificationMeta('nickName'); + @override + late final GeneratedColumn nickName = GeneratedColumn( + 'nick_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _avatarSvgCompressedMeta = + const VerificationMeta('avatarSvgCompressed'); + @override + late final GeneratedColumn avatarSvgCompressed = + GeneratedColumn('avatar_svg_compressed', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _senderProfileCounterMeta = + const VerificationMeta('senderProfileCounter'); + @override + late final GeneratedColumn senderProfileCounter = GeneratedColumn( + 'sender_profile_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _acceptedMeta = + const VerificationMeta('accepted'); + @override + late final GeneratedColumn accepted = GeneratedColumn( + 'accepted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deletedByUserMeta = + const VerificationMeta('deletedByUser'); + @override + late final GeneratedColumn deletedByUser = GeneratedColumn( + 'deleted_by_user', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_by_user" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _requestedMeta = + const VerificationMeta('requested'); + @override + late final GeneratedColumn requested = GeneratedColumn( + 'requested', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _blockedMeta = + const VerificationMeta('blocked'); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _verifiedMeta = + const VerificationMeta('verified'); + @override + late final GeneratedColumn verified = GeneratedColumn( + 'verified', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _accountDeletedMeta = + const VerificationMeta('accountDeleted'); + @override + late final GeneratedColumn accountDeleted = GeneratedColumn( + 'account_deleted', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("account_deleted" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + userId, + username, + displayName, + nickName, + avatarSvgCompressed, + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'contacts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } + if (data.containsKey('username')) { + context.handle(_usernameMeta, + username.isAcceptableOrUnknown(data['username']!, _usernameMeta)); + } else if (isInserting) { + context.missing(_usernameMeta); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, _displayNameMeta)); + } + if (data.containsKey('nick_name')) { + context.handle(_nickNameMeta, + nickName.isAcceptableOrUnknown(data['nick_name']!, _nickNameMeta)); + } + if (data.containsKey('avatar_svg_compressed')) { + context.handle( + _avatarSvgCompressedMeta, + avatarSvgCompressed.isAcceptableOrUnknown( + data['avatar_svg_compressed']!, _avatarSvgCompressedMeta)); + } + if (data.containsKey('sender_profile_counter')) { + context.handle( + _senderProfileCounterMeta, + senderProfileCounter.isAcceptableOrUnknown( + data['sender_profile_counter']!, _senderProfileCounterMeta)); + } + if (data.containsKey('accepted')) { + context.handle(_acceptedMeta, + accepted.isAcceptableOrUnknown(data['accepted']!, _acceptedMeta)); + } + if (data.containsKey('deleted_by_user')) { + context.handle( + _deletedByUserMeta, + deletedByUser.isAcceptableOrUnknown( + data['deleted_by_user']!, _deletedByUserMeta)); + } + if (data.containsKey('requested')) { + context.handle(_requestedMeta, + requested.isAcceptableOrUnknown(data['requested']!, _requestedMeta)); + } + if (data.containsKey('blocked')) { + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); + } + if (data.containsKey('verified')) { + context.handle(_verifiedMeta, + verified.isAcceptableOrUnknown(data['verified']!, _verifiedMeta)); + } + if (data.containsKey('account_deleted')) { + context.handle( + _accountDeletedMeta, + accountDeleted.isAcceptableOrUnknown( + data['account_deleted']!, _accountDeletedMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {userId}; + @override + Contact map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Contact( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}user_id'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + displayName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}display_name']), + nickName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}nick_name']), + avatarSvgCompressed: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}avatar_svg_compressed']), + senderProfileCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}sender_profile_counter'])!, + accepted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}accepted'])!, + deletedByUser: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_by_user'])!, + requested: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}requested'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + verified: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}verified'])!, + accountDeleted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}account_deleted'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ContactsTable createAlias(String alias) { + return $ContactsTable(attachedDatabase, alias); + } +} + +class Contact extends DataClass implements Insertable { + final int userId; + final String username; + final String? displayName; + final String? nickName; + final Uint8List? avatarSvgCompressed; + final int senderProfileCounter; + final bool accepted; + final bool deletedByUser; + final bool requested; + final bool blocked; + final bool verified; + final bool accountDeleted; + final DateTime createdAt; + const Contact( + {required this.userId, + required this.username, + this.displayName, + this.nickName, + this.avatarSvgCompressed, + required this.senderProfileCounter, + required this.accepted, + required this.deletedByUser, + required this.requested, + required this.blocked, + required this.verified, + required this.accountDeleted, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['username'] = Variable(username); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || nickName != null) { + map['nick_name'] = Variable(nickName); + } + if (!nullToAbsent || avatarSvgCompressed != null) { + map['avatar_svg_compressed'] = Variable(avatarSvgCompressed); + } + map['sender_profile_counter'] = Variable(senderProfileCounter); + map['accepted'] = Variable(accepted); + map['deleted_by_user'] = Variable(deletedByUser); + map['requested'] = Variable(requested); + map['blocked'] = Variable(blocked); + map['verified'] = Variable(verified); + map['account_deleted'] = Variable(accountDeleted); + map['created_at'] = Variable(createdAt); + return map; + } + + ContactsCompanion toCompanion(bool nullToAbsent) { + return ContactsCompanion( + userId: Value(userId), + username: Value(username), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + nickName: nickName == null && nullToAbsent + ? const Value.absent() + : Value(nickName), + avatarSvgCompressed: avatarSvgCompressed == null && nullToAbsent + ? const Value.absent() + : Value(avatarSvgCompressed), + senderProfileCounter: Value(senderProfileCounter), + accepted: Value(accepted), + deletedByUser: Value(deletedByUser), + requested: Value(requested), + blocked: Value(blocked), + verified: Value(verified), + accountDeleted: Value(accountDeleted), + createdAt: Value(createdAt), + ); + } + + factory Contact.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Contact( + userId: serializer.fromJson(json['userId']), + username: serializer.fromJson(json['username']), + displayName: serializer.fromJson(json['displayName']), + nickName: serializer.fromJson(json['nickName']), + avatarSvgCompressed: + serializer.fromJson(json['avatarSvgCompressed']), + senderProfileCounter: + serializer.fromJson(json['senderProfileCounter']), + accepted: serializer.fromJson(json['accepted']), + deletedByUser: serializer.fromJson(json['deletedByUser']), + requested: serializer.fromJson(json['requested']), + blocked: serializer.fromJson(json['blocked']), + verified: serializer.fromJson(json['verified']), + accountDeleted: serializer.fromJson(json['accountDeleted']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'username': serializer.toJson(username), + 'displayName': serializer.toJson(displayName), + 'nickName': serializer.toJson(nickName), + 'avatarSvgCompressed': serializer.toJson(avatarSvgCompressed), + 'senderProfileCounter': serializer.toJson(senderProfileCounter), + 'accepted': serializer.toJson(accepted), + 'deletedByUser': serializer.toJson(deletedByUser), + 'requested': serializer.toJson(requested), + 'blocked': serializer.toJson(blocked), + 'verified': serializer.toJson(verified), + 'accountDeleted': serializer.toJson(accountDeleted), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Contact copyWith( + {int? userId, + String? username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), + int? senderProfileCounter, + bool? accepted, + bool? deletedByUser, + bool? requested, + bool? blocked, + bool? verified, + bool? accountDeleted, + DateTime? createdAt}) => + Contact( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName.present ? displayName.value : this.displayName, + nickName: nickName.present ? nickName.value : this.nickName, + avatarSvgCompressed: avatarSvgCompressed.present + ? avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + ); + Contact copyWithCompanion(ContactsCompanion data) { + return Contact( + userId: data.userId.present ? data.userId.value : this.userId, + username: data.username.present ? data.username.value : this.username, + displayName: + data.displayName.present ? data.displayName.value : this.displayName, + nickName: data.nickName.present ? data.nickName.value : this.nickName, + avatarSvgCompressed: data.avatarSvgCompressed.present + ? data.avatarSvgCompressed.value + : this.avatarSvgCompressed, + senderProfileCounter: data.senderProfileCounter.present + ? data.senderProfileCounter.value + : this.senderProfileCounter, + accepted: data.accepted.present ? data.accepted.value : this.accepted, + deletedByUser: data.deletedByUser.present + ? data.deletedByUser.value + : this.deletedByUser, + requested: data.requested.present ? data.requested.value : this.requested, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + verified: data.verified.present ? data.verified.value : this.verified, + accountDeleted: data.accountDeleted.present + ? data.accountDeleted.value + : this.accountDeleted, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Contact(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + userId, + username, + displayName, + nickName, + $driftBlobEquality.hash(avatarSvgCompressed), + senderProfileCounter, + accepted, + deletedByUser, + requested, + blocked, + verified, + accountDeleted, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Contact && + other.userId == this.userId && + other.username == this.username && + other.displayName == this.displayName && + other.nickName == this.nickName && + $driftBlobEquality.equals( + other.avatarSvgCompressed, this.avatarSvgCompressed) && + other.senderProfileCounter == this.senderProfileCounter && + other.accepted == this.accepted && + other.deletedByUser == this.deletedByUser && + other.requested == this.requested && + other.blocked == this.blocked && + other.verified == this.verified && + other.accountDeleted == this.accountDeleted && + other.createdAt == this.createdAt); +} + +class ContactsCompanion extends UpdateCompanion { + final Value userId; + final Value username; + final Value displayName; + final Value nickName; + final Value avatarSvgCompressed; + final Value senderProfileCounter; + final Value accepted; + final Value deletedByUser; + final Value requested; + final Value blocked; + final Value verified; + final Value accountDeleted; + final Value createdAt; + const ContactsCompanion({ + this.userId = const Value.absent(), + this.username = const Value.absent(), + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + }); + ContactsCompanion.insert({ + this.userId = const Value.absent(), + required String username, + this.displayName = const Value.absent(), + this.nickName = const Value.absent(), + this.avatarSvgCompressed = const Value.absent(), + this.senderProfileCounter = const Value.absent(), + this.accepted = const Value.absent(), + this.deletedByUser = const Value.absent(), + this.requested = const Value.absent(), + this.blocked = const Value.absent(), + this.verified = const Value.absent(), + this.accountDeleted = const Value.absent(), + this.createdAt = const Value.absent(), + }) : username = Value(username); + static Insertable custom({ + Expression? userId, + Expression? username, + Expression? displayName, + Expression? nickName, + Expression? avatarSvgCompressed, + Expression? senderProfileCounter, + Expression? accepted, + Expression? deletedByUser, + Expression? requested, + Expression? blocked, + Expression? verified, + Expression? accountDeleted, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (username != null) 'username': username, + if (displayName != null) 'display_name': displayName, + if (nickName != null) 'nick_name': nickName, + if (avatarSvgCompressed != null) + 'avatar_svg_compressed': avatarSvgCompressed, + if (senderProfileCounter != null) + 'sender_profile_counter': senderProfileCounter, + if (accepted != null) 'accepted': accepted, + if (deletedByUser != null) 'deleted_by_user': deletedByUser, + if (requested != null) 'requested': requested, + if (blocked != null) 'blocked': blocked, + if (verified != null) 'verified': verified, + if (accountDeleted != null) 'account_deleted': accountDeleted, + if (createdAt != null) 'created_at': createdAt, + }); + } + + ContactsCompanion copyWith( + {Value? userId, + Value? username, + Value? displayName, + Value? nickName, + Value? avatarSvgCompressed, + Value? senderProfileCounter, + Value? accepted, + Value? deletedByUser, + Value? requested, + Value? blocked, + Value? verified, + Value? accountDeleted, + Value? createdAt}) { + return ContactsCompanion( + userId: userId ?? this.userId, + username: username ?? this.username, + displayName: displayName ?? this.displayName, + nickName: nickName ?? this.nickName, + avatarSvgCompressed: avatarSvgCompressed ?? this.avatarSvgCompressed, + senderProfileCounter: senderProfileCounter ?? this.senderProfileCounter, + accepted: accepted ?? this.accepted, + deletedByUser: deletedByUser ?? this.deletedByUser, + requested: requested ?? this.requested, + blocked: blocked ?? this.blocked, + verified: verified ?? this.verified, + accountDeleted: accountDeleted ?? this.accountDeleted, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (nickName.present) { + map['nick_name'] = Variable(nickName.value); + } + if (avatarSvgCompressed.present) { + map['avatar_svg_compressed'] = + Variable(avatarSvgCompressed.value); + } + if (senderProfileCounter.present) { + map['sender_profile_counter'] = Variable(senderProfileCounter.value); + } + if (accepted.present) { + map['accepted'] = Variable(accepted.value); + } + if (deletedByUser.present) { + map['deleted_by_user'] = Variable(deletedByUser.value); + } + if (requested.present) { + map['requested'] = Variable(requested.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (verified.present) { + map['verified'] = Variable(verified.value); + } + if (accountDeleted.present) { + map['account_deleted'] = Variable(accountDeleted.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContactsCompanion(') + ..write('userId: $userId, ') + ..write('username: $username, ') + ..write('displayName: $displayName, ') + ..write('nickName: $nickName, ') + ..write('avatarSvgCompressed: $avatarSvgCompressed, ') + ..write('senderProfileCounter: $senderProfileCounter, ') + ..write('accepted: $accepted, ') + ..write('deletedByUser: $deletedByUser, ') + ..write('requested: $requested, ') + ..write('blocked: $blocked, ') + ..write('verified: $verified, ') + ..write('accountDeleted: $accountDeleted, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _isGroupAdminMeta = + const VerificationMeta('isGroupAdmin'); + @override + late final GeneratedColumn isGroupAdmin = GeneratedColumn( + 'is_group_admin', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_group_admin" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _isDirectChatMeta = + const VerificationMeta('isDirectChat'); + @override + late final GeneratedColumn isDirectChat = GeneratedColumn( + 'is_direct_chat', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_direct_chat" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _archivedMeta = + const VerificationMeta('archived'); + @override + late final GeneratedColumn archived = GeneratedColumn( + 'archived', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _joinedGroupMeta = + const VerificationMeta('joinedGroup'); + @override + late final GeneratedColumn joinedGroup = GeneratedColumn( + 'joined_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("joined_group" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _leftGroupMeta = + const VerificationMeta('leftGroup'); + @override + late final GeneratedColumn leftGroup = GeneratedColumn( + 'left_group', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("left_group" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deletedContentMeta = + const VerificationMeta('deletedContent'); + @override + late final GeneratedColumn deletedContent = GeneratedColumn( + 'deleted_content', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_content" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _stateVersionIdMeta = + const VerificationMeta('stateVersionId'); + @override + late final GeneratedColumn stateVersionId = GeneratedColumn( + 'state_version_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _stateEncryptionKeyMeta = + const VerificationMeta('stateEncryptionKey'); + @override + late final GeneratedColumn stateEncryptionKey = + GeneratedColumn('state_encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _myGroupPrivateKeyMeta = + const VerificationMeta('myGroupPrivateKey'); + @override + late final GeneratedColumn myGroupPrivateKey = + GeneratedColumn('my_group_private_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _groupNameMeta = + const VerificationMeta('groupName'); + @override + late final GeneratedColumn groupName = GeneratedColumn( + 'group_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _totalMediaCounterMeta = + const VerificationMeta('totalMediaCounter'); + @override + late final GeneratedColumn totalMediaCounter = GeneratedColumn( + 'total_media_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _alsoBestFriendMeta = + const VerificationMeta('alsoBestFriend'); + @override + late final GeneratedColumn alsoBestFriend = GeneratedColumn( + 'also_best_friend', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("also_best_friend" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _deleteMessagesAfterMillisecondsMeta = + const VerificationMeta('deleteMessagesAfterMilliseconds'); + @override + late final GeneratedColumn deleteMessagesAfterMilliseconds = + GeneratedColumn( + 'delete_messages_after_milliseconds', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(defaultDeleteMessagesAfterMilliseconds)); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _lastMessageSendMeta = + const VerificationMeta('lastMessageSend'); + @override + late final GeneratedColumn lastMessageSend = + GeneratedColumn('last_message_send', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastMessageReceivedMeta = + const VerificationMeta('lastMessageReceived'); + @override + late final GeneratedColumn lastMessageReceived = + GeneratedColumn('last_message_received', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameCounterChangeMeta = + const VerificationMeta('lastFlameCounterChange'); + @override + late final GeneratedColumn lastFlameCounterChange = + GeneratedColumn('last_flame_counter_change', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastFlameSyncMeta = + const VerificationMeta('lastFlameSync'); + @override + late final GeneratedColumn lastFlameSync = + GeneratedColumn('last_flame_sync', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _flameCounterMeta = + const VerificationMeta('flameCounter'); + @override + late final GeneratedColumn flameCounter = GeneratedColumn( + 'flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _maxFlameCounterMeta = + const VerificationMeta('maxFlameCounter'); + @override + late final GeneratedColumn maxFlameCounter = GeneratedColumn( + 'max_flame_counter', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _maxFlameCounterFromMeta = + const VerificationMeta('maxFlameCounterFrom'); + @override + late final GeneratedColumn maxFlameCounterFrom = + GeneratedColumn('max_flame_counter_from', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastMessageExchangeMeta = + const VerificationMeta('lastMessageExchange'); + @override + late final GeneratedColumn lastMessageExchange = + GeneratedColumn('last_message_exchange', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + stateEncryptionKey, + myGroupPrivateKey, + groupName, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'groups'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('is_group_admin')) { + context.handle( + _isGroupAdminMeta, + isGroupAdmin.isAcceptableOrUnknown( + data['is_group_admin']!, _isGroupAdminMeta)); + } + if (data.containsKey('is_direct_chat')) { + context.handle( + _isDirectChatMeta, + isDirectChat.isAcceptableOrUnknown( + data['is_direct_chat']!, _isDirectChatMeta)); + } + if (data.containsKey('pinned')) { + context.handle(_pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + } + if (data.containsKey('archived')) { + context.handle(_archivedMeta, + archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta)); + } + if (data.containsKey('joined_group')) { + context.handle( + _joinedGroupMeta, + joinedGroup.isAcceptableOrUnknown( + data['joined_group']!, _joinedGroupMeta)); + } + if (data.containsKey('left_group')) { + context.handle(_leftGroupMeta, + leftGroup.isAcceptableOrUnknown(data['left_group']!, _leftGroupMeta)); + } + if (data.containsKey('deleted_content')) { + context.handle( + _deletedContentMeta, + deletedContent.isAcceptableOrUnknown( + data['deleted_content']!, _deletedContentMeta)); + } + if (data.containsKey('state_version_id')) { + context.handle( + _stateVersionIdMeta, + stateVersionId.isAcceptableOrUnknown( + data['state_version_id']!, _stateVersionIdMeta)); + } + if (data.containsKey('state_encryption_key')) { + context.handle( + _stateEncryptionKeyMeta, + stateEncryptionKey.isAcceptableOrUnknown( + data['state_encryption_key']!, _stateEncryptionKeyMeta)); + } + if (data.containsKey('my_group_private_key')) { + context.handle( + _myGroupPrivateKeyMeta, + myGroupPrivateKey.isAcceptableOrUnknown( + data['my_group_private_key']!, _myGroupPrivateKeyMeta)); + } + if (data.containsKey('group_name')) { + context.handle(_groupNameMeta, + groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta)); + } else if (isInserting) { + context.missing(_groupNameMeta); + } + if (data.containsKey('total_media_counter')) { + context.handle( + _totalMediaCounterMeta, + totalMediaCounter.isAcceptableOrUnknown( + data['total_media_counter']!, _totalMediaCounterMeta)); + } + if (data.containsKey('also_best_friend')) { + context.handle( + _alsoBestFriendMeta, + alsoBestFriend.isAcceptableOrUnknown( + data['also_best_friend']!, _alsoBestFriendMeta)); + } + if (data.containsKey('delete_messages_after_milliseconds')) { + context.handle( + _deleteMessagesAfterMillisecondsMeta, + deleteMessagesAfterMilliseconds.isAcceptableOrUnknown( + data['delete_messages_after_milliseconds']!, + _deleteMessagesAfterMillisecondsMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('last_message_send')) { + context.handle( + _lastMessageSendMeta, + lastMessageSend.isAcceptableOrUnknown( + data['last_message_send']!, _lastMessageSendMeta)); + } + if (data.containsKey('last_message_received')) { + context.handle( + _lastMessageReceivedMeta, + lastMessageReceived.isAcceptableOrUnknown( + data['last_message_received']!, _lastMessageReceivedMeta)); + } + if (data.containsKey('last_flame_counter_change')) { + context.handle( + _lastFlameCounterChangeMeta, + lastFlameCounterChange.isAcceptableOrUnknown( + data['last_flame_counter_change']!, _lastFlameCounterChangeMeta)); + } + if (data.containsKey('last_flame_sync')) { + context.handle( + _lastFlameSyncMeta, + lastFlameSync.isAcceptableOrUnknown( + data['last_flame_sync']!, _lastFlameSyncMeta)); + } + if (data.containsKey('flame_counter')) { + context.handle( + _flameCounterMeta, + flameCounter.isAcceptableOrUnknown( + data['flame_counter']!, _flameCounterMeta)); + } + if (data.containsKey('max_flame_counter')) { + context.handle( + _maxFlameCounterMeta, + maxFlameCounter.isAcceptableOrUnknown( + data['max_flame_counter']!, _maxFlameCounterMeta)); + } + if (data.containsKey('max_flame_counter_from')) { + context.handle( + _maxFlameCounterFromMeta, + maxFlameCounterFrom.isAcceptableOrUnknown( + data['max_flame_counter_from']!, _maxFlameCounterFromMeta)); + } + if (data.containsKey('last_message_exchange')) { + context.handle( + _lastMessageExchangeMeta, + lastMessageExchange.isAcceptableOrUnknown( + data['last_message_exchange']!, _lastMessageExchangeMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupId}; + @override + Group map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Group( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + isGroupAdmin: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_group_admin'])!, + isDirectChat: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_direct_chat'])!, + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + archived: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, + joinedGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}joined_group'])!, + leftGroup: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}left_group'])!, + deletedContent: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_content'])!, + stateVersionId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}state_version_id'])!, + stateEncryptionKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}state_encryption_key']), + myGroupPrivateKey: attachedDatabase.typeMapping.read( + DriftSqlType.blob, data['${effectivePrefix}my_group_private_key']), + groupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_name'])!, + totalMediaCounter: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}total_media_counter'])!, + alsoBestFriend: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}also_best_friend'])!, + deleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}delete_messages_after_milliseconds'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + lastMessageSend: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_message_send']), + lastMessageReceived: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_received']), + lastFlameCounterChange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_flame_counter_change']), + lastFlameSync: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}last_flame_sync']), + flameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}flame_counter'])!, + maxFlameCounter: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}max_flame_counter'])!, + maxFlameCounterFrom: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}max_flame_counter_from']), + lastMessageExchange: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_exchange'])!, + ); + } + + @override + $GroupsTable createAlias(String alias) { + return $GroupsTable(attachedDatabase, alias); + } +} + +class Group extends DataClass implements Insertable { + final String groupId; + final bool isGroupAdmin; + final bool isDirectChat; + final bool pinned; + final bool archived; + final bool joinedGroup; + final bool leftGroup; + final bool deletedContent; + final int stateVersionId; + final Uint8List? stateEncryptionKey; + final Uint8List? myGroupPrivateKey; + final String groupName; + final int totalMediaCounter; + final bool alsoBestFriend; + final int deleteMessagesAfterMilliseconds; + final DateTime createdAt; + final DateTime? lastMessageSend; + final DateTime? lastMessageReceived; + final DateTime? lastFlameCounterChange; + final DateTime? lastFlameSync; + final int flameCounter; + final int maxFlameCounter; + final DateTime? maxFlameCounterFrom; + final DateTime lastMessageExchange; + const Group( + {required this.groupId, + required this.isGroupAdmin, + required this.isDirectChat, + required this.pinned, + required this.archived, + required this.joinedGroup, + required this.leftGroup, + required this.deletedContent, + required this.stateVersionId, + this.stateEncryptionKey, + this.myGroupPrivateKey, + required this.groupName, + required this.totalMediaCounter, + required this.alsoBestFriend, + required this.deleteMessagesAfterMilliseconds, + required this.createdAt, + this.lastMessageSend, + this.lastMessageReceived, + this.lastFlameCounterChange, + this.lastFlameSync, + required this.flameCounter, + required this.maxFlameCounter, + this.maxFlameCounterFrom, + required this.lastMessageExchange}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['is_group_admin'] = Variable(isGroupAdmin); + map['is_direct_chat'] = Variable(isDirectChat); + map['pinned'] = Variable(pinned); + map['archived'] = Variable(archived); + map['joined_group'] = Variable(joinedGroup); + map['left_group'] = Variable(leftGroup); + map['deleted_content'] = Variable(deletedContent); + map['state_version_id'] = Variable(stateVersionId); + if (!nullToAbsent || stateEncryptionKey != null) { + map['state_encryption_key'] = Variable(stateEncryptionKey); + } + if (!nullToAbsent || myGroupPrivateKey != null) { + map['my_group_private_key'] = Variable(myGroupPrivateKey); + } + map['group_name'] = Variable(groupName); + map['total_media_counter'] = Variable(totalMediaCounter); + map['also_best_friend'] = Variable(alsoBestFriend); + map['delete_messages_after_milliseconds'] = + Variable(deleteMessagesAfterMilliseconds); + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || lastMessageSend != null) { + map['last_message_send'] = Variable(lastMessageSend); + } + if (!nullToAbsent || lastMessageReceived != null) { + map['last_message_received'] = Variable(lastMessageReceived); + } + if (!nullToAbsent || lastFlameCounterChange != null) { + map['last_flame_counter_change'] = + Variable(lastFlameCounterChange); + } + if (!nullToAbsent || lastFlameSync != null) { + map['last_flame_sync'] = Variable(lastFlameSync); + } + map['flame_counter'] = Variable(flameCounter); + map['max_flame_counter'] = Variable(maxFlameCounter); + if (!nullToAbsent || maxFlameCounterFrom != null) { + map['max_flame_counter_from'] = Variable(maxFlameCounterFrom); + } + map['last_message_exchange'] = Variable(lastMessageExchange); + return map; + } + + GroupsCompanion toCompanion(bool nullToAbsent) { + return GroupsCompanion( + groupId: Value(groupId), + isGroupAdmin: Value(isGroupAdmin), + isDirectChat: Value(isDirectChat), + pinned: Value(pinned), + archived: Value(archived), + joinedGroup: Value(joinedGroup), + leftGroup: Value(leftGroup), + deletedContent: Value(deletedContent), + stateVersionId: Value(stateVersionId), + stateEncryptionKey: stateEncryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(stateEncryptionKey), + myGroupPrivateKey: myGroupPrivateKey == null && nullToAbsent + ? const Value.absent() + : Value(myGroupPrivateKey), + groupName: Value(groupName), + totalMediaCounter: Value(totalMediaCounter), + alsoBestFriend: Value(alsoBestFriend), + deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds), + createdAt: Value(createdAt), + lastMessageSend: lastMessageSend == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageSend), + lastMessageReceived: lastMessageReceived == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageReceived), + lastFlameCounterChange: lastFlameCounterChange == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameCounterChange), + lastFlameSync: lastFlameSync == null && nullToAbsent + ? const Value.absent() + : Value(lastFlameSync), + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: maxFlameCounterFrom == null && nullToAbsent + ? const Value.absent() + : Value(maxFlameCounterFrom), + lastMessageExchange: Value(lastMessageExchange), + ); + } + + factory Group.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Group( + groupId: serializer.fromJson(json['groupId']), + isGroupAdmin: serializer.fromJson(json['isGroupAdmin']), + isDirectChat: serializer.fromJson(json['isDirectChat']), + pinned: serializer.fromJson(json['pinned']), + archived: serializer.fromJson(json['archived']), + joinedGroup: serializer.fromJson(json['joinedGroup']), + leftGroup: serializer.fromJson(json['leftGroup']), + deletedContent: serializer.fromJson(json['deletedContent']), + stateVersionId: serializer.fromJson(json['stateVersionId']), + stateEncryptionKey: + serializer.fromJson(json['stateEncryptionKey']), + myGroupPrivateKey: + serializer.fromJson(json['myGroupPrivateKey']), + groupName: serializer.fromJson(json['groupName']), + totalMediaCounter: serializer.fromJson(json['totalMediaCounter']), + alsoBestFriend: serializer.fromJson(json['alsoBestFriend']), + deleteMessagesAfterMilliseconds: + serializer.fromJson(json['deleteMessagesAfterMilliseconds']), + createdAt: serializer.fromJson(json['createdAt']), + lastMessageSend: serializer.fromJson(json['lastMessageSend']), + lastMessageReceived: + serializer.fromJson(json['lastMessageReceived']), + lastFlameCounterChange: + serializer.fromJson(json['lastFlameCounterChange']), + lastFlameSync: serializer.fromJson(json['lastFlameSync']), + flameCounter: serializer.fromJson(json['flameCounter']), + maxFlameCounter: serializer.fromJson(json['maxFlameCounter']), + maxFlameCounterFrom: + serializer.fromJson(json['maxFlameCounterFrom']), + lastMessageExchange: + serializer.fromJson(json['lastMessageExchange']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'isGroupAdmin': serializer.toJson(isGroupAdmin), + 'isDirectChat': serializer.toJson(isDirectChat), + 'pinned': serializer.toJson(pinned), + 'archived': serializer.toJson(archived), + 'joinedGroup': serializer.toJson(joinedGroup), + 'leftGroup': serializer.toJson(leftGroup), + 'deletedContent': serializer.toJson(deletedContent), + 'stateVersionId': serializer.toJson(stateVersionId), + 'stateEncryptionKey': serializer.toJson(stateEncryptionKey), + 'myGroupPrivateKey': serializer.toJson(myGroupPrivateKey), + 'groupName': serializer.toJson(groupName), + 'totalMediaCounter': serializer.toJson(totalMediaCounter), + 'alsoBestFriend': serializer.toJson(alsoBestFriend), + 'deleteMessagesAfterMilliseconds': + serializer.toJson(deleteMessagesAfterMilliseconds), + 'createdAt': serializer.toJson(createdAt), + 'lastMessageSend': serializer.toJson(lastMessageSend), + 'lastMessageReceived': serializer.toJson(lastMessageReceived), + 'lastFlameCounterChange': + serializer.toJson(lastFlameCounterChange), + 'lastFlameSync': serializer.toJson(lastFlameSync), + 'flameCounter': serializer.toJson(flameCounter), + 'maxFlameCounter': serializer.toJson(maxFlameCounter), + 'maxFlameCounterFrom': serializer.toJson(maxFlameCounterFrom), + 'lastMessageExchange': serializer.toJson(lastMessageExchange), + }; + } + + Group copyWith( + {String? groupId, + bool? isGroupAdmin, + bool? isDirectChat, + bool? pinned, + bool? archived, + bool? joinedGroup, + bool? leftGroup, + bool? deletedContent, + int? stateVersionId, + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), + String? groupName, + int? totalMediaCounter, + bool? alsoBestFriend, + int? deleteMessagesAfterMilliseconds, + DateTime? createdAt, + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + int? flameCounter, + int? maxFlameCounter, + Value maxFlameCounterFrom = const Value.absent(), + DateTime? lastMessageExchange}) => + Group( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey.present + ? stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey.present + ? myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend.present + ? lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: lastMessageReceived.present + ? lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange.present + ? lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: + lastFlameSync.present ? lastFlameSync.value : this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom.present + ? maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + ); + Group copyWithCompanion(GroupsCompanion data) { + return Group( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + isGroupAdmin: data.isGroupAdmin.present + ? data.isGroupAdmin.value + : this.isGroupAdmin, + isDirectChat: data.isDirectChat.present + ? data.isDirectChat.value + : this.isDirectChat, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + archived: data.archived.present ? data.archived.value : this.archived, + joinedGroup: + data.joinedGroup.present ? data.joinedGroup.value : this.joinedGroup, + leftGroup: data.leftGroup.present ? data.leftGroup.value : this.leftGroup, + deletedContent: data.deletedContent.present + ? data.deletedContent.value + : this.deletedContent, + stateVersionId: data.stateVersionId.present + ? data.stateVersionId.value + : this.stateVersionId, + stateEncryptionKey: data.stateEncryptionKey.present + ? data.stateEncryptionKey.value + : this.stateEncryptionKey, + myGroupPrivateKey: data.myGroupPrivateKey.present + ? data.myGroupPrivateKey.value + : this.myGroupPrivateKey, + groupName: data.groupName.present ? data.groupName.value : this.groupName, + totalMediaCounter: data.totalMediaCounter.present + ? data.totalMediaCounter.value + : this.totalMediaCounter, + alsoBestFriend: data.alsoBestFriend.present + ? data.alsoBestFriend.value + : this.alsoBestFriend, + deleteMessagesAfterMilliseconds: + data.deleteMessagesAfterMilliseconds.present + ? data.deleteMessagesAfterMilliseconds.value + : this.deleteMessagesAfterMilliseconds, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + lastMessageSend: data.lastMessageSend.present + ? data.lastMessageSend.value + : this.lastMessageSend, + lastMessageReceived: data.lastMessageReceived.present + ? data.lastMessageReceived.value + : this.lastMessageReceived, + lastFlameCounterChange: data.lastFlameCounterChange.present + ? data.lastFlameCounterChange.value + : this.lastFlameCounterChange, + lastFlameSync: data.lastFlameSync.present + ? data.lastFlameSync.value + : this.lastFlameSync, + flameCounter: data.flameCounter.present + ? data.flameCounter.value + : this.flameCounter, + maxFlameCounter: data.maxFlameCounter.present + ? data.maxFlameCounter.value + : this.maxFlameCounter, + maxFlameCounterFrom: data.maxFlameCounterFrom.present + ? data.maxFlameCounterFrom.value + : this.maxFlameCounterFrom, + lastMessageExchange: data.lastMessageExchange.present + ? data.lastMessageExchange.value + : this.lastMessageExchange, + ); + } + + @override + String toString() { + return (StringBuffer('Group(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + groupId, + isGroupAdmin, + isDirectChat, + pinned, + archived, + joinedGroup, + leftGroup, + deletedContent, + stateVersionId, + $driftBlobEquality.hash(stateEncryptionKey), + $driftBlobEquality.hash(myGroupPrivateKey), + groupName, + totalMediaCounter, + alsoBestFriend, + deleteMessagesAfterMilliseconds, + createdAt, + lastMessageSend, + lastMessageReceived, + lastFlameCounterChange, + lastFlameSync, + flameCounter, + maxFlameCounter, + maxFlameCounterFrom, + lastMessageExchange + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Group && + other.groupId == this.groupId && + other.isGroupAdmin == this.isGroupAdmin && + other.isDirectChat == this.isDirectChat && + other.pinned == this.pinned && + other.archived == this.archived && + other.joinedGroup == this.joinedGroup && + other.leftGroup == this.leftGroup && + other.deletedContent == this.deletedContent && + other.stateVersionId == this.stateVersionId && + $driftBlobEquality.equals( + other.stateEncryptionKey, this.stateEncryptionKey) && + $driftBlobEquality.equals( + other.myGroupPrivateKey, this.myGroupPrivateKey) && + other.groupName == this.groupName && + other.totalMediaCounter == this.totalMediaCounter && + other.alsoBestFriend == this.alsoBestFriend && + other.deleteMessagesAfterMilliseconds == + this.deleteMessagesAfterMilliseconds && + other.createdAt == this.createdAt && + other.lastMessageSend == this.lastMessageSend && + other.lastMessageReceived == this.lastMessageReceived && + other.lastFlameCounterChange == this.lastFlameCounterChange && + other.lastFlameSync == this.lastFlameSync && + other.flameCounter == this.flameCounter && + other.maxFlameCounter == this.maxFlameCounter && + other.maxFlameCounterFrom == this.maxFlameCounterFrom && + other.lastMessageExchange == this.lastMessageExchange); +} + +class GroupsCompanion extends UpdateCompanion { + final Value groupId; + final Value isGroupAdmin; + final Value isDirectChat; + final Value pinned; + final Value archived; + final Value joinedGroup; + final Value leftGroup; + final Value deletedContent; + final Value stateVersionId; + final Value stateEncryptionKey; + final Value myGroupPrivateKey; + final Value groupName; + final Value totalMediaCounter; + final Value alsoBestFriend; + final Value deleteMessagesAfterMilliseconds; + final Value createdAt; + final Value lastMessageSend; + final Value lastMessageReceived; + final Value lastFlameCounterChange; + final Value lastFlameSync; + final Value flameCounter; + final Value maxFlameCounter; + final Value maxFlameCounterFrom; + final Value lastMessageExchange; + final Value rowid; + const GroupsCompanion({ + this.groupId = const Value.absent(), + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + this.groupName = const Value.absent(), + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupsCompanion.insert({ + required String groupId, + this.isGroupAdmin = const Value.absent(), + this.isDirectChat = const Value.absent(), + this.pinned = const Value.absent(), + this.archived = const Value.absent(), + this.joinedGroup = const Value.absent(), + this.leftGroup = const Value.absent(), + this.deletedContent = const Value.absent(), + this.stateVersionId = const Value.absent(), + this.stateEncryptionKey = const Value.absent(), + this.myGroupPrivateKey = const Value.absent(), + required String groupName, + this.totalMediaCounter = const Value.absent(), + this.alsoBestFriend = const Value.absent(), + this.deleteMessagesAfterMilliseconds = const Value.absent(), + this.createdAt = const Value.absent(), + this.lastMessageSend = const Value.absent(), + this.lastMessageReceived = const Value.absent(), + this.lastFlameCounterChange = const Value.absent(), + this.lastFlameSync = const Value.absent(), + this.flameCounter = const Value.absent(), + this.maxFlameCounter = const Value.absent(), + this.maxFlameCounterFrom = const Value.absent(), + this.lastMessageExchange = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + groupName = Value(groupName); + static Insertable custom({ + Expression? groupId, + Expression? isGroupAdmin, + Expression? isDirectChat, + Expression? pinned, + Expression? archived, + Expression? joinedGroup, + Expression? leftGroup, + Expression? deletedContent, + Expression? stateVersionId, + Expression? stateEncryptionKey, + Expression? myGroupPrivateKey, + Expression? groupName, + Expression? totalMediaCounter, + Expression? alsoBestFriend, + Expression? deleteMessagesAfterMilliseconds, + Expression? createdAt, + Expression? lastMessageSend, + Expression? lastMessageReceived, + Expression? lastFlameCounterChange, + Expression? lastFlameSync, + Expression? flameCounter, + Expression? maxFlameCounter, + Expression? maxFlameCounterFrom, + Expression? lastMessageExchange, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (isGroupAdmin != null) 'is_group_admin': isGroupAdmin, + if (isDirectChat != null) 'is_direct_chat': isDirectChat, + if (pinned != null) 'pinned': pinned, + if (archived != null) 'archived': archived, + if (joinedGroup != null) 'joined_group': joinedGroup, + if (leftGroup != null) 'left_group': leftGroup, + if (deletedContent != null) 'deleted_content': deletedContent, + if (stateVersionId != null) 'state_version_id': stateVersionId, + if (stateEncryptionKey != null) + 'state_encryption_key': stateEncryptionKey, + if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey, + if (groupName != null) 'group_name': groupName, + if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter, + if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend, + if (deleteMessagesAfterMilliseconds != null) + 'delete_messages_after_milliseconds': deleteMessagesAfterMilliseconds, + if (createdAt != null) 'created_at': createdAt, + if (lastMessageSend != null) 'last_message_send': lastMessageSend, + if (lastMessageReceived != null) + 'last_message_received': lastMessageReceived, + if (lastFlameCounterChange != null) + 'last_flame_counter_change': lastFlameCounterChange, + if (lastFlameSync != null) 'last_flame_sync': lastFlameSync, + if (flameCounter != null) 'flame_counter': flameCounter, + if (maxFlameCounter != null) 'max_flame_counter': maxFlameCounter, + if (maxFlameCounterFrom != null) + 'max_flame_counter_from': maxFlameCounterFrom, + if (lastMessageExchange != null) + 'last_message_exchange': lastMessageExchange, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupsCompanion copyWith( + {Value? groupId, + Value? isGroupAdmin, + Value? isDirectChat, + Value? pinned, + Value? archived, + Value? joinedGroup, + Value? leftGroup, + Value? deletedContent, + Value? stateVersionId, + Value? stateEncryptionKey, + Value? myGroupPrivateKey, + Value? groupName, + Value? totalMediaCounter, + Value? alsoBestFriend, + Value? deleteMessagesAfterMilliseconds, + Value? createdAt, + Value? lastMessageSend, + Value? lastMessageReceived, + Value? lastFlameCounterChange, + Value? lastFlameSync, + Value? flameCounter, + Value? maxFlameCounter, + Value? maxFlameCounterFrom, + Value? lastMessageExchange, + Value? rowid}) { + return GroupsCompanion( + groupId: groupId ?? this.groupId, + isGroupAdmin: isGroupAdmin ?? this.isGroupAdmin, + isDirectChat: isDirectChat ?? this.isDirectChat, + pinned: pinned ?? this.pinned, + archived: archived ?? this.archived, + joinedGroup: joinedGroup ?? this.joinedGroup, + leftGroup: leftGroup ?? this.leftGroup, + deletedContent: deletedContent ?? this.deletedContent, + stateVersionId: stateVersionId ?? this.stateVersionId, + stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey, + groupName: groupName ?? this.groupName, + totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter, + alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ?? + this.deleteMessagesAfterMilliseconds, + createdAt: createdAt ?? this.createdAt, + lastMessageSend: lastMessageSend ?? this.lastMessageSend, + lastMessageReceived: lastMessageReceived ?? this.lastMessageReceived, + lastFlameCounterChange: + lastFlameCounterChange ?? this.lastFlameCounterChange, + lastFlameSync: lastFlameSync ?? this.lastFlameSync, + flameCounter: flameCounter ?? this.flameCounter, + maxFlameCounter: maxFlameCounter ?? this.maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom ?? this.maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (isGroupAdmin.present) { + map['is_group_admin'] = Variable(isGroupAdmin.value); + } + if (isDirectChat.present) { + map['is_direct_chat'] = Variable(isDirectChat.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (archived.present) { + map['archived'] = Variable(archived.value); + } + if (joinedGroup.present) { + map['joined_group'] = Variable(joinedGroup.value); + } + if (leftGroup.present) { + map['left_group'] = Variable(leftGroup.value); + } + if (deletedContent.present) { + map['deleted_content'] = Variable(deletedContent.value); + } + if (stateVersionId.present) { + map['state_version_id'] = Variable(stateVersionId.value); + } + if (stateEncryptionKey.present) { + map['state_encryption_key'] = + Variable(stateEncryptionKey.value); + } + if (myGroupPrivateKey.present) { + map['my_group_private_key'] = + Variable(myGroupPrivateKey.value); + } + if (groupName.present) { + map['group_name'] = Variable(groupName.value); + } + if (totalMediaCounter.present) { + map['total_media_counter'] = Variable(totalMediaCounter.value); + } + if (alsoBestFriend.present) { + map['also_best_friend'] = Variable(alsoBestFriend.value); + } + if (deleteMessagesAfterMilliseconds.present) { + map['delete_messages_after_milliseconds'] = + Variable(deleteMessagesAfterMilliseconds.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (lastMessageSend.present) { + map['last_message_send'] = Variable(lastMessageSend.value); + } + if (lastMessageReceived.present) { + map['last_message_received'] = + Variable(lastMessageReceived.value); + } + if (lastFlameCounterChange.present) { + map['last_flame_counter_change'] = + Variable(lastFlameCounterChange.value); + } + if (lastFlameSync.present) { + map['last_flame_sync'] = Variable(lastFlameSync.value); + } + if (flameCounter.present) { + map['flame_counter'] = Variable(flameCounter.value); + } + if (maxFlameCounter.present) { + map['max_flame_counter'] = Variable(maxFlameCounter.value); + } + if (maxFlameCounterFrom.present) { + map['max_flame_counter_from'] = + Variable(maxFlameCounterFrom.value); + } + if (lastMessageExchange.present) { + map['last_message_exchange'] = + Variable(lastMessageExchange.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupsCompanion(') + ..write('groupId: $groupId, ') + ..write('isGroupAdmin: $isGroupAdmin, ') + ..write('isDirectChat: $isDirectChat, ') + ..write('pinned: $pinned, ') + ..write('archived: $archived, ') + ..write('joinedGroup: $joinedGroup, ') + ..write('leftGroup: $leftGroup, ') + ..write('deletedContent: $deletedContent, ') + ..write('stateVersionId: $stateVersionId, ') + ..write('stateEncryptionKey: $stateEncryptionKey, ') + ..write('myGroupPrivateKey: $myGroupPrivateKey, ') + ..write('groupName: $groupName, ') + ..write('totalMediaCounter: $totalMediaCounter, ') + ..write('alsoBestFriend: $alsoBestFriend, ') + ..write( + 'deleteMessagesAfterMilliseconds: $deleteMessagesAfterMilliseconds, ') + ..write('createdAt: $createdAt, ') + ..write('lastMessageSend: $lastMessageSend, ') + ..write('lastMessageReceived: $lastMessageReceived, ') + ..write('lastFlameCounterChange: $lastFlameCounterChange, ') + ..write('lastFlameSync: $lastFlameSync, ') + ..write('flameCounter: $flameCounter, ') + ..write('maxFlameCounter: $maxFlameCounter, ') + ..write('maxFlameCounterFrom: $maxFlameCounterFrom, ') + ..write('lastMessageExchange: $lastMessageExchange, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MediaFilesTable extends MediaFiles + with TableInfo<$MediaFilesTable, MediaFile> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MediaFilesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _mediaIdMeta = + const VerificationMeta('mediaId'); + @override + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($MediaFilesTable.$convertertype); + @override + late final GeneratedColumnWithTypeConverter + uploadState = GeneratedColumn('upload_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter($MediaFilesTable.$converteruploadStaten); + @override + late final GeneratedColumnWithTypeConverter + downloadState = GeneratedColumn( + 'download_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter( + $MediaFilesTable.$converterdownloadStaten); + static const VerificationMeta _requiresAuthenticationMeta = + const VerificationMeta('requiresAuthentication'); + @override + late final GeneratedColumn requiresAuthentication = + GeneratedColumn('requires_authentication', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("requires_authentication" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _reopenByContactMeta = + const VerificationMeta('reopenByContact'); + @override + late final GeneratedColumn reopenByContact = GeneratedColumn( + 'reopen_by_contact', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("reopen_by_contact" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _storedMeta = const VerificationMeta('stored'); + @override + late final GeneratedColumn stored = GeneratedColumn( + 'stored', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _isDraftMediaMeta = + const VerificationMeta('isDraftMedia'); + @override + late final GeneratedColumn isDraftMedia = GeneratedColumn( + 'is_draft_media', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_draft_media" IN (0, 1))'), + defaultValue: const Constant(false)); + @override + late final GeneratedColumnWithTypeConverter?, String> + reuploadRequestedBy = GeneratedColumn( + 'reupload_requested_by', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $MediaFilesTable.$converterreuploadRequestedByn); + static const VerificationMeta _displayLimitInMillisecondsMeta = + const VerificationMeta('displayLimitInMilliseconds'); + @override + late final GeneratedColumn displayLimitInMilliseconds = + GeneratedColumn('display_limit_in_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _removeAudioMeta = + const VerificationMeta('removeAudio'); + @override + late final GeneratedColumn removeAudio = GeneratedColumn( + 'remove_audio', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("remove_audio" IN (0, 1))')); + static const VerificationMeta _downloadTokenMeta = + const VerificationMeta('downloadToken'); + @override + late final GeneratedColumn downloadToken = + GeneratedColumn('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionKeyMeta = + const VerificationMeta('encryptionKey'); + @override + late final GeneratedColumn encryptionKey = + GeneratedColumn('encryption_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionMacMeta = + const VerificationMeta('encryptionMac'); + @override + late final GeneratedColumn encryptionMac = + GeneratedColumn('encryption_mac', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _encryptionNonceMeta = + const VerificationMeta('encryptionNonce'); + @override + late final GeneratedColumn encryptionNonce = + GeneratedColumn('encryption_nonce', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + reopenByContact, + stored, + isDraftMedia, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + downloadToken, + encryptionKey, + encryptionMac, + encryptionNonce, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'media_files'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('media_id')) { + context.handle(_mediaIdMeta, + mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); + } else if (isInserting) { + context.missing(_mediaIdMeta); + } + if (data.containsKey('requires_authentication')) { + context.handle( + _requiresAuthenticationMeta, + requiresAuthentication.isAcceptableOrUnknown( + data['requires_authentication']!, _requiresAuthenticationMeta)); + } + if (data.containsKey('reopen_by_contact')) { + context.handle( + _reopenByContactMeta, + reopenByContact.isAcceptableOrUnknown( + data['reopen_by_contact']!, _reopenByContactMeta)); + } + if (data.containsKey('stored')) { + context.handle(_storedMeta, + stored.isAcceptableOrUnknown(data['stored']!, _storedMeta)); + } + if (data.containsKey('is_draft_media')) { + context.handle( + _isDraftMediaMeta, + isDraftMedia.isAcceptableOrUnknown( + data['is_draft_media']!, _isDraftMediaMeta)); + } + if (data.containsKey('display_limit_in_milliseconds')) { + context.handle( + _displayLimitInMillisecondsMeta, + displayLimitInMilliseconds.isAcceptableOrUnknown( + data['display_limit_in_milliseconds']!, + _displayLimitInMillisecondsMeta)); + } + if (data.containsKey('remove_audio')) { + context.handle( + _removeAudioMeta, + removeAudio.isAcceptableOrUnknown( + data['remove_audio']!, _removeAudioMeta)); + } + if (data.containsKey('download_token')) { + context.handle( + _downloadTokenMeta, + downloadToken.isAcceptableOrUnknown( + data['download_token']!, _downloadTokenMeta)); + } + if (data.containsKey('encryption_key')) { + context.handle( + _encryptionKeyMeta, + encryptionKey.isAcceptableOrUnknown( + data['encryption_key']!, _encryptionKeyMeta)); + } + if (data.containsKey('encryption_mac')) { + context.handle( + _encryptionMacMeta, + encryptionMac.isAcceptableOrUnknown( + data['encryption_mac']!, _encryptionMacMeta)); + } + if (data.containsKey('encryption_nonce')) { + context.handle( + _encryptionNonceMeta, + encryptionNonce.isAcceptableOrUnknown( + data['encryption_nonce']!, _encryptionNonceMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {mediaId}; + @override + MediaFile map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MediaFile( + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id'])!, + type: $MediaFilesTable.$convertertype.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + uploadState: $MediaFilesTable.$converteruploadStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}upload_state'])), + downloadState: $MediaFilesTable.$converterdownloadStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}download_state'])), + requiresAuthentication: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}requires_authentication'])!, + reopenByContact: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, + stored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, + isDraftMedia: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!, + reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}reupload_requested_by'])), + displayLimitInMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_limit_in_milliseconds']), + removeAudio: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}remove_audio']), + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), + encryptionKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_key']), + encryptionMac: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_mac']), + encryptionNonce: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}encryption_nonce']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $MediaFilesTable createAlias(String alias) { + return $MediaFilesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MediaType.values); + static JsonTypeConverter2 $converteruploadState = + const EnumNameConverter(UploadState.values); + static JsonTypeConverter2 + $converteruploadStaten = + JsonTypeConverter2.asNullable($converteruploadState); + static JsonTypeConverter2 + $converterdownloadState = + const EnumNameConverter(DownloadState.values); + static JsonTypeConverter2 + $converterdownloadStaten = + JsonTypeConverter2.asNullable($converterdownloadState); + static TypeConverter, String> $converterreuploadRequestedBy = + IntListTypeConverter(); + static TypeConverter?, String?> $converterreuploadRequestedByn = + NullAwareTypeConverter.wrap($converterreuploadRequestedBy); +} + +class MediaFile extends DataClass implements Insertable { + final String mediaId; + final MediaType type; + final UploadState? uploadState; + final DownloadState? downloadState; + final bool requiresAuthentication; + final bool reopenByContact; + final bool stored; + final bool isDraftMedia; + final List? reuploadRequestedBy; + final int? displayLimitInMilliseconds; + final bool? removeAudio; + final Uint8List? downloadToken; + final Uint8List? encryptionKey; + final Uint8List? encryptionMac; + final Uint8List? encryptionNonce; + final DateTime createdAt; + const MediaFile( + {required this.mediaId, + required this.type, + this.uploadState, + this.downloadState, + required this.requiresAuthentication, + required this.reopenByContact, + required this.stored, + required this.isDraftMedia, + this.reuploadRequestedBy, + this.displayLimitInMilliseconds, + this.removeAudio, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['media_id'] = Variable(mediaId); + { + map['type'] = + Variable($MediaFilesTable.$convertertype.toSql(type)); + } + if (!nullToAbsent || uploadState != null) { + map['upload_state'] = Variable( + $MediaFilesTable.$converteruploadStaten.toSql(uploadState)); + } + if (!nullToAbsent || downloadState != null) { + map['download_state'] = Variable( + $MediaFilesTable.$converterdownloadStaten.toSql(downloadState)); + } + map['requires_authentication'] = Variable(requiresAuthentication); + map['reopen_by_contact'] = Variable(reopenByContact); + map['stored'] = Variable(stored); + map['is_draft_media'] = Variable(isDraftMedia); + if (!nullToAbsent || reuploadRequestedBy != null) { + map['reupload_requested_by'] = Variable($MediaFilesTable + .$converterreuploadRequestedByn + .toSql(reuploadRequestedBy)); + } + if (!nullToAbsent || displayLimitInMilliseconds != null) { + map['display_limit_in_milliseconds'] = + Variable(displayLimitInMilliseconds); + } + if (!nullToAbsent || removeAudio != null) { + map['remove_audio'] = Variable(removeAudio); + } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || encryptionKey != null) { + map['encryption_key'] = Variable(encryptionKey); + } + if (!nullToAbsent || encryptionMac != null) { + map['encryption_mac'] = Variable(encryptionMac); + } + if (!nullToAbsent || encryptionNonce != null) { + map['encryption_nonce'] = Variable(encryptionNonce); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MediaFilesCompanion toCompanion(bool nullToAbsent) { + return MediaFilesCompanion( + mediaId: Value(mediaId), + type: Value(type), + uploadState: uploadState == null && nullToAbsent + ? const Value.absent() + : Value(uploadState), + downloadState: downloadState == null && nullToAbsent + ? const Value.absent() + : Value(downloadState), + requiresAuthentication: Value(requiresAuthentication), + reopenByContact: Value(reopenByContact), + stored: Value(stored), + isDraftMedia: Value(isDraftMedia), + reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent + ? const Value.absent() + : Value(reuploadRequestedBy), + displayLimitInMilliseconds: + displayLimitInMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(displayLimitInMilliseconds), + removeAudio: removeAudio == null && nullToAbsent + ? const Value.absent() + : Value(removeAudio), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + encryptionKey: encryptionKey == null && nullToAbsent + ? const Value.absent() + : Value(encryptionKey), + encryptionMac: encryptionMac == null && nullToAbsent + ? const Value.absent() + : Value(encryptionMac), + encryptionNonce: encryptionNonce == null && nullToAbsent + ? const Value.absent() + : Value(encryptionNonce), + createdAt: Value(createdAt), + ); + } + + factory MediaFile.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MediaFile( + mediaId: serializer.fromJson(json['mediaId']), + type: $MediaFilesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + uploadState: $MediaFilesTable.$converteruploadStaten + .fromJson(serializer.fromJson(json['uploadState'])), + downloadState: $MediaFilesTable.$converterdownloadStaten + .fromJson(serializer.fromJson(json['downloadState'])), + requiresAuthentication: + serializer.fromJson(json['requiresAuthentication']), + reopenByContact: serializer.fromJson(json['reopenByContact']), + stored: serializer.fromJson(json['stored']), + isDraftMedia: serializer.fromJson(json['isDraftMedia']), + reuploadRequestedBy: + serializer.fromJson?>(json['reuploadRequestedBy']), + displayLimitInMilliseconds: + serializer.fromJson(json['displayLimitInMilliseconds']), + removeAudio: serializer.fromJson(json['removeAudio']), + downloadToken: serializer.fromJson(json['downloadToken']), + encryptionKey: serializer.fromJson(json['encryptionKey']), + encryptionMac: serializer.fromJson(json['encryptionMac']), + encryptionNonce: serializer.fromJson(json['encryptionNonce']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'mediaId': serializer.toJson(mediaId), + 'type': serializer + .toJson($MediaFilesTable.$convertertype.toJson(type)), + 'uploadState': serializer.toJson( + $MediaFilesTable.$converteruploadStaten.toJson(uploadState)), + 'downloadState': serializer.toJson( + $MediaFilesTable.$converterdownloadStaten.toJson(downloadState)), + 'requiresAuthentication': serializer.toJson(requiresAuthentication), + 'reopenByContact': serializer.toJson(reopenByContact), + 'stored': serializer.toJson(stored), + 'isDraftMedia': serializer.toJson(isDraftMedia), + 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), + 'displayLimitInMilliseconds': + serializer.toJson(displayLimitInMilliseconds), + 'removeAudio': serializer.toJson(removeAudio), + 'downloadToken': serializer.toJson(downloadToken), + 'encryptionKey': serializer.toJson(encryptionKey), + 'encryptionMac': serializer.toJson(encryptionMac), + 'encryptionNonce': serializer.toJson(encryptionNonce), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MediaFile copyWith( + {String? mediaId, + MediaType? type, + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + bool? requiresAuthentication, + bool? reopenByContact, + bool? stored, + bool? isDraftMedia, + Value?> reuploadRequestedBy = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + DateTime? createdAt}) => + MediaFile( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState.present ? uploadState.value : this.uploadState, + downloadState: + downloadState.present ? downloadState.value : this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + reopenByContact: reopenByContact ?? this.reopenByContact, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy.present + ? reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds.present + ? displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: removeAudio.present ? removeAudio.value : this.removeAudio, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, + encryptionKey: + encryptionKey.present ? encryptionKey.value : this.encryptionKey, + encryptionMac: + encryptionMac.present ? encryptionMac.value : this.encryptionMac, + encryptionNonce: encryptionNonce.present + ? encryptionNonce.value + : this.encryptionNonce, + createdAt: createdAt ?? this.createdAt, + ); + MediaFile copyWithCompanion(MediaFilesCompanion data) { + return MediaFile( + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + type: data.type.present ? data.type.value : this.type, + uploadState: + data.uploadState.present ? data.uploadState.value : this.uploadState, + downloadState: data.downloadState.present + ? data.downloadState.value + : this.downloadState, + requiresAuthentication: data.requiresAuthentication.present + ? data.requiresAuthentication.value + : this.requiresAuthentication, + reopenByContact: data.reopenByContact.present + ? data.reopenByContact.value + : this.reopenByContact, + stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, + reuploadRequestedBy: data.reuploadRequestedBy.present + ? data.reuploadRequestedBy.value + : this.reuploadRequestedBy, + displayLimitInMilliseconds: data.displayLimitInMilliseconds.present + ? data.displayLimitInMilliseconds.value + : this.displayLimitInMilliseconds, + removeAudio: + data.removeAudio.present ? data.removeAudio.value : this.removeAudio, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + encryptionKey: data.encryptionKey.present + ? data.encryptionKey.value + : this.encryptionKey, + encryptionMac: data.encryptionMac.present + ? data.encryptionMac.value + : this.encryptionMac, + encryptionNonce: data.encryptionNonce.present + ? data.encryptionNonce.value + : this.encryptionNonce, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MediaFile(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('reopenByContact: $reopenByContact, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + mediaId, + type, + uploadState, + downloadState, + requiresAuthentication, + reopenByContact, + stored, + isDraftMedia, + reuploadRequestedBy, + displayLimitInMilliseconds, + removeAudio, + $driftBlobEquality.hash(downloadToken), + $driftBlobEquality.hash(encryptionKey), + $driftBlobEquality.hash(encryptionMac), + $driftBlobEquality.hash(encryptionNonce), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MediaFile && + other.mediaId == this.mediaId && + other.type == this.type && + other.uploadState == this.uploadState && + other.downloadState == this.downloadState && + other.requiresAuthentication == this.requiresAuthentication && + other.reopenByContact == this.reopenByContact && + other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && + other.reuploadRequestedBy == this.reuploadRequestedBy && + other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && + other.removeAudio == this.removeAudio && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + $driftBlobEquality.equals(other.encryptionKey, this.encryptionKey) && + $driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) && + $driftBlobEquality.equals( + other.encryptionNonce, this.encryptionNonce) && + other.createdAt == this.createdAt); +} + +class MediaFilesCompanion extends UpdateCompanion { + final Value mediaId; + final Value type; + final Value uploadState; + final Value downloadState; + final Value requiresAuthentication; + final Value reopenByContact; + final Value stored; + final Value isDraftMedia; + final Value?> reuploadRequestedBy; + final Value displayLimitInMilliseconds; + final Value removeAudio; + final Value downloadToken; + final Value encryptionKey; + final Value encryptionMac; + final Value encryptionNonce; + final Value createdAt; + final Value rowid; + const MediaFilesCompanion({ + this.mediaId = const Value.absent(), + this.type = const Value.absent(), + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.reopenByContact = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MediaFilesCompanion.insert({ + required String mediaId, + required MediaType type, + this.uploadState = const Value.absent(), + this.downloadState = const Value.absent(), + this.requiresAuthentication = const Value.absent(), + this.reopenByContact = const Value.absent(), + this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), + this.reuploadRequestedBy = const Value.absent(), + this.displayLimitInMilliseconds = const Value.absent(), + this.removeAudio = const Value.absent(), + this.downloadToken = const Value.absent(), + this.encryptionKey = const Value.absent(), + this.encryptionMac = const Value.absent(), + this.encryptionNonce = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : mediaId = Value(mediaId), + type = Value(type); + static Insertable custom({ + Expression? mediaId, + Expression? type, + Expression? uploadState, + Expression? downloadState, + Expression? requiresAuthentication, + Expression? reopenByContact, + Expression? stored, + Expression? isDraftMedia, + Expression? reuploadRequestedBy, + Expression? displayLimitInMilliseconds, + Expression? removeAudio, + Expression? downloadToken, + Expression? encryptionKey, + Expression? encryptionMac, + Expression? encryptionNonce, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (mediaId != null) 'media_id': mediaId, + if (type != null) 'type': type, + if (uploadState != null) 'upload_state': uploadState, + if (downloadState != null) 'download_state': downloadState, + if (requiresAuthentication != null) + 'requires_authentication': requiresAuthentication, + if (reopenByContact != null) 'reopen_by_contact': reopenByContact, + if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, + if (reuploadRequestedBy != null) + 'reupload_requested_by': reuploadRequestedBy, + if (displayLimitInMilliseconds != null) + 'display_limit_in_milliseconds': displayLimitInMilliseconds, + if (removeAudio != null) 'remove_audio': removeAudio, + if (downloadToken != null) 'download_token': downloadToken, + if (encryptionKey != null) 'encryption_key': encryptionKey, + if (encryptionMac != null) 'encryption_mac': encryptionMac, + if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MediaFilesCompanion copyWith( + {Value? mediaId, + Value? type, + Value? uploadState, + Value? downloadState, + Value? requiresAuthentication, + Value? reopenByContact, + Value? stored, + Value? isDraftMedia, + Value?>? reuploadRequestedBy, + Value? displayLimitInMilliseconds, + Value? removeAudio, + Value? downloadToken, + Value? encryptionKey, + Value? encryptionMac, + Value? encryptionNonce, + Value? createdAt, + Value? rowid}) { + return MediaFilesCompanion( + mediaId: mediaId ?? this.mediaId, + type: type ?? this.type, + uploadState: uploadState ?? this.uploadState, + downloadState: downloadState ?? this.downloadState, + requiresAuthentication: + requiresAuthentication ?? this.requiresAuthentication, + reopenByContact: reopenByContact ?? this.reopenByContact, + stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, + displayLimitInMilliseconds: + displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, + removeAudio: removeAudio ?? this.removeAudio, + downloadToken: downloadToken ?? this.downloadToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + encryptionMac: encryptionMac ?? this.encryptionMac, + encryptionNonce: encryptionNonce ?? this.encryptionNonce, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (type.present) { + map['type'] = + Variable($MediaFilesTable.$convertertype.toSql(type.value)); + } + if (uploadState.present) { + map['upload_state'] = Variable( + $MediaFilesTable.$converteruploadStaten.toSql(uploadState.value)); + } + if (downloadState.present) { + map['download_state'] = Variable( + $MediaFilesTable.$converterdownloadStaten.toSql(downloadState.value)); + } + if (requiresAuthentication.present) { + map['requires_authentication'] = + Variable(requiresAuthentication.value); + } + if (reopenByContact.present) { + map['reopen_by_contact'] = Variable(reopenByContact.value); + } + if (stored.present) { + map['stored'] = Variable(stored.value); + } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable(isDraftMedia.value); + } + if (reuploadRequestedBy.present) { + map['reupload_requested_by'] = Variable($MediaFilesTable + .$converterreuploadRequestedByn + .toSql(reuploadRequestedBy.value)); + } + if (displayLimitInMilliseconds.present) { + map['display_limit_in_milliseconds'] = + Variable(displayLimitInMilliseconds.value); + } + if (removeAudio.present) { + map['remove_audio'] = Variable(removeAudio.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (encryptionKey.present) { + map['encryption_key'] = Variable(encryptionKey.value); + } + if (encryptionMac.present) { + map['encryption_mac'] = Variable(encryptionMac.value); + } + if (encryptionNonce.present) { + map['encryption_nonce'] = Variable(encryptionNonce.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MediaFilesCompanion(') + ..write('mediaId: $mediaId, ') + ..write('type: $type, ') + ..write('uploadState: $uploadState, ') + ..write('downloadState: $downloadState, ') + ..write('requiresAuthentication: $requiresAuthentication, ') + ..write('reopenByContact: $reopenByContact, ') + ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') + ..write('reuploadRequestedBy: $reuploadRequestedBy, ') + ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') + ..write('removeAudio: $removeAudio, ') + ..write('downloadToken: $downloadToken, ') + ..write('encryptionKey: $encryptionKey, ') + ..write('encryptionMac: $encryptionMac, ') + ..write('encryptionNonce: $encryptionNonce, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessagesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderIdMeta = + const VerificationMeta('senderId'); + @override + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($MessagesTable.$convertertype); + static const VerificationMeta _contentMeta = + const VerificationMeta('content'); + @override + late final GeneratedColumn content = GeneratedColumn( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _mediaIdMeta = + const VerificationMeta('mediaId'); + @override + late final GeneratedColumn mediaId = GeneratedColumn( + 'media_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES media_files (media_id) ON DELETE SET NULL')); + static const VerificationMeta _mediaStoredMeta = + const VerificationMeta('mediaStored'); + @override + late final GeneratedColumn mediaStored = GeneratedColumn( + 'media_stored', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("media_stored" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _downloadTokenMeta = + const VerificationMeta('downloadToken'); + @override + late final GeneratedColumn downloadToken = + GeneratedColumn('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _quotesMessageIdMeta = + const VerificationMeta('quotesMessageId'); + @override + late final GeneratedColumn quotesMessageId = GeneratedColumn( + 'quotes_message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _isDeletedFromSenderMeta = + const VerificationMeta('isDeletedFromSender'); + @override + late final GeneratedColumn isDeletedFromSender = GeneratedColumn( + 'is_deleted_from_sender', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_deleted_from_sender" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _openedAtMeta = + const VerificationMeta('openedAt'); + @override + late final GeneratedColumn openedAt = GeneratedColumn( + 'opened_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _openedByAllMeta = + const VerificationMeta('openedByAll'); + @override + late final GeneratedColumn openedByAll = GeneratedColumn( + 'opened_by_all', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _modifiedAtMeta = + const VerificationMeta('modifiedAt'); + @override + late final GeneratedColumn modifiedAt = GeneratedColumn( + 'modified_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _ackByUserMeta = + const VerificationMeta('ackByUser'); + @override + late final GeneratedColumn ackByUser = GeneratedColumn( + 'ack_by_user', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _ackByServerMeta = + const VerificationMeta('ackByServer'); + @override + late final GeneratedColumn ackByServer = GeneratedColumn( + 'ack_by_server', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + groupId, + messageId, + senderId, + type, + content, + mediaId, + mediaStored, + downloadToken, + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'messages'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('sender_id')) { + context.handle(_senderIdMeta, + senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta)); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta)); + } + if (data.containsKey('media_id')) { + context.handle(_mediaIdMeta, + mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); + } + if (data.containsKey('media_stored')) { + context.handle( + _mediaStoredMeta, + mediaStored.isAcceptableOrUnknown( + data['media_stored']!, _mediaStoredMeta)); + } + if (data.containsKey('download_token')) { + context.handle( + _downloadTokenMeta, + downloadToken.isAcceptableOrUnknown( + data['download_token']!, _downloadTokenMeta)); + } + if (data.containsKey('quotes_message_id')) { + context.handle( + _quotesMessageIdMeta, + quotesMessageId.isAcceptableOrUnknown( + data['quotes_message_id']!, _quotesMessageIdMeta)); + } + if (data.containsKey('is_deleted_from_sender')) { + context.handle( + _isDeletedFromSenderMeta, + isDeletedFromSender.isAcceptableOrUnknown( + data['is_deleted_from_sender']!, _isDeletedFromSenderMeta)); + } + if (data.containsKey('opened_at')) { + context.handle(_openedAtMeta, + openedAt.isAcceptableOrUnknown(data['opened_at']!, _openedAtMeta)); + } + if (data.containsKey('opened_by_all')) { + context.handle( + _openedByAllMeta, + openedByAll.isAcceptableOrUnknown( + data['opened_by_all']!, _openedByAllMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('modified_at')) { + context.handle( + _modifiedAtMeta, + modifiedAt.isAcceptableOrUnknown( + data['modified_at']!, _modifiedAtMeta)); + } + if (data.containsKey('ack_by_user')) { + context.handle( + _ackByUserMeta, + ackByUser.isAcceptableOrUnknown( + data['ack_by_user']!, _ackByUserMeta)); + } + if (data.containsKey('ack_by_server')) { + context.handle( + _ackByServerMeta, + ackByServer.isAcceptableOrUnknown( + data['ack_by_server']!, _ackByServerMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId}; + @override + Message map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Message( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + type: $MessagesTable.$convertertype.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + mediaId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_id']), + mediaStored: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!, + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), + quotesMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), + isDeletedFromSender: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}is_deleted_from_sender'])!, + openedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_at']), + openedByAll: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}opened_by_all']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + modifiedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), + ackByUser: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_user']), + ackByServer: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server']), + ); + } + + @override + $MessagesTable createAlias(String alias) { + return $MessagesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MessageType.values); +} + +class Message extends DataClass implements Insertable { + final String groupId; + final String messageId; + final int? senderId; + final MessageType type; + final String? content; + final String? mediaId; + final bool mediaStored; + final Uint8List? downloadToken; + final String? quotesMessageId; + final bool isDeletedFromSender; + final DateTime? openedAt; + final DateTime? openedByAll; + final DateTime createdAt; + final DateTime? modifiedAt; + final DateTime? ackByUser; + final DateTime? ackByServer; + const Message( + {required this.groupId, + required this.messageId, + this.senderId, + required this.type, + this.content, + this.mediaId, + required this.mediaStored, + this.downloadToken, + this.quotesMessageId, + required this.isDeletedFromSender, + this.openedAt, + this.openedByAll, + required this.createdAt, + this.modifiedAt, + this.ackByUser, + this.ackByServer}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + { + map['type'] = Variable($MessagesTable.$convertertype.toSql(type)); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + if (!nullToAbsent || mediaId != null) { + map['media_id'] = Variable(mediaId); + } + map['media_stored'] = Variable(mediaStored); + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } + if (!nullToAbsent || quotesMessageId != null) { + map['quotes_message_id'] = Variable(quotesMessageId); + } + map['is_deleted_from_sender'] = Variable(isDeletedFromSender); + if (!nullToAbsent || openedAt != null) { + map['opened_at'] = Variable(openedAt); + } + if (!nullToAbsent || openedByAll != null) { + map['opened_by_all'] = Variable(openedByAll); + } + map['created_at'] = Variable(createdAt); + if (!nullToAbsent || modifiedAt != null) { + map['modified_at'] = Variable(modifiedAt); + } + if (!nullToAbsent || ackByUser != null) { + map['ack_by_user'] = Variable(ackByUser); + } + if (!nullToAbsent || ackByServer != null) { + map['ack_by_server'] = Variable(ackByServer); + } + return map; + } + + MessagesCompanion toCompanion(bool nullToAbsent) { + return MessagesCompanion( + groupId: Value(groupId), + messageId: Value(messageId), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + type: Value(type), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + mediaId: mediaId == null && nullToAbsent + ? const Value.absent() + : Value(mediaId), + mediaStored: Value(mediaStored), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), + quotesMessageId: quotesMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotesMessageId), + isDeletedFromSender: Value(isDeletedFromSender), + openedAt: openedAt == null && nullToAbsent + ? const Value.absent() + : Value(openedAt), + openedByAll: openedByAll == null && nullToAbsent + ? const Value.absent() + : Value(openedByAll), + createdAt: Value(createdAt), + modifiedAt: modifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(modifiedAt), + ackByUser: ackByUser == null && nullToAbsent + ? const Value.absent() + : Value(ackByUser), + ackByServer: ackByServer == null && nullToAbsent + ? const Value.absent() + : Value(ackByServer), + ); + } + + factory Message.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Message( + groupId: serializer.fromJson(json['groupId']), + messageId: serializer.fromJson(json['messageId']), + senderId: serializer.fromJson(json['senderId']), + type: $MessagesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + content: serializer.fromJson(json['content']), + mediaId: serializer.fromJson(json['mediaId']), + mediaStored: serializer.fromJson(json['mediaStored']), + downloadToken: serializer.fromJson(json['downloadToken']), + quotesMessageId: serializer.fromJson(json['quotesMessageId']), + isDeletedFromSender: + serializer.fromJson(json['isDeletedFromSender']), + openedAt: serializer.fromJson(json['openedAt']), + openedByAll: serializer.fromJson(json['openedByAll']), + createdAt: serializer.fromJson(json['createdAt']), + modifiedAt: serializer.fromJson(json['modifiedAt']), + ackByUser: serializer.fromJson(json['ackByUser']), + ackByServer: serializer.fromJson(json['ackByServer']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'messageId': serializer.toJson(messageId), + 'senderId': serializer.toJson(senderId), + 'type': + serializer.toJson($MessagesTable.$convertertype.toJson(type)), + 'content': serializer.toJson(content), + 'mediaId': serializer.toJson(mediaId), + 'mediaStored': serializer.toJson(mediaStored), + 'downloadToken': serializer.toJson(downloadToken), + 'quotesMessageId': serializer.toJson(quotesMessageId), + 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), + 'openedAt': serializer.toJson(openedAt), + 'openedByAll': serializer.toJson(openedByAll), + 'createdAt': serializer.toJson(createdAt), + 'modifiedAt': serializer.toJson(modifiedAt), + 'ackByUser': serializer.toJson(ackByUser), + 'ackByServer': serializer.toJson(ackByServer), + }; + } + + Message copyWith( + {String? groupId, + String? messageId, + Value senderId = const Value.absent(), + MessageType? type, + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + bool? mediaStored, + Value downloadToken = const Value.absent(), + Value quotesMessageId = const Value.absent(), + bool? isDeletedFromSender, + Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), + DateTime? createdAt, + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent()}) => + Message( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId.present ? senderId.value : this.senderId, + type: type ?? this.type, + content: content.present ? content.value : this.content, + mediaId: mediaId.present ? mediaId.value : this.mediaId, + mediaStored: mediaStored ?? this.mediaStored, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, + quotesMessageId: quotesMessageId.present + ? quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt.present ? openedAt.value : this.openedAt, + openedByAll: openedByAll.present ? openedByAll.value : this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, + ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser, + ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer, + ); + Message copyWithCompanion(MessagesCompanion data) { + return Message( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + type: data.type.present ? data.type.value : this.type, + content: data.content.present ? data.content.value : this.content, + mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + mediaStored: + data.mediaStored.present ? data.mediaStored.value : this.mediaStored, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, + quotesMessageId: data.quotesMessageId.present + ? data.quotesMessageId.value + : this.quotesMessageId, + isDeletedFromSender: data.isDeletedFromSender.present + ? data.isDeletedFromSender.value + : this.isDeletedFromSender, + openedAt: data.openedAt.present ? data.openedAt.value : this.openedAt, + openedByAll: + data.openedByAll.present ? data.openedByAll.value : this.openedByAll, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + modifiedAt: + data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, + ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser, + ackByServer: + data.ackByServer.present ? data.ackByServer.value : this.ackByServer, + ); + } + + @override + String toString() { + return (StringBuffer('Message(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('mediaStored: $mediaStored, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupId, + messageId, + senderId, + type, + content, + mediaId, + mediaStored, + $driftBlobEquality.hash(downloadToken), + quotesMessageId, + isDeletedFromSender, + openedAt, + openedByAll, + createdAt, + modifiedAt, + ackByUser, + ackByServer); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Message && + other.groupId == this.groupId && + other.messageId == this.messageId && + other.senderId == this.senderId && + other.type == this.type && + other.content == this.content && + other.mediaId == this.mediaId && + other.mediaStored == this.mediaStored && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && + other.quotesMessageId == this.quotesMessageId && + other.isDeletedFromSender == this.isDeletedFromSender && + other.openedAt == this.openedAt && + other.openedByAll == this.openedByAll && + other.createdAt == this.createdAt && + other.modifiedAt == this.modifiedAt && + other.ackByUser == this.ackByUser && + other.ackByServer == this.ackByServer); +} + +class MessagesCompanion extends UpdateCompanion { + final Value groupId; + final Value messageId; + final Value senderId; + final Value type; + final Value content; + final Value mediaId; + final Value mediaStored; + final Value downloadToken; + final Value quotesMessageId; + final Value isDeletedFromSender; + final Value openedAt; + final Value openedByAll; + final Value createdAt; + final Value modifiedAt; + final Value ackByUser; + final Value ackByServer; + final Value rowid; + const MessagesCompanion({ + this.groupId = const Value.absent(), + this.messageId = const Value.absent(), + this.senderId = const Value.absent(), + this.type = const Value.absent(), + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.mediaStored = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessagesCompanion.insert({ + required String groupId, + required String messageId, + this.senderId = const Value.absent(), + required MessageType type, + this.content = const Value.absent(), + this.mediaId = const Value.absent(), + this.mediaStored = const Value.absent(), + this.downloadToken = const Value.absent(), + this.quotesMessageId = const Value.absent(), + this.isDeletedFromSender = const Value.absent(), + this.openedAt = const Value.absent(), + this.openedByAll = const Value.absent(), + this.createdAt = const Value.absent(), + this.modifiedAt = const Value.absent(), + this.ackByUser = const Value.absent(), + this.ackByServer = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + messageId = Value(messageId), + type = Value(type); + static Insertable custom({ + Expression? groupId, + Expression? messageId, + Expression? senderId, + Expression? type, + Expression? content, + Expression? mediaId, + Expression? mediaStored, + Expression? downloadToken, + Expression? quotesMessageId, + Expression? isDeletedFromSender, + Expression? openedAt, + Expression? openedByAll, + Expression? createdAt, + Expression? modifiedAt, + Expression? ackByUser, + Expression? ackByServer, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (messageId != null) 'message_id': messageId, + if (senderId != null) 'sender_id': senderId, + if (type != null) 'type': type, + if (content != null) 'content': content, + if (mediaId != null) 'media_id': mediaId, + if (mediaStored != null) 'media_stored': mediaStored, + if (downloadToken != null) 'download_token': downloadToken, + if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, + if (isDeletedFromSender != null) + 'is_deleted_from_sender': isDeletedFromSender, + if (openedAt != null) 'opened_at': openedAt, + if (openedByAll != null) 'opened_by_all': openedByAll, + if (createdAt != null) 'created_at': createdAt, + if (modifiedAt != null) 'modified_at': modifiedAt, + if (ackByUser != null) 'ack_by_user': ackByUser, + if (ackByServer != null) 'ack_by_server': ackByServer, + if (rowid != null) 'rowid': rowid, + }); + } + + MessagesCompanion copyWith( + {Value? groupId, + Value? messageId, + Value? senderId, + Value? type, + Value? content, + Value? mediaId, + Value? mediaStored, + Value? downloadToken, + Value? quotesMessageId, + Value? isDeletedFromSender, + Value? openedAt, + Value? openedByAll, + Value? createdAt, + Value? modifiedAt, + Value? ackByUser, + Value? ackByServer, + Value? rowid}) { + return MessagesCompanion( + groupId: groupId ?? this.groupId, + messageId: messageId ?? this.messageId, + senderId: senderId ?? this.senderId, + type: type ?? this.type, + content: content ?? this.content, + mediaId: mediaId ?? this.mediaId, + mediaStored: mediaStored ?? this.mediaStored, + downloadToken: downloadToken ?? this.downloadToken, + quotesMessageId: quotesMessageId ?? this.quotesMessageId, + isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, + openedAt: openedAt ?? this.openedAt, + openedByAll: openedByAll ?? this.openedByAll, + createdAt: createdAt ?? this.createdAt, + modifiedAt: modifiedAt ?? this.modifiedAt, + ackByUser: ackByUser ?? this.ackByUser, + ackByServer: ackByServer ?? this.ackByServer, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (type.present) { + map['type'] = + Variable($MessagesTable.$convertertype.toSql(type.value)); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (mediaId.present) { + map['media_id'] = Variable(mediaId.value); + } + if (mediaStored.present) { + map['media_stored'] = Variable(mediaStored.value); + } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } + if (quotesMessageId.present) { + map['quotes_message_id'] = Variable(quotesMessageId.value); + } + if (isDeletedFromSender.present) { + map['is_deleted_from_sender'] = Variable(isDeletedFromSender.value); + } + if (openedAt.present) { + map['opened_at'] = Variable(openedAt.value); + } + if (openedByAll.present) { + map['opened_by_all'] = Variable(openedByAll.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (modifiedAt.present) { + map['modified_at'] = Variable(modifiedAt.value); + } + if (ackByUser.present) { + map['ack_by_user'] = Variable(ackByUser.value); + } + if (ackByServer.present) { + map['ack_by_server'] = Variable(ackByServer.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessagesCompanion(') + ..write('groupId: $groupId, ') + ..write('messageId: $messageId, ') + ..write('senderId: $senderId, ') + ..write('type: $type, ') + ..write('content: $content, ') + ..write('mediaId: $mediaId, ') + ..write('mediaStored: $mediaStored, ') + ..write('downloadToken: $downloadToken, ') + ..write('quotesMessageId: $quotesMessageId, ') + ..write('isDeletedFromSender: $isDeletedFromSender, ') + ..write('openedAt: $openedAt, ') + ..write('openedByAll: $openedByAll, ') + ..write('createdAt: $createdAt, ') + ..write('modifiedAt: $modifiedAt, ') + ..write('ackByUser: $ackByUser, ') + ..write('ackByServer: $ackByServer, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MessageHistoriesTable extends MessageHistories + with TableInfo<$MessageHistoriesTable, MessageHistory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessageHistoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _contentMeta = + const VerificationMeta('content'); + @override + late final GeneratedColumn content = GeneratedColumn( + 'content', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [id, messageId, contactId, content, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_histories'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } + if (data.containsKey('content')) { + context.handle(_contentMeta, + content.isAcceptableOrUnknown(data['content']!, _contentMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + MessageHistory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageHistory( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $MessageHistoriesTable createAlias(String alias) { + return $MessageHistoriesTable(attachedDatabase, alias); + } +} + +class MessageHistory extends DataClass implements Insertable { + final int id; + final String messageId; + final int? contactId; + final String? content; + final DateTime createdAt; + const MessageHistory( + {required this.id, + required this.messageId, + this.contactId, + this.content, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['message_id'] = Variable(messageId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || content != null) { + map['content'] = Variable(content); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MessageHistoriesCompanion toCompanion(bool nullToAbsent) { + return MessageHistoriesCompanion( + id: Value(id), + messageId: Value(messageId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + content: content == null && nullToAbsent + ? const Value.absent() + : Value(content), + createdAt: Value(createdAt), + ); + } + + factory MessageHistory.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageHistory( + id: serializer.fromJson(json['id']), + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + content: serializer.fromJson(json['content']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'content': serializer.toJson(content), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MessageHistory copyWith( + {int? id, + String? messageId, + Value contactId = const Value.absent(), + Value content = const Value.absent(), + DateTime? createdAt}) => + MessageHistory( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId.present ? contactId.value : this.contactId, + content: content.present ? content.value : this.content, + createdAt: createdAt ?? this.createdAt, + ); + MessageHistory copyWithCompanion(MessageHistoriesCompanion data) { + return MessageHistory( + id: data.id.present ? data.id.value : this.id, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + content: data.content.present ? data.content.value : this.content, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageHistory(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, messageId, contactId, content, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageHistory && + other.id == this.id && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.content == this.content && + other.createdAt == this.createdAt); +} + +class MessageHistoriesCompanion extends UpdateCompanion { + final Value id; + final Value messageId; + final Value contactId; + final Value content; + final Value createdAt; + const MessageHistoriesCompanion({ + this.id = const Value.absent(), + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }); + MessageHistoriesCompanion.insert({ + this.id = const Value.absent(), + required String messageId, + this.contactId = const Value.absent(), + this.content = const Value.absent(), + this.createdAt = const Value.absent(), + }) : messageId = Value(messageId); + static Insertable custom({ + Expression? id, + Expression? messageId, + Expression? contactId, + Expression? content, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (content != null) 'content': content, + if (createdAt != null) 'created_at': createdAt, + }); + } + + MessageHistoriesCompanion copyWith( + {Value? id, + Value? messageId, + Value? contactId, + Value? content, + Value? createdAt}) { + return MessageHistoriesCompanion( + id: id ?? this.id, + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + content: content ?? this.content, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageHistoriesCompanion(') + ..write('id: $id, ') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('content: $content, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $ReactionsTable extends Reactions + with TableInfo<$ReactionsTable, Reaction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReactionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _emojiMeta = const VerificationMeta('emoji'); + @override + late final GeneratedColumn emoji = GeneratedColumn( + 'emoji', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderIdMeta = + const VerificationMeta('senderId'); + @override + late final GeneratedColumn senderId = GeneratedColumn( + 'sender_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [messageId, emoji, senderId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'reactions'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('emoji')) { + context.handle( + _emojiMeta, emoji.isAcceptableOrUnknown(data['emoji']!, _emojiMeta)); + } else if (isInserting) { + context.missing(_emojiMeta); + } + if (data.containsKey('sender_id')) { + context.handle(_senderIdMeta, + senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId, senderId, emoji}; + @override + Reaction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Reaction( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + emoji: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}emoji'])!, + senderId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}sender_id']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReactionsTable createAlias(String alias) { + return $ReactionsTable(attachedDatabase, alias); + } +} + +class Reaction extends DataClass implements Insertable { + final String messageId; + final String emoji; + final int? senderId; + final DateTime createdAt; + const Reaction( + {required this.messageId, + required this.emoji, + this.senderId, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['emoji'] = Variable(emoji); + if (!nullToAbsent || senderId != null) { + map['sender_id'] = Variable(senderId); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReactionsCompanion toCompanion(bool nullToAbsent) { + return ReactionsCompanion( + messageId: Value(messageId), + emoji: Value(emoji), + senderId: senderId == null && nullToAbsent + ? const Value.absent() + : Value(senderId), + createdAt: Value(createdAt), + ); + } + + factory Reaction.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Reaction( + messageId: serializer.fromJson(json['messageId']), + emoji: serializer.fromJson(json['emoji']), + senderId: serializer.fromJson(json['senderId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'emoji': serializer.toJson(emoji), + 'senderId': serializer.toJson(senderId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Reaction copyWith( + {String? messageId, + String? emoji, + Value senderId = const Value.absent(), + DateTime? createdAt}) => + Reaction( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId.present ? senderId.value : this.senderId, + createdAt: createdAt ?? this.createdAt, + ); + Reaction copyWithCompanion(ReactionsCompanion data) { + return Reaction( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + emoji: data.emoji.present ? data.emoji.value : this.emoji, + senderId: data.senderId.present ? data.senderId.value : this.senderId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Reaction(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, emoji, senderId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Reaction && + other.messageId == this.messageId && + other.emoji == this.emoji && + other.senderId == this.senderId && + other.createdAt == this.createdAt); +} + +class ReactionsCompanion extends UpdateCompanion { + final Value messageId; + final Value emoji; + final Value senderId; + final Value createdAt; + final Value rowid; + const ReactionsCompanion({ + this.messageId = const Value.absent(), + this.emoji = const Value.absent(), + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReactionsCompanion.insert({ + required String messageId, + required String emoji, + this.senderId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + emoji = Value(emoji); + static Insertable custom({ + Expression? messageId, + Expression? emoji, + Expression? senderId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (emoji != null) 'emoji': emoji, + if (senderId != null) 'sender_id': senderId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReactionsCompanion copyWith( + {Value? messageId, + Value? emoji, + Value? senderId, + Value? createdAt, + Value? rowid}) { + return ReactionsCompanion( + messageId: messageId ?? this.messageId, + emoji: emoji ?? this.emoji, + senderId: senderId ?? this.senderId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (emoji.present) { + map['emoji'] = Variable(emoji.value); + } + if (senderId.present) { + map['sender_id'] = Variable(senderId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('emoji: $emoji, ') + ..write('senderId: $senderId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupMembersTable extends GroupMembers + with TableInfo<$GroupMembersTable, GroupMember> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupMembersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + @override + late final GeneratedColumnWithTypeConverter + memberState = GeneratedColumn('member_state', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter( + $GroupMembersTable.$convertermemberStaten); + static const VerificationMeta _groupPublicKeyMeta = + const VerificationMeta('groupPublicKey'); + @override + late final GeneratedColumn groupPublicKey = + GeneratedColumn('group_public_key', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); + static const VerificationMeta _lastMessageMeta = + const VerificationMeta('lastMessage'); + @override + late final GeneratedColumn lastMessage = GeneratedColumn( + 'last_message', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [groupId, contactId, memberState, groupPublicKey, lastMessage, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_members'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('group_public_key')) { + context.handle( + _groupPublicKeyMeta, + groupPublicKey.isAcceptableOrUnknown( + data['group_public_key']!, _groupPublicKeyMeta)); + } + if (data.containsKey('last_message')) { + context.handle( + _lastMessageMeta, + lastMessage.isAcceptableOrUnknown( + data['last_message']!, _lastMessageMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupId, contactId}; + @override + GroupMember map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMember( + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + memberState: $GroupMembersTable.$convertermemberStaten.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}member_state'])), + groupPublicKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}group_public_key']), + lastMessage: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_message']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $GroupMembersTable createAlias(String alias) { + return $GroupMembersTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertermemberState = + const EnumNameConverter(MemberState.values); + static JsonTypeConverter2 + $convertermemberStaten = + JsonTypeConverter2.asNullable($convertermemberState); +} + +class GroupMember extends DataClass implements Insertable { + final String groupId; + final int contactId; + final MemberState? memberState; + final Uint8List? groupPublicKey; + final DateTime? lastMessage; + final DateTime createdAt; + const GroupMember( + {required this.groupId, + required this.contactId, + this.memberState, + this.groupPublicKey, + this.lastMessage, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || memberState != null) { + map['member_state'] = Variable( + $GroupMembersTable.$convertermemberStaten.toSql(memberState)); + } + if (!nullToAbsent || groupPublicKey != null) { + map['group_public_key'] = Variable(groupPublicKey); + } + if (!nullToAbsent || lastMessage != null) { + map['last_message'] = Variable(lastMessage); + } + map['created_at'] = Variable(createdAt); + return map; + } + + GroupMembersCompanion toCompanion(bool nullToAbsent) { + return GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(contactId), + memberState: memberState == null && nullToAbsent + ? const Value.absent() + : Value(memberState), + groupPublicKey: groupPublicKey == null && nullToAbsent + ? const Value.absent() + : Value(groupPublicKey), + lastMessage: lastMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastMessage), + createdAt: Value(createdAt), + ); + } + + factory GroupMember.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMember( + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + memberState: $GroupMembersTable.$convertermemberStaten + .fromJson(serializer.fromJson(json['memberState'])), + groupPublicKey: serializer.fromJson(json['groupPublicKey']), + lastMessage: serializer.fromJson(json['lastMessage']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'memberState': serializer.toJson( + $GroupMembersTable.$convertermemberStaten.toJson(memberState)), + 'groupPublicKey': serializer.toJson(groupPublicKey), + 'lastMessage': serializer.toJson(lastMessage), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GroupMember copyWith( + {String? groupId, + int? contactId, + Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), + DateTime? createdAt}) => + GroupMember( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState.present ? memberState.value : this.memberState, + groupPublicKey: + groupPublicKey.present ? groupPublicKey.value : this.groupPublicKey, + lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, + createdAt: createdAt ?? this.createdAt, + ); + GroupMember copyWithCompanion(GroupMembersCompanion data) { + return GroupMember( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + memberState: + data.memberState.present ? data.memberState.value : this.memberState, + groupPublicKey: data.groupPublicKey.present + ? data.groupPublicKey.value + : this.groupPublicKey, + lastMessage: + data.lastMessage.present ? data.lastMessage.value : this.lastMessage, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMember(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, contactId, memberState, + $driftBlobEquality.hash(groupPublicKey), lastMessage, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMember && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.memberState == this.memberState && + $driftBlobEquality.equals( + other.groupPublicKey, this.groupPublicKey) && + other.lastMessage == this.lastMessage && + other.createdAt == this.createdAt); +} + +class GroupMembersCompanion extends UpdateCompanion { + final Value groupId; + final Value contactId; + final Value memberState; + final Value groupPublicKey; + final Value lastMessage; + final Value createdAt; + final Value rowid; + const GroupMembersCompanion({ + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMembersCompanion.insert({ + required String groupId, + required int contactId, + this.memberState = const Value.absent(), + this.groupPublicKey = const Value.absent(), + this.lastMessage = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + contactId = Value(contactId); + static Insertable custom({ + Expression? groupId, + Expression? contactId, + Expression? memberState, + Expression? groupPublicKey, + Expression? lastMessage, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (memberState != null) 'member_state': memberState, + if (groupPublicKey != null) 'group_public_key': groupPublicKey, + if (lastMessage != null) 'last_message': lastMessage, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMembersCompanion copyWith( + {Value? groupId, + Value? contactId, + Value? memberState, + Value? groupPublicKey, + Value? lastMessage, + Value? createdAt, + Value? rowid}) { + return GroupMembersCompanion( + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + memberState: memberState ?? this.memberState, + groupPublicKey: groupPublicKey ?? this.groupPublicKey, + lastMessage: lastMessage ?? this.lastMessage, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (memberState.present) { + map['member_state'] = Variable( + $GroupMembersTable.$convertermemberStaten.toSql(memberState.value)); + } + if (groupPublicKey.present) { + map['group_public_key'] = Variable(groupPublicKey.value); + } + if (lastMessage.present) { + map['last_message'] = Variable(lastMessage.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMembersCompanion(') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('memberState: $memberState, ') + ..write('groupPublicKey: $groupPublicKey, ') + ..write('lastMessage: $lastMessage, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReceiptsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _receiptIdMeta = + const VerificationMeta('receiptId'); + @override + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _messageMeta = + const VerificationMeta('message'); + @override + late final GeneratedColumn message = GeneratedColumn( + 'message', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _contactWillSendsReceiptMeta = + const VerificationMeta('contactWillSendsReceipt'); + @override + late final GeneratedColumn contactWillSendsReceipt = + GeneratedColumn('contact_will_sends_receipt', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("contact_will_sends_receipt" IN (0, 1))'), + defaultValue: const Constant(true)); + static const VerificationMeta _ackByServerAtMeta = + const VerificationMeta('ackByServerAt'); + @override + late final GeneratedColumn ackByServerAt = + GeneratedColumn('ack_by_server_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _retryCountMeta = + const VerificationMeta('retryCount'); + @override + late final GeneratedColumn retryCount = GeneratedColumn( + 'retry_count', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _lastRetryMeta = + const VerificationMeta('lastRetry'); + @override + late final GeneratedColumn lastRetry = GeneratedColumn( + 'last_retry', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + receiptId, + contactId, + messageId, + message, + contactWillSendsReceipt, + ackByServerAt, + retryCount, + lastRetry, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'receipts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('receipt_id')) { + context.handle(_receiptIdMeta, + receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta)); + } else if (isInserting) { + context.missing(_receiptIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } + if (data.containsKey('message')) { + context.handle(_messageMeta, + message.isAcceptableOrUnknown(data['message']!, _messageMeta)); + } else if (isInserting) { + context.missing(_messageMeta); + } + if (data.containsKey('contact_will_sends_receipt')) { + context.handle( + _contactWillSendsReceiptMeta, + contactWillSendsReceipt.isAcceptableOrUnknown( + data['contact_will_sends_receipt']!, + _contactWillSendsReceiptMeta)); + } + if (data.containsKey('ack_by_server_at')) { + context.handle( + _ackByServerAtMeta, + ackByServerAt.isAcceptableOrUnknown( + data['ack_by_server_at']!, _ackByServerAtMeta)); + } + if (data.containsKey('retry_count')) { + context.handle( + _retryCountMeta, + retryCount.isAcceptableOrUnknown( + data['retry_count']!, _retryCountMeta)); + } + if (data.containsKey('last_retry')) { + context.handle(_lastRetryMeta, + lastRetry.isAcceptableOrUnknown(data['last_retry']!, _lastRetryMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {receiptId}; + @override + Receipt map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Receipt( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), + message: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}message'])!, + contactWillSendsReceipt: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}contact_will_sends_receipt'])!, + ackByServerAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']), + retryCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}retry_count'])!, + lastRetry: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_retry']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReceiptsTable createAlias(String alias) { + return $ReceiptsTable(attachedDatabase, alias); + } +} + +class Receipt extends DataClass implements Insertable { + final String receiptId; + final int contactId; + final String? messageId; + + /// This is the protobuf 'Message' + final Uint8List message; + final bool contactWillSendsReceipt; + final DateTime? ackByServerAt; + final int retryCount; + final DateTime? lastRetry; + final DateTime createdAt; + const Receipt( + {required this.receiptId, + required this.contactId, + this.messageId, + required this.message, + required this.contactWillSendsReceipt, + this.ackByServerAt, + required this.retryCount, + this.lastRetry, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['contact_id'] = Variable(contactId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + map['message'] = Variable(message); + map['contact_will_sends_receipt'] = Variable(contactWillSendsReceipt); + if (!nullToAbsent || ackByServerAt != null) { + map['ack_by_server_at'] = Variable(ackByServerAt); + } + map['retry_count'] = Variable(retryCount); + if (!nullToAbsent || lastRetry != null) { + map['last_retry'] = Variable(lastRetry); + } + map['created_at'] = Variable(createdAt); + return map; + } + + ReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(contactId), + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + message: Value(message), + contactWillSendsReceipt: Value(contactWillSendsReceipt), + ackByServerAt: ackByServerAt == null && nullToAbsent + ? const Value.absent() + : Value(ackByServerAt), + retryCount: Value(retryCount), + lastRetry: lastRetry == null && nullToAbsent + ? const Value.absent() + : Value(lastRetry), + createdAt: Value(createdAt), + ); + } + + factory Receipt.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Receipt( + receiptId: serializer.fromJson(json['receiptId']), + contactId: serializer.fromJson(json['contactId']), + messageId: serializer.fromJson(json['messageId']), + message: serializer.fromJson(json['message']), + contactWillSendsReceipt: + serializer.fromJson(json['contactWillSendsReceipt']), + ackByServerAt: serializer.fromJson(json['ackByServerAt']), + retryCount: serializer.fromJson(json['retryCount']), + lastRetry: serializer.fromJson(json['lastRetry']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'contactId': serializer.toJson(contactId), + 'messageId': serializer.toJson(messageId), + 'message': serializer.toJson(message), + 'contactWillSendsReceipt': + serializer.toJson(contactWillSendsReceipt), + 'ackByServerAt': serializer.toJson(ackByServerAt), + 'retryCount': serializer.toJson(retryCount), + 'lastRetry': serializer.toJson(lastRetry), + 'createdAt': serializer.toJson(createdAt), + }; + } + + Receipt copyWith( + {String? receiptId, + int? contactId, + Value messageId = const Value.absent(), + Uint8List? message, + bool? contactWillSendsReceipt, + Value ackByServerAt = const Value.absent(), + int? retryCount, + Value lastRetry = const Value.absent(), + DateTime? createdAt}) => + Receipt( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId.present ? messageId.value : this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + ackByServerAt: + ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry.present ? lastRetry.value : this.lastRetry, + createdAt: createdAt ?? this.createdAt, + ); + Receipt copyWithCompanion(ReceiptsCompanion data) { + return Receipt( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + message: data.message.present ? data.message.value : this.message, + contactWillSendsReceipt: data.contactWillSendsReceipt.present + ? data.contactWillSendsReceipt.value + : this.contactWillSendsReceipt, + ackByServerAt: data.ackByServerAt.present + ? data.ackByServerAt.value + : this.ackByServerAt, + retryCount: + data.retryCount.present ? data.retryCount.value : this.retryCount, + lastRetry: data.lastRetry.present ? data.lastRetry.value : this.lastRetry, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('Receipt(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + receiptId, + contactId, + messageId, + $driftBlobEquality.hash(message), + contactWillSendsReceipt, + ackByServerAt, + retryCount, + lastRetry, + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Receipt && + other.receiptId == this.receiptId && + other.contactId == this.contactId && + other.messageId == this.messageId && + $driftBlobEquality.equals(other.message, this.message) && + other.contactWillSendsReceipt == this.contactWillSendsReceipt && + other.ackByServerAt == this.ackByServerAt && + other.retryCount == this.retryCount && + other.lastRetry == this.lastRetry && + other.createdAt == this.createdAt); +} + +class ReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value contactId; + final Value messageId; + final Value message; + final Value contactWillSendsReceipt; + final Value ackByServerAt; + final Value retryCount; + final Value lastRetry; + final Value createdAt; + final Value rowid; + const ReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.contactId = const Value.absent(), + this.messageId = const Value.absent(), + this.message = const Value.absent(), + this.contactWillSendsReceipt = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceiptsCompanion.insert({ + required String receiptId, + required int contactId, + this.messageId = const Value.absent(), + required Uint8List message, + this.contactWillSendsReceipt = const Value.absent(), + this.ackByServerAt = const Value.absent(), + this.retryCount = const Value.absent(), + this.lastRetry = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId), + contactId = Value(contactId), + message = Value(message); + static Insertable custom({ + Expression? receiptId, + Expression? contactId, + Expression? messageId, + Expression? message, + Expression? contactWillSendsReceipt, + Expression? ackByServerAt, + Expression? retryCount, + Expression? lastRetry, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (contactId != null) 'contact_id': contactId, + if (messageId != null) 'message_id': messageId, + if (message != null) 'message': message, + if (contactWillSendsReceipt != null) + 'contact_will_sends_receipt': contactWillSendsReceipt, + if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, + if (retryCount != null) 'retry_count': retryCount, + if (lastRetry != null) 'last_retry': lastRetry, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceiptsCompanion copyWith( + {Value? receiptId, + Value? contactId, + Value? messageId, + Value? message, + Value? contactWillSendsReceipt, + Value? ackByServerAt, + Value? retryCount, + Value? lastRetry, + Value? createdAt, + Value? rowid}) { + return ReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + contactId: contactId ?? this.contactId, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + contactWillSendsReceipt: + contactWillSendsReceipt ?? this.contactWillSendsReceipt, + ackByServerAt: ackByServerAt ?? this.ackByServerAt, + retryCount: retryCount ?? this.retryCount, + lastRetry: lastRetry ?? this.lastRetry, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (message.present) { + map['message'] = Variable(message.value); + } + if (contactWillSendsReceipt.present) { + map['contact_will_sends_receipt'] = + Variable(contactWillSendsReceipt.value); + } + if (ackByServerAt.present) { + map['ack_by_server_at'] = Variable(ackByServerAt.value); + } + if (retryCount.present) { + map['retry_count'] = Variable(retryCount.value); + } + if (lastRetry.present) { + map['last_retry'] = Variable(lastRetry.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('contactId: $contactId, ') + ..write('messageId: $messageId, ') + ..write('message: $message, ') + ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') + ..write('ackByServerAt: $ackByServerAt, ') + ..write('retryCount: $retryCount, ') + ..write('lastRetry: $lastRetry, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ReceivedReceiptsTable extends ReceivedReceipts + with TableInfo<$ReceivedReceiptsTable, ReceivedReceipt> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ReceivedReceiptsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _receiptIdMeta = + const VerificationMeta('receiptId'); + @override + late final GeneratedColumn receiptId = GeneratedColumn( + 'receipt_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [receiptId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'received_receipts'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('receipt_id')) { + context.handle(_receiptIdMeta, + receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta)); + } else if (isInserting) { + context.missing(_receiptIdMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {receiptId}; + @override + ReceivedReceipt map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ReceivedReceipt( + receiptId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $ReceivedReceiptsTable createAlias(String alias) { + return $ReceivedReceiptsTable(attachedDatabase, alias); + } +} + +class ReceivedReceipt extends DataClass implements Insertable { + final String receiptId; + final DateTime createdAt; + const ReceivedReceipt({required this.receiptId, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['receipt_id'] = Variable(receiptId); + map['created_at'] = Variable(createdAt); + return map; + } + + ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) { + return ReceivedReceiptsCompanion( + receiptId: Value(receiptId), + createdAt: Value(createdAt), + ); + } + + factory ReceivedReceipt.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReceivedReceipt( + receiptId: serializer.fromJson(json['receiptId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'receiptId': serializer.toJson(receiptId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + ReceivedReceipt copyWith({String? receiptId, DateTime? createdAt}) => + ReceivedReceipt( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + ); + ReceivedReceipt copyWithCompanion(ReceivedReceiptsCompanion data) { + return ReceivedReceipt( + receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('ReceivedReceipt(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(receiptId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ReceivedReceipt && + other.receiptId == this.receiptId && + other.createdAt == this.createdAt); +} + +class ReceivedReceiptsCompanion extends UpdateCompanion { + final Value receiptId; + final Value createdAt; + final Value rowid; + const ReceivedReceiptsCompanion({ + this.receiptId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + ReceivedReceiptsCompanion.insert({ + required String receiptId, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : receiptId = Value(receiptId); + static Insertable custom({ + Expression? receiptId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (receiptId != null) 'receipt_id': receiptId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + ReceivedReceiptsCompanion copyWith( + {Value? receiptId, + Value? createdAt, + Value? rowid}) { + return ReceivedReceiptsCompanion( + receiptId: receiptId ?? this.receiptId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (receiptId.present) { + map['receipt_id'] = Variable(receiptId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ReceivedReceiptsCompanion(') + ..write('receiptId: $receiptId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores + with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalIdentityKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _deviceIdMeta = + const VerificationMeta('deviceId'); + @override + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _identityKeyMeta = + const VerificationMeta('identityKey'); + @override + late final GeneratedColumn identityKey = + GeneratedColumn('identity_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [deviceId, name, identityKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_identity_key_stores'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('device_id')) { + context.handle(_deviceIdMeta, + deviceId.isAcceptableOrUnknown(data['device_id']!, _deviceIdMeta)); + } else if (isInserting) { + context.missing(_deviceIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('identity_key')) { + context.handle( + _identityKeyMeta, + identityKey.isAcceptableOrUnknown( + data['identity_key']!, _identityKeyMeta)); + } else if (isInserting) { + context.missing(_identityKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalIdentityKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalIdentityKeyStore( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + identityKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}identity_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalIdentityKeyStoresTable createAlias(String alias) { + return $SignalIdentityKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalIdentityKeyStore extends DataClass + implements Insertable { + final int deviceId; + final String name; + final Uint8List identityKey; + final DateTime createdAt; + const SignalIdentityKeyStore( + {required this.deviceId, + required this.name, + required this.identityKey, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['identity_key'] = Variable(identityKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalIdentityKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalIdentityKeyStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + identityKey: Value(identityKey), + createdAt: Value(createdAt), + ); + } + + factory SignalIdentityKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalIdentityKeyStore( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + identityKey: serializer.fromJson(json['identityKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'identityKey': serializer.toJson(identityKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalIdentityKeyStore copyWith( + {int? deviceId, + String? name, + Uint8List? identityKey, + DateTime? createdAt}) => + SignalIdentityKeyStore( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalIdentityKeyStore copyWithCompanion( + SignalIdentityKeyStoresCompanion data) { + return SignalIdentityKeyStore( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + identityKey: + data.identityKey.present ? data.identityKey.value : this.identityKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStore(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(identityKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalIdentityKeyStore && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.identityKey, this.identityKey) && + other.createdAt == this.createdAt); +} + +class SignalIdentityKeyStoresCompanion + extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value identityKey; + final Value createdAt; + final Value rowid; + const SignalIdentityKeyStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.identityKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalIdentityKeyStoresCompanion.insert({ + required int deviceId, + required String name, + required Uint8List identityKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + identityKey = Value(identityKey); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? identityKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (identityKey != null) 'identity_key': identityKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalIdentityKeyStoresCompanion copyWith( + {Value? deviceId, + Value? name, + Value? identityKey, + Value? createdAt, + Value? rowid}) { + return SignalIdentityKeyStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + identityKey: identityKey ?? this.identityKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (identityKey.present) { + map['identity_key'] = Variable(identityKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalIdentityKeyStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('identityKey: $identityKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalPreKeyStoresTable extends SignalPreKeyStores + with TableInfo<$SignalPreKeyStoresTable, SignalPreKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalPreKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _preKeyIdMeta = + const VerificationMeta('preKeyId'); + @override + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _preKeyMeta = const VerificationMeta('preKey'); + @override + late final GeneratedColumn preKey = GeneratedColumn( + 'pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_pre_key_stores'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('pre_key_id')) { + context.handle(_preKeyIdMeta, + preKeyId.isAcceptableOrUnknown(data['pre_key_id']!, _preKeyIdMeta)); + } + if (data.containsKey('pre_key')) { + context.handle(_preKeyMeta, + preKey.isAcceptableOrUnknown(data['pre_key']!, _preKeyMeta)); + } else if (isInserting) { + context.missing(_preKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {preKeyId}; + @override + SignalPreKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalPreKeyStore( + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalPreKeyStoresTable createAlias(String alias) { + return $SignalPreKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalPreKeyStore extends DataClass + implements Insertable { + final int preKeyId; + final Uint8List preKey; + final DateTime createdAt; + const SignalPreKeyStore( + {required this.preKeyId, required this.preKey, required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalPreKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalPreKeyStoresCompanion( + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalPreKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalPreKeyStore( + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalPreKeyStore copyWith( + {int? preKeyId, Uint8List? preKey, DateTime? createdAt}) => + SignalPreKeyStore( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalPreKeyStore copyWithCompanion(SignalPreKeyStoresCompanion data) { + return SignalPreKeyStore( + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStore(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalPreKeyStore && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalPreKeyStoresCompanion extends UpdateCompanion { + final Value preKeyId; + final Value preKey; + final Value createdAt; + const SignalPreKeyStoresCompanion({ + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalPreKeyStoresCompanion.insert({ + this.preKeyId = const Value.absent(), + required Uint8List preKey, + this.createdAt = const Value.absent(), + }) : preKey = Value(preKey); + static Insertable custom({ + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalPreKeyStoresCompanion copyWith( + {Value? preKeyId, + Value? preKey, + Value? createdAt}) { + return SignalPreKeyStoresCompanion( + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalPreKeyStoresCompanion(') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $SignalSenderKeyStoresTable extends SignalSenderKeyStores + with TableInfo<$SignalSenderKeyStoresTable, SignalSenderKeyStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalSenderKeyStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _senderKeyNameMeta = + const VerificationMeta('senderKeyName'); + @override + late final GeneratedColumn senderKeyName = GeneratedColumn( + 'sender_key_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _senderKeyMeta = + const VerificationMeta('senderKey'); + @override + late final GeneratedColumn senderKey = GeneratedColumn( + 'sender_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + @override + List get $columns => [senderKeyName, senderKey]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_sender_key_stores'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('sender_key_name')) { + context.handle( + _senderKeyNameMeta, + senderKeyName.isAcceptableOrUnknown( + data['sender_key_name']!, _senderKeyNameMeta)); + } else if (isInserting) { + context.missing(_senderKeyNameMeta); + } + if (data.containsKey('sender_key')) { + context.handle(_senderKeyMeta, + senderKey.isAcceptableOrUnknown(data['sender_key']!, _senderKeyMeta)); + } else if (isInserting) { + context.missing(_senderKeyMeta); + } + return context; + } + + @override + Set get $primaryKey => {senderKeyName}; + @override + SignalSenderKeyStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSenderKeyStore( + senderKeyName: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}sender_key_name'])!, + senderKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}sender_key'])!, + ); + } + + @override + $SignalSenderKeyStoresTable createAlias(String alias) { + return $SignalSenderKeyStoresTable(attachedDatabase, alias); + } +} + +class SignalSenderKeyStore extends DataClass + implements Insertable { + final String senderKeyName; + final Uint8List senderKey; + const SignalSenderKeyStore( + {required this.senderKeyName, required this.senderKey}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['sender_key_name'] = Variable(senderKeyName); + map['sender_key'] = Variable(senderKey); + return map; + } + + SignalSenderKeyStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSenderKeyStoresCompanion( + senderKeyName: Value(senderKeyName), + senderKey: Value(senderKey), + ); + } + + factory SignalSenderKeyStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSenderKeyStore( + senderKeyName: serializer.fromJson(json['senderKeyName']), + senderKey: serializer.fromJson(json['senderKey']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'senderKeyName': serializer.toJson(senderKeyName), + 'senderKey': serializer.toJson(senderKey), + }; + } + + SignalSenderKeyStore copyWith( + {String? senderKeyName, Uint8List? senderKey}) => + SignalSenderKeyStore( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + ); + SignalSenderKeyStore copyWithCompanion(SignalSenderKeyStoresCompanion data) { + return SignalSenderKeyStore( + senderKeyName: data.senderKeyName.present + ? data.senderKeyName.value + : this.senderKeyName, + senderKey: data.senderKey.present ? data.senderKey.value : this.senderKey, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStore(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(senderKeyName, $driftBlobEquality.hash(senderKey)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSenderKeyStore && + other.senderKeyName == this.senderKeyName && + $driftBlobEquality.equals(other.senderKey, this.senderKey)); +} + +class SignalSenderKeyStoresCompanion + extends UpdateCompanion { + final Value senderKeyName; + final Value senderKey; + final Value rowid; + const SignalSenderKeyStoresCompanion({ + this.senderKeyName = const Value.absent(), + this.senderKey = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSenderKeyStoresCompanion.insert({ + required String senderKeyName, + required Uint8List senderKey, + this.rowid = const Value.absent(), + }) : senderKeyName = Value(senderKeyName), + senderKey = Value(senderKey); + static Insertable custom({ + Expression? senderKeyName, + Expression? senderKey, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (senderKeyName != null) 'sender_key_name': senderKeyName, + if (senderKey != null) 'sender_key': senderKey, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSenderKeyStoresCompanion copyWith( + {Value? senderKeyName, + Value? senderKey, + Value? rowid}) { + return SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName ?? this.senderKeyName, + senderKey: senderKey ?? this.senderKey, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (senderKeyName.present) { + map['sender_key_name'] = Variable(senderKeyName.value); + } + if (senderKey.present) { + map['sender_key'] = Variable(senderKey.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSenderKeyStoresCompanion(') + ..write('senderKeyName: $senderKeyName, ') + ..write('senderKey: $senderKey, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalSessionStoresTable extends SignalSessionStores + with TableInfo<$SignalSessionStoresTable, SignalSessionStore> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalSessionStoresTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _deviceIdMeta = + const VerificationMeta('deviceId'); + @override + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _sessionRecordMeta = + const VerificationMeta('sessionRecord'); + @override + late final GeneratedColumn sessionRecord = + GeneratedColumn('session_record', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [deviceId, name, sessionRecord, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_session_stores'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('device_id')) { + context.handle(_deviceIdMeta, + deviceId.isAcceptableOrUnknown(data['device_id']!, _deviceIdMeta)); + } else if (isInserting) { + context.missing(_deviceIdMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('session_record')) { + context.handle( + _sessionRecordMeta, + sessionRecord.isAcceptableOrUnknown( + data['session_record']!, _sessionRecordMeta)); + } else if (isInserting) { + context.missing(_sessionRecordMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {deviceId, name}; + @override + SignalSessionStore map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalSessionStore( + deviceId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}device_id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + sessionRecord: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}session_record'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalSessionStoresTable createAlias(String alias) { + return $SignalSessionStoresTable(attachedDatabase, alias); + } +} + +class SignalSessionStore extends DataClass + implements Insertable { + final int deviceId; + final String name; + final Uint8List sessionRecord; + final DateTime createdAt; + const SignalSessionStore( + {required this.deviceId, + required this.name, + required this.sessionRecord, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['device_id'] = Variable(deviceId); + map['name'] = Variable(name); + map['session_record'] = Variable(sessionRecord); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalSessionStoresCompanion toCompanion(bool nullToAbsent) { + return SignalSessionStoresCompanion( + deviceId: Value(deviceId), + name: Value(name), + sessionRecord: Value(sessionRecord), + createdAt: Value(createdAt), + ); + } + + factory SignalSessionStore.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalSessionStore( + deviceId: serializer.fromJson(json['deviceId']), + name: serializer.fromJson(json['name']), + sessionRecord: serializer.fromJson(json['sessionRecord']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'deviceId': serializer.toJson(deviceId), + 'name': serializer.toJson(name), + 'sessionRecord': serializer.toJson(sessionRecord), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalSessionStore copyWith( + {int? deviceId, + String? name, + Uint8List? sessionRecord, + DateTime? createdAt}) => + SignalSessionStore( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + ); + SignalSessionStore copyWithCompanion(SignalSessionStoresCompanion data) { + return SignalSessionStore( + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + name: data.name.present ? data.name.value : this.name, + sessionRecord: data.sessionRecord.present + ? data.sessionRecord.value + : this.sessionRecord, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalSessionStore(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + deviceId, name, $driftBlobEquality.hash(sessionRecord), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalSessionStore && + other.deviceId == this.deviceId && + other.name == this.name && + $driftBlobEquality.equals(other.sessionRecord, this.sessionRecord) && + other.createdAt == this.createdAt); +} + +class SignalSessionStoresCompanion extends UpdateCompanion { + final Value deviceId; + final Value name; + final Value sessionRecord; + final Value createdAt; + final Value rowid; + const SignalSessionStoresCompanion({ + this.deviceId = const Value.absent(), + this.name = const Value.absent(), + this.sessionRecord = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalSessionStoresCompanion.insert({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : deviceId = Value(deviceId), + name = Value(name), + sessionRecord = Value(sessionRecord); + static Insertable custom({ + Expression? deviceId, + Expression? name, + Expression? sessionRecord, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (deviceId != null) 'device_id': deviceId, + if (name != null) 'name': name, + if (sessionRecord != null) 'session_record': sessionRecord, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalSessionStoresCompanion copyWith( + {Value? deviceId, + Value? name, + Value? sessionRecord, + Value? createdAt, + Value? rowid}) { + return SignalSessionStoresCompanion( + deviceId: deviceId ?? this.deviceId, + name: name ?? this.name, + sessionRecord: sessionRecord ?? this.sessionRecord, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (sessionRecord.present) { + map['session_record'] = Variable(sessionRecord.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalSessionStoresCompanion(') + ..write('deviceId: $deviceId, ') + ..write('name: $name, ') + ..write('sessionRecord: $sessionRecord, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalContactPreKeysTable extends SignalContactPreKeys + with TableInfo<$SignalContactPreKeysTable, SignalContactPreKey> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalContactPreKeysTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _preKeyIdMeta = + const VerificationMeta('preKeyId'); + @override + late final GeneratedColumn preKeyId = GeneratedColumn( + 'pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _preKeyMeta = const VerificationMeta('preKey'); + @override + late final GeneratedColumn preKey = GeneratedColumn( + 'pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => + [contactId, preKeyId, preKey, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_pre_keys'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('pre_key_id')) { + context.handle(_preKeyIdMeta, + preKeyId.isAcceptableOrUnknown(data['pre_key_id']!, _preKeyIdMeta)); + } else if (isInserting) { + context.missing(_preKeyIdMeta); + } + if (data.containsKey('pre_key')) { + context.handle(_preKeyMeta, + preKey.isAcceptableOrUnknown(data['pre_key']!, _preKeyMeta)); + } else if (isInserting) { + context.missing(_preKeyMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {contactId, preKeyId}; + @override + SignalContactPreKey map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactPreKey( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + preKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}pre_key_id'])!, + preKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}pre_key'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalContactPreKeysTable createAlias(String alias) { + return $SignalContactPreKeysTable(attachedDatabase, alias); + } +} + +class SignalContactPreKey extends DataClass + implements Insertable { + final int contactId; + final int preKeyId; + final Uint8List preKey; + final DateTime createdAt; + const SignalContactPreKey( + {required this.contactId, + required this.preKeyId, + required this.preKey, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['contact_id'] = Variable(contactId); + map['pre_key_id'] = Variable(preKeyId); + map['pre_key'] = Variable(preKey); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalContactPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactPreKeysCompanion( + contactId: Value(contactId), + preKeyId: Value(preKeyId), + preKey: Value(preKey), + createdAt: Value(createdAt), + ); + } + + factory SignalContactPreKey.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactPreKey( + contactId: serializer.fromJson(json['contactId']), + preKeyId: serializer.fromJson(json['preKeyId']), + preKey: serializer.fromJson(json['preKey']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'contactId': serializer.toJson(contactId), + 'preKeyId': serializer.toJson(preKeyId), + 'preKey': serializer.toJson(preKey), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalContactPreKey copyWith( + {int? contactId, + int? preKeyId, + Uint8List? preKey, + DateTime? createdAt}) => + SignalContactPreKey( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactPreKey copyWithCompanion(SignalContactPreKeysCompanion data) { + return SignalContactPreKey( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + preKeyId: data.preKeyId.present ? data.preKeyId.value : this.preKeyId, + preKey: data.preKey.present ? data.preKey.value : this.preKey, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKey(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, preKeyId, $driftBlobEquality.hash(preKey), createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactPreKey && + other.contactId == this.contactId && + other.preKeyId == this.preKeyId && + $driftBlobEquality.equals(other.preKey, this.preKey) && + other.createdAt == this.createdAt); +} + +class SignalContactPreKeysCompanion + extends UpdateCompanion { + final Value contactId; + final Value preKeyId; + final Value preKey; + final Value createdAt; + final Value rowid; + const SignalContactPreKeysCompanion({ + this.contactId = const Value.absent(), + this.preKeyId = const Value.absent(), + this.preKey = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + SignalContactPreKeysCompanion.insert({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : contactId = Value(contactId), + preKeyId = Value(preKeyId), + preKey = Value(preKey); + static Insertable custom({ + Expression? contactId, + Expression? preKeyId, + Expression? preKey, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (preKeyId != null) 'pre_key_id': preKeyId, + if (preKey != null) 'pre_key': preKey, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + SignalContactPreKeysCompanion copyWith( + {Value? contactId, + Value? preKeyId, + Value? preKey, + Value? createdAt, + Value? rowid}) { + return SignalContactPreKeysCompanion( + contactId: contactId ?? this.contactId, + preKeyId: preKeyId ?? this.preKeyId, + preKey: preKey ?? this.preKey, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (preKeyId.present) { + map['pre_key_id'] = Variable(preKeyId.value); + } + if (preKey.present) { + map['pre_key'] = Variable(preKey.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('preKeyId: $preKeyId, ') + ..write('preKey: $preKey, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SignalContactSignedPreKeysTable extends SignalContactSignedPreKeys + with + TableInfo<$SignalContactSignedPreKeysTable, SignalContactSignedPreKey> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SignalContactSignedPreKeysTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES contacts (user_id) ON DELETE CASCADE')); + static const VerificationMeta _signedPreKeyIdMeta = + const VerificationMeta('signedPreKeyId'); + @override + late final GeneratedColumn signedPreKeyId = GeneratedColumn( + 'signed_pre_key_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _signedPreKeyMeta = + const VerificationMeta('signedPreKey'); + @override + late final GeneratedColumn signedPreKey = + GeneratedColumn('signed_pre_key', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _signedPreKeySignatureMeta = + const VerificationMeta('signedPreKeySignature'); + @override + late final GeneratedColumn signedPreKeySignature = + GeneratedColumn('signed_pre_key_signature', aliasedName, false, + type: DriftSqlType.blob, requiredDuringInsert: true); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + contactId, + signedPreKeyId, + signedPreKey, + signedPreKeySignature, + createdAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'signal_contact_signed_pre_keys'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } + if (data.containsKey('signed_pre_key_id')) { + context.handle( + _signedPreKeyIdMeta, + signedPreKeyId.isAcceptableOrUnknown( + data['signed_pre_key_id']!, _signedPreKeyIdMeta)); + } else if (isInserting) { + context.missing(_signedPreKeyIdMeta); + } + if (data.containsKey('signed_pre_key')) { + context.handle( + _signedPreKeyMeta, + signedPreKey.isAcceptableOrUnknown( + data['signed_pre_key']!, _signedPreKeyMeta)); + } else if (isInserting) { + context.missing(_signedPreKeyMeta); + } + if (data.containsKey('signed_pre_key_signature')) { + context.handle( + _signedPreKeySignatureMeta, + signedPreKeySignature.isAcceptableOrUnknown( + data['signed_pre_key_signature']!, _signedPreKeySignatureMeta)); + } else if (isInserting) { + context.missing(_signedPreKeySignatureMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {contactId}; + @override + SignalContactSignedPreKey map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SignalContactSignedPreKey( + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + signedPreKeyId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}signed_pre_key_id'])!, + signedPreKey: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}signed_pre_key'])!, + signedPreKeySignature: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}signed_pre_key_signature'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + ); + } + + @override + $SignalContactSignedPreKeysTable createAlias(String alias) { + return $SignalContactSignedPreKeysTable(attachedDatabase, alias); + } +} + +class SignalContactSignedPreKey extends DataClass + implements Insertable { + final int contactId; + final int signedPreKeyId; + final Uint8List signedPreKey; + final Uint8List signedPreKeySignature; + final DateTime createdAt; + const SignalContactSignedPreKey( + {required this.contactId, + required this.signedPreKeyId, + required this.signedPreKey, + required this.signedPreKeySignature, + required this.createdAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['contact_id'] = Variable(contactId); + map['signed_pre_key_id'] = Variable(signedPreKeyId); + map['signed_pre_key'] = Variable(signedPreKey); + map['signed_pre_key_signature'] = + Variable(signedPreKeySignature); + map['created_at'] = Variable(createdAt); + return map; + } + + SignalContactSignedPreKeysCompanion toCompanion(bool nullToAbsent) { + return SignalContactSignedPreKeysCompanion( + contactId: Value(contactId), + signedPreKeyId: Value(signedPreKeyId), + signedPreKey: Value(signedPreKey), + signedPreKeySignature: Value(signedPreKeySignature), + createdAt: Value(createdAt), + ); + } + + factory SignalContactSignedPreKey.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SignalContactSignedPreKey( + contactId: serializer.fromJson(json['contactId']), + signedPreKeyId: serializer.fromJson(json['signedPreKeyId']), + signedPreKey: serializer.fromJson(json['signedPreKey']), + signedPreKeySignature: + serializer.fromJson(json['signedPreKeySignature']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'contactId': serializer.toJson(contactId), + 'signedPreKeyId': serializer.toJson(signedPreKeyId), + 'signedPreKey': serializer.toJson(signedPreKey), + 'signedPreKeySignature': + serializer.toJson(signedPreKeySignature), + 'createdAt': serializer.toJson(createdAt), + }; + } + + SignalContactSignedPreKey copyWith( + {int? contactId, + int? signedPreKeyId, + Uint8List? signedPreKey, + Uint8List? signedPreKeySignature, + DateTime? createdAt}) => + SignalContactSignedPreKey( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + SignalContactSignedPreKey copyWithCompanion( + SignalContactSignedPreKeysCompanion data) { + return SignalContactSignedPreKey( + contactId: data.contactId.present ? data.contactId.value : this.contactId, + signedPreKeyId: data.signedPreKeyId.present + ? data.signedPreKeyId.value + : this.signedPreKeyId, + signedPreKey: data.signedPreKey.present + ? data.signedPreKey.value + : this.signedPreKey, + signedPreKeySignature: data.signedPreKeySignature.present + ? data.signedPreKeySignature.value + : this.signedPreKeySignature, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKey(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + contactId, + signedPreKeyId, + $driftBlobEquality.hash(signedPreKey), + $driftBlobEquality.hash(signedPreKeySignature), + createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SignalContactSignedPreKey && + other.contactId == this.contactId && + other.signedPreKeyId == this.signedPreKeyId && + $driftBlobEquality.equals(other.signedPreKey, this.signedPreKey) && + $driftBlobEquality.equals( + other.signedPreKeySignature, this.signedPreKeySignature) && + other.createdAt == this.createdAt); +} + +class SignalContactSignedPreKeysCompanion + extends UpdateCompanion { + final Value contactId; + final Value signedPreKeyId; + final Value signedPreKey; + final Value signedPreKeySignature; + final Value createdAt; + const SignalContactSignedPreKeysCompanion({ + this.contactId = const Value.absent(), + this.signedPreKeyId = const Value.absent(), + this.signedPreKey = const Value.absent(), + this.signedPreKeySignature = const Value.absent(), + this.createdAt = const Value.absent(), + }); + SignalContactSignedPreKeysCompanion.insert({ + this.contactId = const Value.absent(), + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + this.createdAt = const Value.absent(), + }) : signedPreKeyId = Value(signedPreKeyId), + signedPreKey = Value(signedPreKey), + signedPreKeySignature = Value(signedPreKeySignature); + static Insertable custom({ + Expression? contactId, + Expression? signedPreKeyId, + Expression? signedPreKey, + Expression? signedPreKeySignature, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (contactId != null) 'contact_id': contactId, + if (signedPreKeyId != null) 'signed_pre_key_id': signedPreKeyId, + if (signedPreKey != null) 'signed_pre_key': signedPreKey, + if (signedPreKeySignature != null) + 'signed_pre_key_signature': signedPreKeySignature, + if (createdAt != null) 'created_at': createdAt, + }); + } + + SignalContactSignedPreKeysCompanion copyWith( + {Value? contactId, + Value? signedPreKeyId, + Value? signedPreKey, + Value? signedPreKeySignature, + Value? createdAt}) { + return SignalContactSignedPreKeysCompanion( + contactId: contactId ?? this.contactId, + signedPreKeyId: signedPreKeyId ?? this.signedPreKeyId, + signedPreKey: signedPreKey ?? this.signedPreKey, + signedPreKeySignature: + signedPreKeySignature ?? this.signedPreKeySignature, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (signedPreKeyId.present) { + map['signed_pre_key_id'] = Variable(signedPreKeyId.value); + } + if (signedPreKey.present) { + map['signed_pre_key'] = Variable(signedPreKey.value); + } + if (signedPreKeySignature.present) { + map['signed_pre_key_signature'] = + Variable(signedPreKeySignature.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SignalContactSignedPreKeysCompanion(') + ..write('contactId: $contactId, ') + ..write('signedPreKeyId: $signedPreKeyId, ') + ..write('signedPreKey: $signedPreKey, ') + ..write('signedPreKeySignature: $signedPreKeySignature, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class $MessageActionsTable extends MessageActions + with TableInfo<$MessageActionsTable, MessageAction> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MessageActionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); + @override + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (message_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter( + $MessageActionsTable.$convertertype); + static const VerificationMeta _actionAtMeta = + const VerificationMeta('actionAt'); + @override + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [messageId, contactId, type, actionAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'message_actions'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } else if (isInserting) { + context.missing(_contactIdMeta); + } + if (data.containsKey('action_at')) { + context.handle(_actionAtMeta, + actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {messageId, contactId, type}; + @override + MessageAction map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MessageAction( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id'])!, + type: $MessageActionsTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + $MessageActionsTable createAlias(String alias) { + return $MessageActionsTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(MessageActionType.values); +} + +class MessageAction extends DataClass implements Insertable { + final String messageId; + final int contactId; + final MessageActionType type; + final DateTime actionAt; + const MessageAction( + {required this.messageId, + required this.contactId, + required this.type, + required this.actionAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['contact_id'] = Variable(contactId); + { + map['type'] = + Variable($MessageActionsTable.$convertertype.toSql(type)); + } + map['action_at'] = Variable(actionAt); + return map; + } + + MessageActionsCompanion toCompanion(bool nullToAbsent) { + return MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory MessageAction.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MessageAction( + messageId: serializer.fromJson(json['messageId']), + contactId: serializer.fromJson(json['contactId']), + type: $MessageActionsTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'contactId': serializer.toJson(contactId), + 'type': serializer + .toJson($MessageActionsTable.$convertertype.toJson(type)), + 'actionAt': serializer.toJson(actionAt), + }; + } + + MessageAction copyWith( + {String? messageId, + int? contactId, + MessageActionType? type, + DateTime? actionAt}) => + MessageAction( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + MessageAction copyWithCompanion(MessageActionsCompanion data) { + return MessageAction( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('MessageAction(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, contactId, type, actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MessageAction && + other.messageId == this.messageId && + other.contactId == this.contactId && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class MessageActionsCompanion extends UpdateCompanion { + final Value messageId; + final Value contactId; + final Value type; + final Value actionAt; + final Value rowid; + const MessageActionsCompanion({ + this.messageId = const Value.absent(), + this.contactId = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MessageActionsCompanion.insert({ + required String messageId, + required int contactId, + required MessageActionType type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + contactId = Value(contactId), + type = Value(type); + static Insertable custom({ + Expression? messageId, + Expression? contactId, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (contactId != null) 'contact_id': contactId, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MessageActionsCompanion copyWith( + {Value? messageId, + Value? contactId, + Value? type, + Value? actionAt, + Value? rowid}) { + return MessageActionsCompanion( + messageId: messageId ?? this.messageId, + contactId: contactId ?? this.contactId, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (type.present) { + map['type'] = Variable( + $MessageActionsTable.$convertertype.toSql(type.value)); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MessageActionsCompanion(') + ..write('messageId: $messageId, ') + ..write('contactId: $contactId, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupHistoriesTable extends GroupHistories + with TableInfo<$GroupHistoriesTable, GroupHistory> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupHistoriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupHistoryIdMeta = + const VerificationMeta('groupHistoryId'); + @override + late final GeneratedColumn groupHistoryId = GeneratedColumn( + 'group_history_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _groupIdMeta = + const VerificationMeta('groupId'); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES "groups" (group_id) ON DELETE CASCADE')); + static const VerificationMeta _contactIdMeta = + const VerificationMeta('contactId'); + @override + late final GeneratedColumn contactId = GeneratedColumn( + 'contact_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + static const VerificationMeta _affectedContactIdMeta = + const VerificationMeta('affectedContactId'); + @override + late final GeneratedColumn affectedContactId = GeneratedColumn( + 'affected_contact_id', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)')); + static const VerificationMeta _oldGroupNameMeta = + const VerificationMeta('oldGroupName'); + @override + late final GeneratedColumn oldGroupName = GeneratedColumn( + 'old_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _newGroupNameMeta = + const VerificationMeta('newGroupName'); + @override + late final GeneratedColumn newGroupName = GeneratedColumn( + 'new_group_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _newDeleteMessagesAfterMillisecondsMeta = + const VerificationMeta('newDeleteMessagesAfterMilliseconds'); + @override + late final GeneratedColumn newDeleteMessagesAfterMilliseconds = + GeneratedColumn( + 'new_delete_messages_after_milliseconds', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($GroupHistoriesTable.$convertertype); + static const VerificationMeta _actionAtMeta = + const VerificationMeta('actionAt'); + @override + late final GeneratedColumn actionAt = GeneratedColumn( + 'action_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + List get $columns => [ + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_histories'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_history_id')) { + context.handle( + _groupHistoryIdMeta, + groupHistoryId.isAcceptableOrUnknown( + data['group_history_id']!, _groupHistoryIdMeta)); + } else if (isInserting) { + context.missing(_groupHistoryIdMeta); + } + if (data.containsKey('group_id')) { + context.handle(_groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta)); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('contact_id')) { + context.handle(_contactIdMeta, + contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta)); + } + if (data.containsKey('affected_contact_id')) { + context.handle( + _affectedContactIdMeta, + affectedContactId.isAcceptableOrUnknown( + data['affected_contact_id']!, _affectedContactIdMeta)); + } + if (data.containsKey('old_group_name')) { + context.handle( + _oldGroupNameMeta, + oldGroupName.isAcceptableOrUnknown( + data['old_group_name']!, _oldGroupNameMeta)); + } + if (data.containsKey('new_group_name')) { + context.handle( + _newGroupNameMeta, + newGroupName.isAcceptableOrUnknown( + data['new_group_name']!, _newGroupNameMeta)); + } + if (data.containsKey('new_delete_messages_after_milliseconds')) { + context.handle( + _newDeleteMessagesAfterMillisecondsMeta, + newDeleteMessagesAfterMilliseconds.isAcceptableOrUnknown( + data['new_delete_messages_after_milliseconds']!, + _newDeleteMessagesAfterMillisecondsMeta)); + } + if (data.containsKey('action_at')) { + context.handle(_actionAtMeta, + actionAt.isAcceptableOrUnknown(data['action_at']!, _actionAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {groupHistoryId}; + @override + GroupHistory map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupHistory( + groupHistoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}group_history_id'])!, + groupId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}group_id'])!, + contactId: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}contact_id']), + affectedContactId: attachedDatabase.typeMapping.read( + DriftSqlType.int, data['${effectivePrefix}affected_contact_id']), + oldGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}old_group_name']), + newGroupName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}new_group_name']), + newDeleteMessagesAfterMilliseconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}new_delete_messages_after_milliseconds']), + type: $GroupHistoriesTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + actionAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}action_at'])!, + ); + } + + @override + $GroupHistoriesTable createAlias(String alias) { + return $GroupHistoriesTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const EnumNameConverter(GroupActionType.values); +} + +class GroupHistory extends DataClass implements Insertable { + final String groupHistoryId; + final String groupId; + final int? contactId; + final int? affectedContactId; + final String? oldGroupName; + final String? newGroupName; + final int? newDeleteMessagesAfterMilliseconds; + final GroupActionType type; + final DateTime actionAt; + const GroupHistory( + {required this.groupHistoryId, + required this.groupId, + this.contactId, + this.affectedContactId, + this.oldGroupName, + this.newGroupName, + this.newDeleteMessagesAfterMilliseconds, + required this.type, + required this.actionAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_history_id'] = Variable(groupHistoryId); + map['group_id'] = Variable(groupId); + if (!nullToAbsent || contactId != null) { + map['contact_id'] = Variable(contactId); + } + if (!nullToAbsent || affectedContactId != null) { + map['affected_contact_id'] = Variable(affectedContactId); + } + if (!nullToAbsent || oldGroupName != null) { + map['old_group_name'] = Variable(oldGroupName); + } + if (!nullToAbsent || newGroupName != null) { + map['new_group_name'] = Variable(newGroupName); + } + if (!nullToAbsent || newDeleteMessagesAfterMilliseconds != null) { + map['new_delete_messages_after_milliseconds'] = + Variable(newDeleteMessagesAfterMilliseconds); + } + { + map['type'] = + Variable($GroupHistoriesTable.$convertertype.toSql(type)); + } + map['action_at'] = Variable(actionAt); + return map; + } + + GroupHistoriesCompanion toCompanion(bool nullToAbsent) { + return GroupHistoriesCompanion( + groupHistoryId: Value(groupHistoryId), + groupId: Value(groupId), + contactId: contactId == null && nullToAbsent + ? const Value.absent() + : Value(contactId), + affectedContactId: affectedContactId == null && nullToAbsent + ? const Value.absent() + : Value(affectedContactId), + oldGroupName: oldGroupName == null && nullToAbsent + ? const Value.absent() + : Value(oldGroupName), + newGroupName: newGroupName == null && nullToAbsent + ? const Value.absent() + : Value(newGroupName), + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds == null && nullToAbsent + ? const Value.absent() + : Value(newDeleteMessagesAfterMilliseconds), + type: Value(type), + actionAt: Value(actionAt), + ); + } + + factory GroupHistory.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupHistory( + groupHistoryId: serializer.fromJson(json['groupHistoryId']), + groupId: serializer.fromJson(json['groupId']), + contactId: serializer.fromJson(json['contactId']), + affectedContactId: serializer.fromJson(json['affectedContactId']), + oldGroupName: serializer.fromJson(json['oldGroupName']), + newGroupName: serializer.fromJson(json['newGroupName']), + newDeleteMessagesAfterMilliseconds: + serializer.fromJson(json['newDeleteMessagesAfterMilliseconds']), + type: $GroupHistoriesTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + actionAt: serializer.fromJson(json['actionAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupHistoryId': serializer.toJson(groupHistoryId), + 'groupId': serializer.toJson(groupId), + 'contactId': serializer.toJson(contactId), + 'affectedContactId': serializer.toJson(affectedContactId), + 'oldGroupName': serializer.toJson(oldGroupName), + 'newGroupName': serializer.toJson(newGroupName), + 'newDeleteMessagesAfterMilliseconds': + serializer.toJson(newDeleteMessagesAfterMilliseconds), + 'type': serializer + .toJson($GroupHistoriesTable.$convertertype.toJson(type)), + 'actionAt': serializer.toJson(actionAt), + }; + } + + GroupHistory copyWith( + {String? groupHistoryId, + String? groupId, + Value contactId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = const Value.absent(), + GroupActionType? type, + DateTime? actionAt}) => + GroupHistory( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId.present ? contactId.value : this.contactId, + affectedContactId: affectedContactId.present + ? affectedContactId.value + : this.affectedContactId, + oldGroupName: + oldGroupName.present ? oldGroupName.value : this.oldGroupName, + newGroupName: + newGroupName.present ? newGroupName.value : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds.present + ? newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + ); + GroupHistory copyWithCompanion(GroupHistoriesCompanion data) { + return GroupHistory( + groupHistoryId: data.groupHistoryId.present + ? data.groupHistoryId.value + : this.groupHistoryId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + contactId: data.contactId.present ? data.contactId.value : this.contactId, + affectedContactId: data.affectedContactId.present + ? data.affectedContactId.value + : this.affectedContactId, + oldGroupName: data.oldGroupName.present + ? data.oldGroupName.value + : this.oldGroupName, + newGroupName: data.newGroupName.present + ? data.newGroupName.value + : this.newGroupName, + newDeleteMessagesAfterMilliseconds: + data.newDeleteMessagesAfterMilliseconds.present + ? data.newDeleteMessagesAfterMilliseconds.value + : this.newDeleteMessagesAfterMilliseconds, + type: data.type.present ? data.type.value : this.type, + actionAt: data.actionAt.present ? data.actionAt.value : this.actionAt, + ); + } + + @override + String toString() { + return (StringBuffer('GroupHistory(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + groupHistoryId, + groupId, + contactId, + affectedContactId, + oldGroupName, + newGroupName, + newDeleteMessagesAfterMilliseconds, + type, + actionAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupHistory && + other.groupHistoryId == this.groupHistoryId && + other.groupId == this.groupId && + other.contactId == this.contactId && + other.affectedContactId == this.affectedContactId && + other.oldGroupName == this.oldGroupName && + other.newGroupName == this.newGroupName && + other.newDeleteMessagesAfterMilliseconds == + this.newDeleteMessagesAfterMilliseconds && + other.type == this.type && + other.actionAt == this.actionAt); +} + +class GroupHistoriesCompanion extends UpdateCompanion { + final Value groupHistoryId; + final Value groupId; + final Value contactId; + final Value affectedContactId; + final Value oldGroupName; + final Value newGroupName; + final Value newDeleteMessagesAfterMilliseconds; + final Value type; + final Value actionAt; + final Value rowid; + const GroupHistoriesCompanion({ + this.groupHistoryId = const Value.absent(), + this.groupId = const Value.absent(), + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + this.type = const Value.absent(), + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupHistoriesCompanion.insert({ + required String groupHistoryId, + required String groupId, + this.contactId = const Value.absent(), + this.affectedContactId = const Value.absent(), + this.oldGroupName = const Value.absent(), + this.newGroupName = const Value.absent(), + this.newDeleteMessagesAfterMilliseconds = const Value.absent(), + required GroupActionType type, + this.actionAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : groupHistoryId = Value(groupHistoryId), + groupId = Value(groupId), + type = Value(type); + static Insertable custom({ + Expression? groupHistoryId, + Expression? groupId, + Expression? contactId, + Expression? affectedContactId, + Expression? oldGroupName, + Expression? newGroupName, + Expression? newDeleteMessagesAfterMilliseconds, + Expression? type, + Expression? actionAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupHistoryId != null) 'group_history_id': groupHistoryId, + if (groupId != null) 'group_id': groupId, + if (contactId != null) 'contact_id': contactId, + if (affectedContactId != null) 'affected_contact_id': affectedContactId, + if (oldGroupName != null) 'old_group_name': oldGroupName, + if (newGroupName != null) 'new_group_name': newGroupName, + if (newDeleteMessagesAfterMilliseconds != null) + 'new_delete_messages_after_milliseconds': + newDeleteMessagesAfterMilliseconds, + if (type != null) 'type': type, + if (actionAt != null) 'action_at': actionAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupHistoriesCompanion copyWith( + {Value? groupHistoryId, + Value? groupId, + Value? contactId, + Value? affectedContactId, + Value? oldGroupName, + Value? newGroupName, + Value? newDeleteMessagesAfterMilliseconds, + Value? type, + Value? actionAt, + Value? rowid}) { + return GroupHistoriesCompanion( + groupHistoryId: groupHistoryId ?? this.groupHistoryId, + groupId: groupId ?? this.groupId, + contactId: contactId ?? this.contactId, + affectedContactId: affectedContactId ?? this.affectedContactId, + oldGroupName: oldGroupName ?? this.oldGroupName, + newGroupName: newGroupName ?? this.newGroupName, + newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds ?? + this.newDeleteMessagesAfterMilliseconds, + type: type ?? this.type, + actionAt: actionAt ?? this.actionAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupHistoryId.present) { + map['group_history_id'] = Variable(groupHistoryId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (contactId.present) { + map['contact_id'] = Variable(contactId.value); + } + if (affectedContactId.present) { + map['affected_contact_id'] = Variable(affectedContactId.value); + } + if (oldGroupName.present) { + map['old_group_name'] = Variable(oldGroupName.value); + } + if (newGroupName.present) { + map['new_group_name'] = Variable(newGroupName.value); + } + if (newDeleteMessagesAfterMilliseconds.present) { + map['new_delete_messages_after_milliseconds'] = + Variable(newDeleteMessagesAfterMilliseconds.value); + } + if (type.present) { + map['type'] = Variable( + $GroupHistoriesTable.$convertertype.toSql(type.value)); + } + if (actionAt.present) { + map['action_at'] = Variable(actionAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupHistoriesCompanion(') + ..write('groupHistoryId: $groupHistoryId, ') + ..write('groupId: $groupId, ') + ..write('contactId: $contactId, ') + ..write('affectedContactId: $affectedContactId, ') + ..write('oldGroupName: $oldGroupName, ') + ..write('newGroupName: $newGroupName, ') + ..write( + 'newDeleteMessagesAfterMilliseconds: $newDeleteMessagesAfterMilliseconds, ') + ..write('type: $type, ') + ..write('actionAt: $actionAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$TwonlyDB extends GeneratedDatabase { + _$TwonlyDB(QueryExecutor e) : super(e); + $TwonlyDBManager get managers => $TwonlyDBManager(this); + late final $ContactsTable contacts = $ContactsTable(this); + late final $GroupsTable groups = $GroupsTable(this); + late final $MediaFilesTable mediaFiles = $MediaFilesTable(this); + late final $MessagesTable messages = $MessagesTable(this); + late final $MessageHistoriesTable messageHistories = + $MessageHistoriesTable(this); + late final $ReactionsTable reactions = $ReactionsTable(this); + late final $GroupMembersTable groupMembers = $GroupMembersTable(this); + late final $ReceiptsTable receipts = $ReceiptsTable(this); + late final $ReceivedReceiptsTable receivedReceipts = + $ReceivedReceiptsTable(this); + late final $SignalIdentityKeyStoresTable signalIdentityKeyStores = + $SignalIdentityKeyStoresTable(this); + late final $SignalPreKeyStoresTable signalPreKeyStores = + $SignalPreKeyStoresTable(this); + late final $SignalSenderKeyStoresTable signalSenderKeyStores = + $SignalSenderKeyStoresTable(this); + late final $SignalSessionStoresTable signalSessionStores = + $SignalSessionStoresTable(this); + late final $SignalContactPreKeysTable signalContactPreKeys = + $SignalContactPreKeysTable(this); + late final $SignalContactSignedPreKeysTable signalContactSignedPreKeys = + $SignalContactSignedPreKeysTable(this); + late final $MessageActionsTable messageActions = $MessageActionsTable(this); + late final $GroupHistoriesTable groupHistories = $GroupHistoriesTable(this); + late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); + late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); + late final SignalDao signalDao = SignalDao(this as TwonlyDB); + late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB); + late final GroupsDao groupsDao = GroupsDao(this as TwonlyDB); + late final ReactionsDao reactionsDao = ReactionsDao(this as TwonlyDB); + late final MediaFilesDao mediaFilesDao = MediaFilesDao(this as TwonlyDB); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + contacts, + groups, + mediaFiles, + messages, + messageHistories, + reactions, + groupMembers, + receipts, + receivedReceipts, + signalIdentityKeyStores, + signalPreKeyStores, + signalSenderKeyStores, + signalSessionStores, + signalContactPreKeys, + signalContactSignedPreKeys, + messageActions, + groupHistories + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('media_files', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.update), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_histories', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('group_members', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('receipts', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('receipts', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('signal_contact_pre_keys', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('contacts', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('signal_contact_signed_pre_keys', + kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('message_actions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('groups', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('group_histories', kind: UpdateKind.delete), + ], + ), + ], + ); +} + +typedef $$ContactsTableCreateCompanionBuilder = ContactsCompanion Function({ + Value userId, + required String username, + Value displayName, + Value nickName, + Value avatarSvgCompressed, + Value senderProfileCounter, + Value accepted, + Value deletedByUser, + Value requested, + Value blocked, + Value verified, + Value accountDeleted, + Value createdAt, +}); +typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ + Value userId, + Value username, + Value displayName, + Value nickName, + Value avatarSvgCompressed, + Value senderProfileCounter, + Value accepted, + Value deletedByUser, + Value requested, + Value blocked, + Value verified, + Value accountDeleted, + Value createdAt, +}); + +final class $$ContactsTableReferences + extends BaseReferences<_$TwonlyDB, $ContactsTable, Contact> { + $$ContactsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.messages.senderId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.senderId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReactionsTable, List> + _reactionsRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.reactions, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.reactions.senderId)); + + $$ReactionsTableProcessedTableManager get reactionsRefs { + final manager = $$ReactionsTableTableManager($_db, $_db.reactions).filter( + (f) => f.senderId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$GroupMembersTable, List> + _groupMembersRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupMembers, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.groupMembers.contactId)); + + $$GroupMembersTableProcessedTableManager get groupMembersRefs { + final manager = $$GroupMembersTableTableManager($_db, $_db.groupMembers) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupMembersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReceiptsTable, List> _receiptsRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.receipts, + aliasName: + $_aliasNameGenerator(db.contacts.userId, db.receipts.contactId)); + + $$ReceiptsTableProcessedTableManager get receiptsRefs { + final manager = $$ReceiptsTableTableManager($_db, $_db.receipts).filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult.readTableOrNull(_receiptsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$SignalContactPreKeysTable, + List> _signalContactPreKeysRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.signalContactPreKeys, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.signalContactPreKeys.contactId)); + + $$SignalContactPreKeysTableProcessedTableManager + get signalContactPreKeysRefs { + final manager = $$SignalContactPreKeysTableTableManager( + $_db, $_db.signalContactPreKeys) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = + $_typedResult.readTableOrNull(_signalContactPreKeysRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$SignalContactSignedPreKeysTable, + List> _signalContactSignedPreKeysRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.signalContactSignedPreKeys, + aliasName: $_aliasNameGenerator( + db.contacts.userId, db.signalContactSignedPreKeys.contactId)); + + $$SignalContactSignedPreKeysTableProcessedTableManager + get signalContactSignedPreKeysRefs { + final manager = $$SignalContactSignedPreKeysTableTableManager( + $_db, $_db.signalContactSignedPreKeys) + .filter( + (f) => f.contactId.userId.sqlEquals($_itemColumn('user_id')!)); + + final cache = $_typedResult + .readTableOrNull(_signalContactSignedPreKeysRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$ContactsTableFilterComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnFilters(column)); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get nickName => $composableBuilder( + column: $table.nickName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get accepted => $composableBuilder( + column: $table.accepted, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedByUser => $composableBuilder( + column: $table.deletedByUser, builder: (column) => ColumnFilters(column)); + + ColumnFilters get requested => $composableBuilder( + column: $table.requested, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnFilters(column)); + + ColumnFilters get verified => $composableBuilder( + column: $table.verified, builder: (column) => ColumnFilters(column)); + + ColumnFilters get accountDeleted => $composableBuilder( + column: $table.accountDeleted, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableFilterComposer f) f) { + final $$ReactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableFilterComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableFilterComposer f) f) { + final $$GroupMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableFilterComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableFilterComposer f) f) { + final $$ReceiptsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableFilterComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactPreKeysRefs( + Expression Function($$SignalContactPreKeysTableFilterComposer f) + f) { + final $$SignalContactPreKeysTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactPreKeysTableFilterComposer( + $db: $db, + $table: $db.signalContactPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactSignedPreKeysRefs( + Expression Function( + $$SignalContactSignedPreKeysTableFilterComposer f) + f) { + final $$SignalContactSignedPreKeysTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactSignedPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactSignedPreKeysTableFilterComposer( + $db: $db, + $table: $db.signalContactSignedPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$ContactsTableOrderingComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get username => $composableBuilder( + column: $table.username, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get nickName => $composableBuilder( + column: $table.nickName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get accepted => $composableBuilder( + column: $table.accepted, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedByUser => $composableBuilder( + column: $table.deletedByUser, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get requested => $composableBuilder( + column: $table.requested, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get verified => $composableBuilder( + column: $table.verified, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get accountDeleted => $composableBuilder( + column: $table.accountDeleted, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$ContactsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ContactsTable> { + $$ContactsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get username => + $composableBuilder(column: $table.username, builder: (column) => column); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, builder: (column) => column); + + GeneratedColumn get nickName => + $composableBuilder(column: $table.nickName, builder: (column) => column); + + GeneratedColumn get avatarSvgCompressed => $composableBuilder( + column: $table.avatarSvgCompressed, builder: (column) => column); + + GeneratedColumn get senderProfileCounter => $composableBuilder( + column: $table.senderProfileCounter, builder: (column) => column); + + GeneratedColumn get accepted => + $composableBuilder(column: $table.accepted, builder: (column) => column); + + GeneratedColumn get deletedByUser => $composableBuilder( + column: $table.deletedByUser, builder: (column) => column); + + GeneratedColumn get requested => + $composableBuilder(column: $table.requested, builder: (column) => column); + + GeneratedColumn get blocked => + $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get verified => + $composableBuilder(column: $table.verified, builder: (column) => column); + + GeneratedColumn get accountDeleted => $composableBuilder( + column: $table.accountDeleted, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.senderId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableAnnotationComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableAnnotationComposer a) f) { + final $$GroupMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableAnnotationComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableAnnotationComposer a) f) { + final $$ReceiptsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableAnnotationComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactPreKeysRefs( + Expression Function($$SignalContactPreKeysTableAnnotationComposer a) + f) { + final $$SignalContactPreKeysTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactPreKeysTableAnnotationComposer( + $db: $db, + $table: $db.signalContactPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression signalContactSignedPreKeysRefs( + Expression Function( + $$SignalContactSignedPreKeysTableAnnotationComposer a) + f) { + final $$SignalContactSignedPreKeysTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: $db.signalContactSignedPreKeys, + getReferencedColumn: (t) => t.contactId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$SignalContactSignedPreKeysTableAnnotationComposer( + $db: $db, + $table: $db.signalContactSignedPreKeys, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$ContactsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ContactsTable, + Contact, + $$ContactsTableFilterComposer, + $$ContactsTableOrderingComposer, + $$ContactsTableAnnotationComposer, + $$ContactsTableCreateCompanionBuilder, + $$ContactsTableUpdateCompanionBuilder, + (Contact, $$ContactsTableReferences), + Contact, + PrefetchHooks Function( + {bool messagesRefs, + bool reactionsRefs, + bool groupMembersRefs, + bool receiptsRefs, + bool signalContactPreKeysRefs, + bool signalContactSignedPreKeysRefs})> { + $$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ContactsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ContactsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ContactsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value userId = const Value.absent(), + Value username = const Value.absent(), + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), + Value senderProfileCounter = const Value.absent(), + Value accepted = const Value.absent(), + Value deletedByUser = const Value.absent(), + Value requested = const Value.absent(), + Value blocked = const Value.absent(), + Value verified = const Value.absent(), + Value accountDeleted = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + ContactsCompanion( + userId: userId, + username: username, + displayName: displayName, + nickName: nickName, + avatarSvgCompressed: avatarSvgCompressed, + senderProfileCounter: senderProfileCounter, + accepted: accepted, + deletedByUser: deletedByUser, + requested: requested, + blocked: blocked, + verified: verified, + accountDeleted: accountDeleted, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value userId = const Value.absent(), + required String username, + Value displayName = const Value.absent(), + Value nickName = const Value.absent(), + Value avatarSvgCompressed = const Value.absent(), + Value senderProfileCounter = const Value.absent(), + Value accepted = const Value.absent(), + Value deletedByUser = const Value.absent(), + Value requested = const Value.absent(), + Value blocked = const Value.absent(), + Value verified = const Value.absent(), + Value accountDeleted = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + ContactsCompanion.insert( + userId: userId, + username: username, + displayName: displayName, + nickName: nickName, + avatarSvgCompressed: avatarSvgCompressed, + senderProfileCounter: senderProfileCounter, + accepted: accepted, + deletedByUser: deletedByUser, + requested: requested, + blocked: blocked, + verified: verified, + accountDeleted: accountDeleted, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$ContactsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {messagesRefs = false, + reactionsRefs = false, + groupMembersRefs = false, + receiptsRefs = false, + signalContactPreKeysRefs = false, + signalContactSignedPreKeysRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (reactionsRefs) db.reactions, + if (groupMembersRefs) db.groupMembers, + if (receiptsRefs) db.receipts, + if (signalContactPreKeysRefs) db.signalContactPreKeys, + if (signalContactSignedPreKeysRefs) + db.signalContactSignedPreKeys + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.senderId == item.userId), + typedResults: items), + if (reactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._reactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .reactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.senderId == item.userId), + typedResults: items), + if (groupMembersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._groupMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .groupMembersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (receiptsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ContactsTableReferences._receiptsRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .receiptsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (signalContactPreKeysRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._signalContactPreKeysRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .signalContactPreKeysRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items), + if (signalContactSignedPreKeysRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ContactsTableReferences + ._signalContactSignedPreKeysRefsTable(db), + managerFromTypedResult: (p0) => + $$ContactsTableReferences(db, table, p0) + .signalContactSignedPreKeysRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.contactId == item.userId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ContactsTable, + Contact, + $$ContactsTableFilterComposer, + $$ContactsTableOrderingComposer, + $$ContactsTableAnnotationComposer, + $$ContactsTableCreateCompanionBuilder, + $$ContactsTableUpdateCompanionBuilder, + (Contact, $$ContactsTableReferences), + Contact, + PrefetchHooks Function( + {bool messagesRefs, + bool reactionsRefs, + bool groupMembersRefs, + bool receiptsRefs, + bool signalContactPreKeysRefs, + bool signalContactSignedPreKeysRefs})>; +typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({ + required String groupId, + Value isGroupAdmin, + Value isDirectChat, + Value pinned, + Value archived, + Value joinedGroup, + Value leftGroup, + Value deletedContent, + Value stateVersionId, + Value stateEncryptionKey, + Value myGroupPrivateKey, + required String groupName, + Value totalMediaCounter, + Value alsoBestFriend, + Value deleteMessagesAfterMilliseconds, + Value createdAt, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, + Value maxFlameCounter, + Value maxFlameCounterFrom, + Value lastMessageExchange, + Value rowid, +}); +typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ + Value groupId, + Value isGroupAdmin, + Value isDirectChat, + Value pinned, + Value archived, + Value joinedGroup, + Value leftGroup, + Value deletedContent, + Value stateVersionId, + Value stateEncryptionKey, + Value myGroupPrivateKey, + Value groupName, + Value totalMediaCounter, + Value alsoBestFriend, + Value deleteMessagesAfterMilliseconds, + Value createdAt, + Value lastMessageSend, + Value lastMessageReceived, + Value lastFlameCounterChange, + Value lastFlameSync, + Value flameCounter, + Value maxFlameCounter, + Value maxFlameCounterFrom, + Value lastMessageExchange, + Value rowid, +}); + +final class $$GroupsTableReferences + extends BaseReferences<_$TwonlyDB, $GroupsTable, Group> { + $$GroupsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.groups.groupId, db.messages.groupId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$GroupMembersTable, List> + _groupMembersRefsTable(_$TwonlyDB db) => MultiTypedResultKey.fromTable( + db.groupMembers, + aliasName: + $_aliasNameGenerator(db.groups.groupId, db.groupMembers.groupId)); + + $$GroupMembersTableProcessedTableManager get groupMembersRefs { + final manager = $$GroupMembersTableTableManager($_db, $_db.groupMembers) + .filter((f) => + f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupMembersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$GroupHistoriesTable, List> + _groupHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.groupHistories, + aliasName: $_aliasNameGenerator( + db.groups.groupId, db.groupHistories.groupId)); + + $$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs { + final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories) + .filter((f) => + f.groupId.groupId.sqlEquals($_itemColumn('group_id')!)); + + final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isDirectChat => $composableBuilder( + column: $table.isDirectChat, builder: (column) => ColumnFilters(column)); + + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnFilters(column)); + + ColumnFilters get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnFilters(column)); + + ColumnFilters get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => ColumnFilters(column)); + + ColumnFilters get leftGroup => $composableBuilder( + column: $table.leftGroup, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedContent => $composableBuilder( + column: $table.deletedContent, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get stateVersionId => $composableBuilder( + column: $table.stateVersionId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get deleteMessagesAfterMilliseconds => $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => ColumnFilters(column)); + + ColumnFilters get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => ColumnFilters(column)); + + ColumnFilters get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableFilterComposer f) f) { + final $$GroupMembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableFilterComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableFilterComposer f) f) { + final $$GroupHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableFilterComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupId => $composableBuilder( + column: $table.groupId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isDirectChat => $composableBuilder( + column: $table.isDirectChat, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get archived => $composableBuilder( + column: $table.archived, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get leftGroup => $composableBuilder( + column: $table.leftGroup, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deletedContent => $composableBuilder( + column: $table.deletedContent, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stateVersionId => $composableBuilder( + column: $table.stateVersionId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get deleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get flameCounter => $composableBuilder( + column: $table.flameCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, + builder: (column) => ColumnOrderings(column)); +} + +class $$GroupsTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupsTable> { + $$GroupsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupId => + $composableBuilder(column: $table.groupId, builder: (column) => column); + + GeneratedColumn get isGroupAdmin => $composableBuilder( + column: $table.isGroupAdmin, builder: (column) => column); + + GeneratedColumn get isDirectChat => $composableBuilder( + column: $table.isDirectChat, builder: (column) => column); + + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); + + GeneratedColumn get archived => + $composableBuilder(column: $table.archived, builder: (column) => column); + + GeneratedColumn get joinedGroup => $composableBuilder( + column: $table.joinedGroup, builder: (column) => column); + + GeneratedColumn get leftGroup => + $composableBuilder(column: $table.leftGroup, builder: (column) => column); + + GeneratedColumn get deletedContent => $composableBuilder( + column: $table.deletedContent, builder: (column) => column); + + GeneratedColumn get stateVersionId => $composableBuilder( + column: $table.stateVersionId, builder: (column) => column); + + GeneratedColumn get stateEncryptionKey => $composableBuilder( + column: $table.stateEncryptionKey, builder: (column) => column); + + GeneratedColumn get myGroupPrivateKey => $composableBuilder( + column: $table.myGroupPrivateKey, builder: (column) => column); + + GeneratedColumn get groupName => + $composableBuilder(column: $table.groupName, builder: (column) => column); + + GeneratedColumn get totalMediaCounter => $composableBuilder( + column: $table.totalMediaCounter, builder: (column) => column); + + GeneratedColumn get alsoBestFriend => $composableBuilder( + column: $table.alsoBestFriend, builder: (column) => column); + + GeneratedColumn get deleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.deleteMessagesAfterMilliseconds, + builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get lastMessageSend => $composableBuilder( + column: $table.lastMessageSend, builder: (column) => column); + + GeneratedColumn get lastMessageReceived => $composableBuilder( + column: $table.lastMessageReceived, builder: (column) => column); + + GeneratedColumn get lastFlameCounterChange => $composableBuilder( + column: $table.lastFlameCounterChange, builder: (column) => column); + + GeneratedColumn get lastFlameSync => $composableBuilder( + column: $table.lastFlameSync, builder: (column) => column); + + GeneratedColumn get flameCounter => $composableBuilder( + column: $table.flameCounter, builder: (column) => column); + + GeneratedColumn get maxFlameCounter => $composableBuilder( + column: $table.maxFlameCounter, builder: (column) => column); + + GeneratedColumn get maxFlameCounterFrom => $composableBuilder( + column: $table.maxFlameCounterFrom, builder: (column) => column); + + GeneratedColumn get lastMessageExchange => $composableBuilder( + column: $table.lastMessageExchange, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupMembersRefs( + Expression Function($$GroupMembersTableAnnotationComposer a) f) { + final $$GroupMembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupMembers, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupMembersTableAnnotationComposer( + $db: $db, + $table: $db.groupMembers, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression groupHistoriesRefs( + Expression Function($$GroupHistoriesTableAnnotationComposer a) f) { + final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupHistories, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.groupHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$GroupsTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, + PrefetchHooks Function( + {bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs})> { + $$GroupsTableTableManager(_$TwonlyDB db, $GroupsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value isGroupAdmin = const Value.absent(), + Value isDirectChat = const Value.absent(), + Value pinned = const Value.absent(), + Value archived = const Value.absent(), + Value joinedGroup = const Value.absent(), + Value leftGroup = const Value.absent(), + Value deletedContent = const Value.absent(), + Value stateVersionId = const Value.absent(), + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), + Value groupName = const Value.absent(), + Value totalMediaCounter = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterMilliseconds = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + Value maxFlameCounter = const Value.absent(), + Value maxFlameCounterFrom = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isDirectChat: isDirectChat, + pinned: pinned, + archived: archived, + joinedGroup: joinedGroup, + leftGroup: leftGroup, + deletedContent: deletedContent, + stateVersionId: stateVersionId, + stateEncryptionKey: stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey, + groupName: groupName, + totalMediaCounter: totalMediaCounter, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds, + createdAt: createdAt, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + maxFlameCounter: maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupId, + Value isGroupAdmin = const Value.absent(), + Value isDirectChat = const Value.absent(), + Value pinned = const Value.absent(), + Value archived = const Value.absent(), + Value joinedGroup = const Value.absent(), + Value leftGroup = const Value.absent(), + Value deletedContent = const Value.absent(), + Value stateVersionId = const Value.absent(), + Value stateEncryptionKey = const Value.absent(), + Value myGroupPrivateKey = const Value.absent(), + required String groupName, + Value totalMediaCounter = const Value.absent(), + Value alsoBestFriend = const Value.absent(), + Value deleteMessagesAfterMilliseconds = const Value.absent(), + Value createdAt = const Value.absent(), + Value lastMessageSend = const Value.absent(), + Value lastMessageReceived = const Value.absent(), + Value lastFlameCounterChange = const Value.absent(), + Value lastFlameSync = const Value.absent(), + Value flameCounter = const Value.absent(), + Value maxFlameCounter = const Value.absent(), + Value maxFlameCounterFrom = const Value.absent(), + Value lastMessageExchange = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupsCompanion.insert( + groupId: groupId, + isGroupAdmin: isGroupAdmin, + isDirectChat: isDirectChat, + pinned: pinned, + archived: archived, + joinedGroup: joinedGroup, + leftGroup: leftGroup, + deletedContent: deletedContent, + stateVersionId: stateVersionId, + stateEncryptionKey: stateEncryptionKey, + myGroupPrivateKey: myGroupPrivateKey, + groupName: groupName, + totalMediaCounter: totalMediaCounter, + alsoBestFriend: alsoBestFriend, + deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds, + createdAt: createdAt, + lastMessageSend: lastMessageSend, + lastMessageReceived: lastMessageReceived, + lastFlameCounterChange: lastFlameCounterChange, + lastFlameSync: lastFlameSync, + flameCounter: flameCounter, + maxFlameCounter: maxFlameCounter, + maxFlameCounterFrom: maxFlameCounterFrom, + lastMessageExchange: lastMessageExchange, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$GroupsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {messagesRefs = false, + groupMembersRefs = false, + groupHistoriesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (groupMembersRefs) db.groupMembers, + if (groupHistoriesRefs) db.groupHistories + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$GroupsTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0).messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), + typedResults: items), + if (groupMembersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$GroupsTableReferences._groupMembersRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0) + .groupMembersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), + typedResults: items), + if (groupHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$GroupsTableReferences + ._groupHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupsTableReferences(db, table, p0) + .groupHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.groupId == item.groupId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$GroupsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupsTable, + Group, + $$GroupsTableFilterComposer, + $$GroupsTableOrderingComposer, + $$GroupsTableAnnotationComposer, + $$GroupsTableCreateCompanionBuilder, + $$GroupsTableUpdateCompanionBuilder, + (Group, $$GroupsTableReferences), + Group, + PrefetchHooks Function( + {bool messagesRefs, bool groupMembersRefs, bool groupHistoriesRefs})>; +typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ + required String mediaId, + required MediaType type, + Value uploadState, + Value downloadState, + Value requiresAuthentication, + Value reopenByContact, + Value stored, + Value isDraftMedia, + Value?> reuploadRequestedBy, + Value displayLimitInMilliseconds, + Value removeAudio, + Value downloadToken, + Value encryptionKey, + Value encryptionMac, + Value encryptionNonce, + Value createdAt, + Value rowid, +}); +typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ + Value mediaId, + Value type, + Value uploadState, + Value downloadState, + Value requiresAuthentication, + Value reopenByContact, + Value stored, + Value isDraftMedia, + Value?> reuploadRequestedBy, + Value displayLimitInMilliseconds, + Value removeAudio, + Value downloadToken, + Value encryptionKey, + Value encryptionMac, + Value encryptionNonce, + Value createdAt, + Value rowid, +}); + +final class $$MediaFilesTableReferences + extends BaseReferences<_$TwonlyDB, $MediaFilesTable, MediaFile> { + $$MediaFilesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: + $_aliasNameGenerator(db.mediaFiles.mediaId, db.messages.mediaId)); + + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.mediaId.mediaId.sqlEquals($_itemColumn('media_id')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$MediaFilesTableFilterComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get mediaId => $composableBuilder( + column: $table.mediaId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get uploadState => $composableBuilder( + column: $table.uploadState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters + get downloadState => $composableBuilder( + column: $table.downloadState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get reopenByContact => $composableBuilder( + column: $table.reopenByContact, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters?, List, String> + get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnFilters(column)); + + ColumnFilters get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionKey => $composableBuilder( + column: $table.encryptionKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionMac => $composableBuilder( + column: $table.encryptionMac, builder: (column) => ColumnFilters(column)); + + ColumnFilters get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MediaFilesTableOrderingComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get mediaId => $composableBuilder( + column: $table.mediaId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get uploadState => $composableBuilder( + column: $table.uploadState, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get downloadState => $composableBuilder( + column: $table.downloadState, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get reopenByContact => $composableBuilder( + column: $table.reopenByContact, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get stored => $composableBuilder( + column: $table.stored, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get downloadToken => $composableBuilder( + column: $table.downloadToken, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionKey => $composableBuilder( + column: $table.encryptionKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionMac => $composableBuilder( + column: $table.encryptionMac, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$MediaFilesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MediaFilesTable> { + $$MediaFilesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get mediaId => + $composableBuilder(column: $table.mediaId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumnWithTypeConverter get uploadState => + $composableBuilder( + column: $table.uploadState, builder: (column) => column); + + GeneratedColumnWithTypeConverter get downloadState => + $composableBuilder( + column: $table.downloadState, builder: (column) => column); + + GeneratedColumn get requiresAuthentication => $composableBuilder( + column: $table.requiresAuthentication, builder: (column) => column); + + GeneratedColumn get reopenByContact => $composableBuilder( + column: $table.reopenByContact, builder: (column) => column); + + GeneratedColumn get stored => + $composableBuilder(column: $table.stored, builder: (column) => column); + + GeneratedColumn get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> + get reuploadRequestedBy => $composableBuilder( + column: $table.reuploadRequestedBy, builder: (column) => column); + + GeneratedColumn get displayLimitInMilliseconds => $composableBuilder( + column: $table.displayLimitInMilliseconds, builder: (column) => column); + + GeneratedColumn get removeAudio => $composableBuilder( + column: $table.removeAudio, builder: (column) => column); + + GeneratedColumn get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => column); + + GeneratedColumn get encryptionKey => $composableBuilder( + column: $table.encryptionKey, builder: (column) => column); + + GeneratedColumn get encryptionMac => $composableBuilder( + column: $table.encryptionMac, builder: (column) => column); + + GeneratedColumn get encryptionNonce => $composableBuilder( + column: $table.encryptionNonce, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MediaFilesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, $$MediaFilesTableReferences), + MediaFile, + PrefetchHooks Function({bool messagesRefs})> { + $$MediaFilesTableTableManager(_$TwonlyDB db, $MediaFilesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MediaFilesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MediaFilesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MediaFilesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value mediaId = const Value.absent(), + Value type = const Value.absent(), + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + Value requiresAuthentication = const Value.absent(), + Value reopenByContact = const Value.absent(), + Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), + Value?> reuploadRequestedBy = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MediaFilesCompanion( + mediaId: mediaId, + type: type, + uploadState: uploadState, + downloadState: downloadState, + requiresAuthentication: requiresAuthentication, + reopenByContact: reopenByContact, + stored: stored, + isDraftMedia: isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, + downloadToken: downloadToken, + encryptionKey: encryptionKey, + encryptionMac: encryptionMac, + encryptionNonce: encryptionNonce, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String mediaId, + required MediaType type, + Value uploadState = const Value.absent(), + Value downloadState = const Value.absent(), + Value requiresAuthentication = const Value.absent(), + Value reopenByContact = const Value.absent(), + Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), + Value?> reuploadRequestedBy = const Value.absent(), + Value displayLimitInMilliseconds = const Value.absent(), + Value removeAudio = const Value.absent(), + Value downloadToken = const Value.absent(), + Value encryptionKey = const Value.absent(), + Value encryptionMac = const Value.absent(), + Value encryptionNonce = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MediaFilesCompanion.insert( + mediaId: mediaId, + type: type, + uploadState: uploadState, + downloadState: downloadState, + requiresAuthentication: requiresAuthentication, + reopenByContact: reopenByContact, + stored: stored, + isDraftMedia: isDraftMedia, + reuploadRequestedBy: reuploadRequestedBy, + displayLimitInMilliseconds: displayLimitInMilliseconds, + removeAudio: removeAudio, + downloadToken: downloadToken, + encryptionKey: encryptionKey, + encryptionMac: encryptionMac, + encryptionNonce: encryptionNonce, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MediaFilesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messagesRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (messagesRefs) db.messages], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MediaFilesTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$MediaFilesTableReferences(db, table, p0) + .messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.mediaId == item.mediaId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$MediaFilesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MediaFilesTable, + MediaFile, + $$MediaFilesTableFilterComposer, + $$MediaFilesTableOrderingComposer, + $$MediaFilesTableAnnotationComposer, + $$MediaFilesTableCreateCompanionBuilder, + $$MediaFilesTableUpdateCompanionBuilder, + (MediaFile, $$MediaFilesTableReferences), + MediaFile, + PrefetchHooks Function({bool messagesRefs})>; +typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ + required String groupId, + required String messageId, + Value senderId, + required MessageType type, + Value content, + Value mediaId, + Value mediaStored, + Value downloadToken, + Value quotesMessageId, + Value isDeletedFromSender, + Value openedAt, + Value openedByAll, + Value createdAt, + Value modifiedAt, + Value ackByUser, + Value ackByServer, + Value rowid, +}); +typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ + Value groupId, + Value messageId, + Value senderId, + Value type, + Value content, + Value mediaId, + Value mediaStored, + Value downloadToken, + Value quotesMessageId, + Value isDeletedFromSender, + Value openedAt, + Value openedByAll, + Value createdAt, + Value modifiedAt, + Value ackByUser, + Value ackByServer, + Value rowid, +}); + +final class $$MessagesTableReferences + extends BaseReferences<_$TwonlyDB, $MessagesTable, Message> { + $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.messages.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _senderIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.messages.senderId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get senderId { + final $_column = $_itemColumn('sender_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_senderIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MediaFilesTable _mediaIdTable(_$TwonlyDB db) => + db.mediaFiles.createAlias( + $_aliasNameGenerator(db.messages.mediaId, db.mediaFiles.mediaId)); + + $$MediaFilesTableProcessedTableManager? get mediaId { + final $_column = $_itemColumn('media_id'); + if ($_column == null) return null; + final manager = $$MediaFilesTableTableManager($_db, $_db.mediaFiles) + .filter((f) => f.mediaId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_mediaIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static MultiTypedResultKey<$MessageHistoriesTable, List> + _messageHistoriesRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageHistories, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.messageHistories.messageId)); + + $$MessageHistoriesTableProcessedTableManager get messageHistoriesRefs { + final manager = + $$MessageHistoriesTableTableManager($_db, $_db.messageHistories).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = + $_typedResult.readTableOrNull(_messageHistoriesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReactionsTable, List> + _reactionsRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.reactions, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.reactions.messageId)); + + $$ReactionsTableProcessedTableManager get reactionsRefs { + final manager = $$ReactionsTableTableManager($_db, $_db.reactions).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReceiptsTable, List> _receiptsRefsTable( + _$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.receipts, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.receipts.messageId)); + + $$ReceiptsTableProcessedTableManager get receiptsRefs { + final manager = $$ReceiptsTableTableManager($_db, $_db.receipts).filter( + (f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_receiptsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$MessageActionsTable, List> + _messageActionsRefsTable(_$TwonlyDB db) => + MultiTypedResultKey.fromTable(db.messageActions, + aliasName: $_aliasNameGenerator( + db.messages.messageId, db.messageActions.messageId)); + + $$MessageActionsTableProcessedTableManager get messageActionsRefs { + final manager = $$MessageActionsTableTableManager($_db, $_db.messageActions) + .filter((f) => f.messageId.messageId + .sqlEquals($_itemColumn('message_id')!)); + + final cache = $_typedResult.readTableOrNull(_messageActionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$MessagesTableFilterComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnFilters(column)); + + ColumnFilters get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + + ColumnFilters get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnFilters(column)); + + ColumnFilters get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnFilters(column)); + + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get senderId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableFilterComposer get mediaId { + final $$MediaFilesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableFilterComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + Expression messageHistoriesRefs( + Expression Function($$MessageHistoriesTableFilterComposer f) f) { + final $$MessageHistoriesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableFilterComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableFilterComposer f) f) { + final $$ReactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableFilterComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableFilterComposer f) f) { + final $$ReceiptsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableFilterComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression messageActionsRefs( + Expression Function($$MessageActionsTableFilterComposer f) f) { + final $$MessageActionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableFilterComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MessagesTableOrderingComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get downloadToken => $composableBuilder( + column: $table.downloadToken, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get openedAt => $composableBuilder( + column: $table.openedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ackByUser => $composableBuilder( + column: $table.ackByUser, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => ColumnOrderings(column)); + + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get senderId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableOrderingComposer get mediaId { + final $$MediaFilesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableOrderingComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessagesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessagesTable> { + $$MessagesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get messageId => + $composableBuilder(column: $table.messageId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => column); + + GeneratedColumn get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => column); + + GeneratedColumn get quotesMessageId => $composableBuilder( + column: $table.quotesMessageId, builder: (column) => column); + + GeneratedColumn get isDeletedFromSender => $composableBuilder( + column: $table.isDeletedFromSender, builder: (column) => column); + + GeneratedColumn get openedAt => + $composableBuilder(column: $table.openedAt, builder: (column) => column); + + GeneratedColumn get openedByAll => $composableBuilder( + column: $table.openedByAll, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get modifiedAt => $composableBuilder( + column: $table.modifiedAt, builder: (column) => column); + + GeneratedColumn get ackByUser => + $composableBuilder(column: $table.ackByUser, builder: (column) => column); + + GeneratedColumn get ackByServer => $composableBuilder( + column: $table.ackByServer, builder: (column) => column); + + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get senderId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MediaFilesTableAnnotationComposer get mediaId { + final $$MediaFilesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.mediaId, + referencedTable: $db.mediaFiles, + getReferencedColumn: (t) => t.mediaId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MediaFilesTableAnnotationComposer( + $db: $db, + $table: $db.mediaFiles, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + Expression messageHistoriesRefs( + Expression Function($$MessageHistoriesTableAnnotationComposer a) f) { + final $$MessageHistoriesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageHistories, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageHistoriesTableAnnotationComposer( + $db: $db, + $table: $db.messageHistories, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression reactionsRefs( + Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableAnnotationComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression receiptsRefs( + Expression Function($$ReceiptsTableAnnotationComposer a) f) { + final $$ReceiptsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.receipts, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReceiptsTableAnnotationComposer( + $db: $db, + $table: $db.receipts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression messageActionsRefs( + Expression Function($$MessageActionsTableAnnotationComposer a) f) { + final $$MessageActionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messageActions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessageActionsTableAnnotationComposer( + $db: $db, + $table: $db.messageActions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $$MessagesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessagesTable, + Message, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (Message, $$MessagesTableReferences), + Message, + PrefetchHooks Function( + {bool groupId, + bool senderId, + bool mediaId, + bool messageHistoriesRefs, + bool reactionsRefs, + bool receiptsRefs, + bool messageActionsRefs})> { + $$MessagesTableTableManager(_$TwonlyDB db, $MessagesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessagesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value messageId = const Value.absent(), + Value senderId = const Value.absent(), + Value type = const Value.absent(), + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), + Value downloadToken = const Value.absent(), + Value quotesMessageId = const Value.absent(), + Value isDeletedFromSender = const Value.absent(), + Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), + Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessagesCompanion( + groupId: groupId, + messageId: messageId, + senderId: senderId, + type: type, + content: content, + mediaId: mediaId, + mediaStored: mediaStored, + downloadToken: downloadToken, + quotesMessageId: quotesMessageId, + isDeletedFromSender: isDeletedFromSender, + openedAt: openedAt, + openedByAll: openedByAll, + createdAt: createdAt, + modifiedAt: modifiedAt, + ackByUser: ackByUser, + ackByServer: ackByServer, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupId, + required String messageId, + Value senderId = const Value.absent(), + required MessageType type, + Value content = const Value.absent(), + Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), + Value downloadToken = const Value.absent(), + Value quotesMessageId = const Value.absent(), + Value isDeletedFromSender = const Value.absent(), + Value openedAt = const Value.absent(), + Value openedByAll = const Value.absent(), + Value createdAt = const Value.absent(), + Value modifiedAt = const Value.absent(), + Value ackByUser = const Value.absent(), + Value ackByServer = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessagesCompanion.insert( + groupId: groupId, + messageId: messageId, + senderId: senderId, + type: type, + content: content, + mediaId: mediaId, + mediaStored: mediaStored, + downloadToken: downloadToken, + quotesMessageId: quotesMessageId, + isDeletedFromSender: isDeletedFromSender, + openedAt: openedAt, + openedByAll: openedByAll, + createdAt: createdAt, + modifiedAt: modifiedAt, + ackByUser: ackByUser, + ackByServer: ackByServer, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$MessagesTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {groupId = false, + senderId = false, + mediaId = false, + messageHistoriesRefs = false, + reactionsRefs = false, + receiptsRefs = false, + messageActionsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messageHistoriesRefs) db.messageHistories, + if (reactionsRefs) db.reactions, + if (receiptsRefs) db.receipts, + if (messageActionsRefs) db.messageActions + ], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$MessagesTableReferences._groupIdTable(db), + referencedColumn: + $$MessagesTableReferences._groupIdTable(db).groupId, + ) as T; + } + if (senderId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.senderId, + referencedTable: + $$MessagesTableReferences._senderIdTable(db), + referencedColumn: + $$MessagesTableReferences._senderIdTable(db).userId, + ) as T; + } + if (mediaId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.mediaId, + referencedTable: + $$MessagesTableReferences._mediaIdTable(db), + referencedColumn: + $$MessagesTableReferences._mediaIdTable(db).mediaId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (messageHistoriesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences + ._messageHistoriesRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .messageHistoriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items), + if (reactionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MessagesTableReferences._reactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .reactionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items), + if (receiptsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$MessagesTableReferences._receiptsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .receiptsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items), + if (messageActionsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences + ._messageActionsRefsTable(db), + managerFromTypedResult: (p0) => + $$MessagesTableReferences(db, table, p0) + .messageActionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.messageId == item.messageId), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessagesTable, + Message, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (Message, $$MessagesTableReferences), + Message, + PrefetchHooks Function( + {bool groupId, + bool senderId, + bool mediaId, + bool messageHistoriesRefs, + bool reactionsRefs, + bool receiptsRefs, + bool messageActionsRefs})>; +typedef $$MessageHistoriesTableCreateCompanionBuilder + = MessageHistoriesCompanion Function({ + Value id, + required String messageId, + Value contactId, + Value content, + Value createdAt, +}); +typedef $$MessageHistoriesTableUpdateCompanionBuilder + = MessageHistoriesCompanion Function({ + Value id, + Value messageId, + Value contactId, + Value content, + Value createdAt, +}); + +final class $$MessageHistoriesTableReferences + extends BaseReferences<_$TwonlyDB, $MessageHistoriesTable, MessageHistory> { + $$MessageHistoriesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias($_aliasNameGenerator( + db.messageHistories.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MessageHistoriesTableFilterComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableOrderingComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessageHistoriesTable> { + $$MessageHistoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get contactId => + $composableBuilder(column: $table.contactId, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageHistoriesTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessageHistoriesTable, + MessageHistory, + $$MessageHistoriesTableFilterComposer, + $$MessageHistoriesTableOrderingComposer, + $$MessageHistoriesTableAnnotationComposer, + $$MessageHistoriesTableCreateCompanionBuilder, + $$MessageHistoriesTableUpdateCompanionBuilder, + (MessageHistory, $$MessageHistoriesTableReferences), + MessageHistory, + PrefetchHooks Function({bool messageId})> { + $$MessageHistoriesTableTableManager( + _$TwonlyDB db, $MessageHistoriesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessageHistoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessageHistoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessageHistoriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value messageId = const Value.absent(), + Value contactId = const Value.absent(), + Value content = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + MessageHistoriesCompanion( + id: id, + messageId: messageId, + contactId: contactId, + content: content, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String messageId, + Value contactId = const Value.absent(), + Value content = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + MessageHistoriesCompanion.insert( + id: id, + messageId: messageId, + contactId: contactId, + content: content, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MessageHistoriesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$MessageHistoriesTableReferences._messageIdTable(db), + referencedColumn: $$MessageHistoriesTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MessageHistoriesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessageHistoriesTable, + MessageHistory, + $$MessageHistoriesTableFilterComposer, + $$MessageHistoriesTableOrderingComposer, + $$MessageHistoriesTableAnnotationComposer, + $$MessageHistoriesTableCreateCompanionBuilder, + $$MessageHistoriesTableUpdateCompanionBuilder, + (MessageHistory, $$MessageHistoriesTableReferences), + MessageHistory, + PrefetchHooks Function({bool messageId})>; +typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({ + required String messageId, + required String emoji, + Value senderId, + Value createdAt, + Value rowid, +}); +typedef $$ReactionsTableUpdateCompanionBuilder = ReactionsCompanion Function({ + Value messageId, + Value emoji, + Value senderId, + Value createdAt, + Value rowid, +}); + +final class $$ReactionsTableReferences + extends BaseReferences<_$TwonlyDB, $ReactionsTable, Reaction> { + $$ReactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias( + $_aliasNameGenerator(db.reactions.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _senderIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.reactions.senderId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get senderId { + final $_column = $_itemColumn('sender_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_senderIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$ReactionsTableFilterComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get emoji => $composableBuilder( + column: $table.emoji, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get senderId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get emoji => $composableBuilder( + column: $table.emoji, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get senderId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReactionsTable> { + $$ReactionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get emoji => + $composableBuilder(column: $table.emoji, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get senderId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.senderId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReactionsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReactionsTable, + Reaction, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (Reaction, $$ReactionsTableReferences), + Reaction, + PrefetchHooks Function({bool messageId, bool senderId})> { + $$ReactionsTableTableManager(_$TwonlyDB db, $ReactionsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReactionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReactionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReactionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value messageId = const Value.absent(), + Value emoji = const Value.absent(), + Value senderId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReactionsCompanion( + messageId: messageId, + emoji: emoji, + senderId: senderId, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String messageId, + required String emoji, + Value senderId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReactionsCompanion.insert( + messageId: messageId, + emoji: emoji, + senderId: senderId, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$ReactionsTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false, senderId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$ReactionsTableReferences._messageIdTable(db), + referencedColumn: $$ReactionsTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + if (senderId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.senderId, + referencedTable: + $$ReactionsTableReferences._senderIdTable(db), + referencedColumn: + $$ReactionsTableReferences._senderIdTable(db).userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$ReactionsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReactionsTable, + Reaction, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (Reaction, $$ReactionsTableReferences), + Reaction, + PrefetchHooks Function({bool messageId, bool senderId})>; +typedef $$GroupMembersTableCreateCompanionBuilder = GroupMembersCompanion + Function({ + required String groupId, + required int contactId, + Value memberState, + Value groupPublicKey, + Value lastMessage, + Value createdAt, + Value rowid, +}); +typedef $$GroupMembersTableUpdateCompanionBuilder = GroupMembersCompanion + Function({ + Value groupId, + Value contactId, + Value memberState, + Value groupPublicKey, + Value lastMessage, + Value createdAt, + Value rowid, +}); + +final class $$GroupMembersTableReferences + extends BaseReferences<_$TwonlyDB, $GroupMembersTable, GroupMember> { + $$GroupMembersTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.groupMembers.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.groupMembers.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$GroupMembersTableFilterComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnWithTypeConverterFilters + get memberState => $composableBuilder( + column: $table.memberState, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableOrderingComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get memberState => $composableBuilder( + column: $table.memberState, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupMembersTable> { + $$GroupMembersTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumnWithTypeConverter get memberState => + $composableBuilder( + column: $table.memberState, builder: (column) => column); + + GeneratedColumn get groupPublicKey => $composableBuilder( + column: $table.groupPublicKey, builder: (column) => column); + + GeneratedColumn get lastMessage => $composableBuilder( + column: $table.lastMessage, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupMembersTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupMembersTable, + GroupMember, + $$GroupMembersTableFilterComposer, + $$GroupMembersTableOrderingComposer, + $$GroupMembersTableAnnotationComposer, + $$GroupMembersTableCreateCompanionBuilder, + $$GroupMembersTableUpdateCompanionBuilder, + (GroupMember, $$GroupMembersTableReferences), + GroupMember, + PrefetchHooks Function({bool groupId, bool contactId})> { + $$GroupMembersTableTableManager(_$TwonlyDB db, $GroupMembersTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupMembersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupMembersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupMembersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupId = const Value.absent(), + Value contactId = const Value.absent(), + Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupMembersCompanion( + groupId: groupId, + contactId: contactId, + memberState: memberState, + groupPublicKey: groupPublicKey, + lastMessage: lastMessage, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupId, + required int contactId, + Value memberState = const Value.absent(), + Value groupPublicKey = const Value.absent(), + Value lastMessage = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupMembersCompanion.insert( + groupId: groupId, + contactId: contactId, + memberState: memberState, + groupPublicKey: groupPublicKey, + lastMessage: lastMessage, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$GroupMembersTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({groupId = false, contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$GroupMembersTableReferences._groupIdTable(db), + referencedColumn: + $$GroupMembersTableReferences._groupIdTable(db).groupId, + ) as T; + } + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$GroupMembersTableReferences._contactIdTable(db), + referencedColumn: $$GroupMembersTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$GroupMembersTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupMembersTable, + GroupMember, + $$GroupMembersTableFilterComposer, + $$GroupMembersTableOrderingComposer, + $$GroupMembersTableAnnotationComposer, + $$GroupMembersTableCreateCompanionBuilder, + $$GroupMembersTableUpdateCompanionBuilder, + (GroupMember, $$GroupMembersTableReferences), + GroupMember, + PrefetchHooks Function({bool groupId, bool contactId})>; +typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({ + required String receiptId, + required int contactId, + Value messageId, + required Uint8List message, + Value contactWillSendsReceipt, + Value ackByServerAt, + Value retryCount, + Value lastRetry, + Value createdAt, + Value rowid, +}); +typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({ + Value receiptId, + Value contactId, + Value messageId, + Value message, + Value contactWillSendsReceipt, + Value ackByServerAt, + Value retryCount, + Value lastRetry, + Value createdAt, + Value rowid, +}); + +final class $$ReceiptsTableReferences + extends BaseReferences<_$TwonlyDB, $ReceiptsTable, Receipt> { + $$ReceiptsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias( + $_aliasNameGenerator(db.receipts.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias( + $_aliasNameGenerator(db.receipts.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$ReceiptsTableFilterComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get message => $composableBuilder( + column: $table.message, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastRetry => $composableBuilder( + column: $table.lastRetry, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get message => $composableBuilder( + column: $table.message, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastRetry => $composableBuilder( + column: $table.lastRetry, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReceiptsTable> { + $$ReceiptsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get receiptId => + $composableBuilder(column: $table.receiptId, builder: (column) => column); + + GeneratedColumn get message => + $composableBuilder(column: $table.message, builder: (column) => column); + + GeneratedColumn get contactWillSendsReceipt => $composableBuilder( + column: $table.contactWillSendsReceipt, builder: (column) => column); + + GeneratedColumn get ackByServerAt => $composableBuilder( + column: $table.ackByServerAt, builder: (column) => column); + + GeneratedColumn get retryCount => $composableBuilder( + column: $table.retryCount, builder: (column) => column); + + GeneratedColumn get lastRetry => + $composableBuilder(column: $table.lastRetry, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$ReceiptsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReceiptsTable, + Receipt, + $$ReceiptsTableFilterComposer, + $$ReceiptsTableOrderingComposer, + $$ReceiptsTableAnnotationComposer, + $$ReceiptsTableCreateCompanionBuilder, + $$ReceiptsTableUpdateCompanionBuilder, + (Receipt, $$ReceiptsTableReferences), + Receipt, + PrefetchHooks Function({bool contactId, bool messageId})> { + $$ReceiptsTableTableManager(_$TwonlyDB db, $ReceiptsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReceiptsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReceiptsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReceiptsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value receiptId = const Value.absent(), + Value contactId = const Value.absent(), + Value messageId = const Value.absent(), + Value message = const Value.absent(), + Value contactWillSendsReceipt = const Value.absent(), + Value ackByServerAt = const Value.absent(), + Value retryCount = const Value.absent(), + Value lastRetry = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceiptsCompanion( + receiptId: receiptId, + contactId: contactId, + messageId: messageId, + message: message, + contactWillSendsReceipt: contactWillSendsReceipt, + ackByServerAt: ackByServerAt, + retryCount: retryCount, + lastRetry: lastRetry, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String receiptId, + required int contactId, + Value messageId = const Value.absent(), + required Uint8List message, + Value contactWillSendsReceipt = const Value.absent(), + Value ackByServerAt = const Value.absent(), + Value retryCount = const Value.absent(), + Value lastRetry = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceiptsCompanion.insert( + receiptId: receiptId, + contactId: contactId, + messageId: messageId, + message: message, + contactWillSendsReceipt: contactWillSendsReceipt, + ackByServerAt: ackByServerAt, + retryCount: retryCount, + lastRetry: lastRetry, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$ReceiptsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({contactId = false, messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$ReceiptsTableReferences._contactIdTable(db), + referencedColumn: + $$ReceiptsTableReferences._contactIdTable(db).userId, + ) as T; + } + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$ReceiptsTableReferences._messageIdTable(db), + referencedColumn: + $$ReceiptsTableReferences._messageIdTable(db).messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReceiptsTable, + Receipt, + $$ReceiptsTableFilterComposer, + $$ReceiptsTableOrderingComposer, + $$ReceiptsTableAnnotationComposer, + $$ReceiptsTableCreateCompanionBuilder, + $$ReceiptsTableUpdateCompanionBuilder, + (Receipt, $$ReceiptsTableReferences), + Receipt, + PrefetchHooks Function({bool contactId, bool messageId})>; +typedef $$ReceivedReceiptsTableCreateCompanionBuilder + = ReceivedReceiptsCompanion Function({ + required String receiptId, + Value createdAt, + Value rowid, +}); +typedef $$ReceivedReceiptsTableUpdateCompanionBuilder + = ReceivedReceiptsCompanion Function({ + Value receiptId, + Value createdAt, + Value rowid, +}); + +class $$ReceivedReceiptsTableFilterComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$ReceivedReceiptsTableOrderingComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get receiptId => $composableBuilder( + column: $table.receiptId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$ReceivedReceiptsTableAnnotationComposer + extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> { + $$ReceivedReceiptsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get receiptId => + $composableBuilder(column: $table.receiptId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$ReceivedReceiptsTableTableManager extends RootTableManager< + _$TwonlyDB, + $ReceivedReceiptsTable, + ReceivedReceipt, + $$ReceivedReceiptsTableFilterComposer, + $$ReceivedReceiptsTableOrderingComposer, + $$ReceivedReceiptsTableAnnotationComposer, + $$ReceivedReceiptsTableCreateCompanionBuilder, + $$ReceivedReceiptsTableUpdateCompanionBuilder, + ( + ReceivedReceipt, + BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt> + ), + ReceivedReceipt, + PrefetchHooks Function()> { + $$ReceivedReceiptsTableTableManager( + _$TwonlyDB db, $ReceivedReceiptsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ReceivedReceiptsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ReceivedReceiptsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ReceivedReceiptsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value receiptId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceivedReceiptsCompanion( + receiptId: receiptId, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String receiptId, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ReceivedReceiptsCompanion.insert( + receiptId: receiptId, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ReceivedReceiptsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $ReceivedReceiptsTable, + ReceivedReceipt, + $$ReceivedReceiptsTableFilterComposer, + $$ReceivedReceiptsTableOrderingComposer, + $$ReceivedReceiptsTableAnnotationComposer, + $$ReceivedReceiptsTableCreateCompanionBuilder, + $$ReceivedReceiptsTableUpdateCompanionBuilder, + ( + ReceivedReceipt, + BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt> + ), + ReceivedReceipt, + PrefetchHooks Function()>; +typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder + = SignalIdentityKeyStoresCompanion Function({ + required int deviceId, + required String name, + required Uint8List identityKey, + Value createdAt, + Value rowid, +}); +typedef $$SignalIdentityKeyStoresTableUpdateCompanionBuilder + = SignalIdentityKeyStoresCompanion Function({ + Value deviceId, + Value name, + Value identityKey, + Value createdAt, + Value rowid, +}); + +class $$SignalIdentityKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalIdentityKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalIdentityKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalIdentityKeyStoresTable> { + $$SignalIdentityKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get deviceId => + $composableBuilder(column: $table.deviceId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get identityKey => $composableBuilder( + column: $table.identityKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore, + $$SignalIdentityKeyStoresTableFilterComposer, + $$SignalIdentityKeyStoresTableOrderingComposer, + $$SignalIdentityKeyStoresTableAnnotationComposer, + $$SignalIdentityKeyStoresTableCreateCompanionBuilder, + $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, + ( + SignalIdentityKeyStore, + BaseReferences<_$TwonlyDB, $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore> + ), + SignalIdentityKeyStore, + PrefetchHooks Function()> { + $$SignalIdentityKeyStoresTableTableManager( + _$TwonlyDB db, $SignalIdentityKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalIdentityKeyStoresTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalIdentityKeyStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalIdentityKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value deviceId = const Value.absent(), + Value name = const Value.absent(), + Value identityKey = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalIdentityKeyStoresCompanion( + deviceId: deviceId, + name: name, + identityKey: identityKey, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int deviceId, + required String name, + required Uint8List identityKey, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalIdentityKeyStoresCompanion.insert( + deviceId: deviceId, + name: name, + identityKey: identityKey, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalIdentityKeyStoresTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore, + $$SignalIdentityKeyStoresTableFilterComposer, + $$SignalIdentityKeyStoresTableOrderingComposer, + $$SignalIdentityKeyStoresTableAnnotationComposer, + $$SignalIdentityKeyStoresTableCreateCompanionBuilder, + $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, + ( + SignalIdentityKeyStore, + BaseReferences<_$TwonlyDB, $SignalIdentityKeyStoresTable, + SignalIdentityKeyStore> + ), + SignalIdentityKeyStore, + PrefetchHooks Function()>; +typedef $$SignalPreKeyStoresTableCreateCompanionBuilder + = SignalPreKeyStoresCompanion Function({ + Value preKeyId, + required Uint8List preKey, + Value createdAt, +}); +typedef $$SignalPreKeyStoresTableUpdateCompanionBuilder + = SignalPreKeyStoresCompanion Function({ + Value preKeyId, + Value preKey, + Value createdAt, +}); + +class $$SignalPreKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalPreKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalPreKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalPreKeyStoresTable> { + $$SignalPreKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get preKeyId => + $composableBuilder(column: $table.preKeyId, builder: (column) => column); + + GeneratedColumn get preKey => + $composableBuilder(column: $table.preKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalPreKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalPreKeyStoresTable, + SignalPreKeyStore, + $$SignalPreKeyStoresTableFilterComposer, + $$SignalPreKeyStoresTableOrderingComposer, + $$SignalPreKeyStoresTableAnnotationComposer, + $$SignalPreKeyStoresTableCreateCompanionBuilder, + $$SignalPreKeyStoresTableUpdateCompanionBuilder, + ( + SignalPreKeyStore, + BaseReferences<_$TwonlyDB, $SignalPreKeyStoresTable, SignalPreKeyStore> + ), + SignalPreKeyStore, + PrefetchHooks Function()> { + $$SignalPreKeyStoresTableTableManager( + _$TwonlyDB db, $SignalPreKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalPreKeyStoresTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalPreKeyStoresTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SignalPreKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value preKeyId = const Value.absent(), + Value preKey = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SignalPreKeyStoresCompanion( + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value preKeyId = const Value.absent(), + required Uint8List preKey, + Value createdAt = const Value.absent(), + }) => + SignalPreKeyStoresCompanion.insert( + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $SignalPreKeyStoresTable, + SignalPreKeyStore, + $$SignalPreKeyStoresTableFilterComposer, + $$SignalPreKeyStoresTableOrderingComposer, + $$SignalPreKeyStoresTableAnnotationComposer, + $$SignalPreKeyStoresTableCreateCompanionBuilder, + $$SignalPreKeyStoresTableUpdateCompanionBuilder, + ( + SignalPreKeyStore, + BaseReferences<_$TwonlyDB, $SignalPreKeyStoresTable, SignalPreKeyStore> + ), + SignalPreKeyStore, + PrefetchHooks Function()>; +typedef $$SignalSenderKeyStoresTableCreateCompanionBuilder + = SignalSenderKeyStoresCompanion Function({ + required String senderKeyName, + required Uint8List senderKey, + Value rowid, +}); +typedef $$SignalSenderKeyStoresTableUpdateCompanionBuilder + = SignalSenderKeyStoresCompanion Function({ + Value senderKeyName, + Value senderKey, + Value rowid, +}); + +class $$SignalSenderKeyStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get senderKeyName => $composableBuilder( + column: $table.senderKeyName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get senderKey => $composableBuilder( + column: $table.senderKey, builder: (column) => ColumnFilters(column)); +} + +class $$SignalSenderKeyStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get senderKeyName => $composableBuilder( + column: $table.senderKeyName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get senderKey => $composableBuilder( + column: $table.senderKey, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalSenderKeyStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalSenderKeyStoresTable> { + $$SignalSenderKeyStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get senderKeyName => $composableBuilder( + column: $table.senderKeyName, builder: (column) => column); + + GeneratedColumn get senderKey => + $composableBuilder(column: $table.senderKey, builder: (column) => column); +} + +class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalSenderKeyStoresTable, + SignalSenderKeyStore, + $$SignalSenderKeyStoresTableFilterComposer, + $$SignalSenderKeyStoresTableOrderingComposer, + $$SignalSenderKeyStoresTableAnnotationComposer, + $$SignalSenderKeyStoresTableCreateCompanionBuilder, + $$SignalSenderKeyStoresTableUpdateCompanionBuilder, + ( + SignalSenderKeyStore, + BaseReferences<_$TwonlyDB, $SignalSenderKeyStoresTable, + SignalSenderKeyStore> + ), + SignalSenderKeyStore, + PrefetchHooks Function()> { + $$SignalSenderKeyStoresTableTableManager( + _$TwonlyDB db, $SignalSenderKeyStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalSenderKeyStoresTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalSenderKeyStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalSenderKeyStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value senderKeyName = const Value.absent(), + Value senderKey = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSenderKeyStoresCompanion( + senderKeyName: senderKeyName, + senderKey: senderKey, + rowid: rowid, + ), + createCompanionCallback: ({ + required String senderKeyName, + required Uint8List senderKey, + Value rowid = const Value.absent(), + }) => + SignalSenderKeyStoresCompanion.insert( + senderKeyName: senderKeyName, + senderKey: senderKey, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalSenderKeyStoresTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalSenderKeyStoresTable, + SignalSenderKeyStore, + $$SignalSenderKeyStoresTableFilterComposer, + $$SignalSenderKeyStoresTableOrderingComposer, + $$SignalSenderKeyStoresTableAnnotationComposer, + $$SignalSenderKeyStoresTableCreateCompanionBuilder, + $$SignalSenderKeyStoresTableUpdateCompanionBuilder, + ( + SignalSenderKeyStore, + BaseReferences<_$TwonlyDB, $SignalSenderKeyStoresTable, + SignalSenderKeyStore> + ), + SignalSenderKeyStore, + PrefetchHooks Function()>; +typedef $$SignalSessionStoresTableCreateCompanionBuilder + = SignalSessionStoresCompanion Function({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + Value createdAt, + Value rowid, +}); +typedef $$SignalSessionStoresTableUpdateCompanionBuilder + = SignalSessionStoresCompanion Function({ + Value deviceId, + Value name, + Value sessionRecord, + Value createdAt, + Value rowid, +}); + +class $$SignalSessionStoresTableFilterComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get sessionRecord => $composableBuilder( + column: $table.sessionRecord, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); +} + +class $$SignalSessionStoresTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get deviceId => $composableBuilder( + column: $table.deviceId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get sessionRecord => $composableBuilder( + column: $table.sessionRecord, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); +} + +class $$SignalSessionStoresTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalSessionStoresTable> { + $$SignalSessionStoresTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get deviceId => + $composableBuilder(column: $table.deviceId, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get sessionRecord => $composableBuilder( + column: $table.sessionRecord, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); +} + +class $$SignalSessionStoresTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalSessionStoresTable, + SignalSessionStore, + $$SignalSessionStoresTableFilterComposer, + $$SignalSessionStoresTableOrderingComposer, + $$SignalSessionStoresTableAnnotationComposer, + $$SignalSessionStoresTableCreateCompanionBuilder, + $$SignalSessionStoresTableUpdateCompanionBuilder, + ( + SignalSessionStore, + BaseReferences<_$TwonlyDB, $SignalSessionStoresTable, SignalSessionStore> + ), + SignalSessionStore, + PrefetchHooks Function()> { + $$SignalSessionStoresTableTableManager( + _$TwonlyDB db, $SignalSessionStoresTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalSessionStoresTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalSessionStoresTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalSessionStoresTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value deviceId = const Value.absent(), + Value name = const Value.absent(), + Value sessionRecord = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSessionStoresCompanion( + deviceId: deviceId, + name: name, + sessionRecord: sessionRecord, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int deviceId, + required String name, + required Uint8List sessionRecord, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalSessionStoresCompanion.insert( + deviceId: deviceId, + name: name, + sessionRecord: sessionRecord, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $SignalSessionStoresTable, + SignalSessionStore, + $$SignalSessionStoresTableFilterComposer, + $$SignalSessionStoresTableOrderingComposer, + $$SignalSessionStoresTableAnnotationComposer, + $$SignalSessionStoresTableCreateCompanionBuilder, + $$SignalSessionStoresTableUpdateCompanionBuilder, + ( + SignalSessionStore, + BaseReferences<_$TwonlyDB, $SignalSessionStoresTable, SignalSessionStore> + ), + SignalSessionStore, + PrefetchHooks Function()>; +typedef $$SignalContactPreKeysTableCreateCompanionBuilder + = SignalContactPreKeysCompanion Function({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + Value createdAt, + Value rowid, +}); +typedef $$SignalContactPreKeysTableUpdateCompanionBuilder + = SignalContactPreKeysCompanion Function({ + Value contactId, + Value preKeyId, + Value preKey, + Value createdAt, + Value rowid, +}); + +final class $$SignalContactPreKeysTableReferences extends BaseReferences< + _$TwonlyDB, $SignalContactPreKeysTable, SignalContactPreKey> { + $$SignalContactPreKeysTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.signalContactPreKeys.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$SignalContactPreKeysTableFilterComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get preKeyId => $composableBuilder( + column: $table.preKeyId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get preKey => $composableBuilder( + column: $table.preKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalContactPreKeysTable> { + $$SignalContactPreKeysTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get preKeyId => + $composableBuilder(column: $table.preKeyId, builder: (column) => column); + + GeneratedColumn get preKey => + $composableBuilder(column: $table.preKey, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactPreKeysTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalContactPreKeysTable, + SignalContactPreKey, + $$SignalContactPreKeysTableFilterComposer, + $$SignalContactPreKeysTableOrderingComposer, + $$SignalContactPreKeysTableAnnotationComposer, + $$SignalContactPreKeysTableCreateCompanionBuilder, + $$SignalContactPreKeysTableUpdateCompanionBuilder, + (SignalContactPreKey, $$SignalContactPreKeysTableReferences), + SignalContactPreKey, + PrefetchHooks Function({bool contactId})> { + $$SignalContactPreKeysTableTableManager( + _$TwonlyDB db, $SignalContactPreKeysTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalContactPreKeysTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SignalContactPreKeysTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalContactPreKeysTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value contactId = const Value.absent(), + Value preKeyId = const Value.absent(), + Value preKey = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalContactPreKeysCompanion( + contactId: contactId, + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required int contactId, + required int preKeyId, + required Uint8List preKey, + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SignalContactPreKeysCompanion.insert( + contactId: contactId, + preKeyId: preKeyId, + preKey: preKey, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$SignalContactPreKeysTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: $$SignalContactPreKeysTableReferences + ._contactIdTable(db), + referencedColumn: $$SignalContactPreKeysTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$SignalContactPreKeysTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalContactPreKeysTable, + SignalContactPreKey, + $$SignalContactPreKeysTableFilterComposer, + $$SignalContactPreKeysTableOrderingComposer, + $$SignalContactPreKeysTableAnnotationComposer, + $$SignalContactPreKeysTableCreateCompanionBuilder, + $$SignalContactPreKeysTableUpdateCompanionBuilder, + (SignalContactPreKey, $$SignalContactPreKeysTableReferences), + SignalContactPreKey, + PrefetchHooks Function({bool contactId})>; +typedef $$SignalContactSignedPreKeysTableCreateCompanionBuilder + = SignalContactSignedPreKeysCompanion Function({ + Value contactId, + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + Value createdAt, +}); +typedef $$SignalContactSignedPreKeysTableUpdateCompanionBuilder + = SignalContactSignedPreKeysCompanion Function({ + Value contactId, + Value signedPreKeyId, + Value signedPreKey, + Value signedPreKeySignature, + Value createdAt, +}); + +final class $$SignalContactSignedPreKeysTableReferences extends BaseReferences< + _$TwonlyDB, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> { + $$SignalContactSignedPreKeysTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.signalContactSignedPreKeys.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager get contactId { + final $_column = $_itemColumn('contact_id')!; + + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$SignalContactSignedPreKeysTableFilterComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get signedPreKey => $composableBuilder( + column: $table.signedPreKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableOrderingComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get signedPreKey => $composableBuilder( + column: $table.signedPreKey, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableAnnotationComposer + extends Composer<_$TwonlyDB, $SignalContactSignedPreKeysTable> { + $$SignalContactSignedPreKeysTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get signedPreKeyId => $composableBuilder( + column: $table.signedPreKeyId, builder: (column) => column); + + GeneratedColumn get signedPreKey => $composableBuilder( + column: $table.signedPreKey, builder: (column) => column); + + GeneratedColumn get signedPreKeySignature => $composableBuilder( + column: $table.signedPreKeySignature, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< + _$TwonlyDB, + $SignalContactSignedPreKeysTable, + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableFilterComposer, + $$SignalContactSignedPreKeysTableOrderingComposer, + $$SignalContactSignedPreKeysTableAnnotationComposer, + $$SignalContactSignedPreKeysTableCreateCompanionBuilder, + $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, + (SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableReferences), + SignalContactSignedPreKey, + PrefetchHooks Function({bool contactId})> { + $$SignalContactSignedPreKeysTableTableManager( + _$TwonlyDB db, $SignalContactSignedPreKeysTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SignalContactSignedPreKeysTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + $$SignalContactSignedPreKeysTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + $$SignalContactSignedPreKeysTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + Value contactId = const Value.absent(), + Value signedPreKeyId = const Value.absent(), + Value signedPreKey = const Value.absent(), + Value signedPreKeySignature = const Value.absent(), + Value createdAt = const Value.absent(), + }) => + SignalContactSignedPreKeysCompanion( + contactId: contactId, + signedPreKeyId: signedPreKeyId, + signedPreKey: signedPreKey, + signedPreKeySignature: signedPreKeySignature, + createdAt: createdAt, + ), + createCompanionCallback: ({ + Value contactId = const Value.absent(), + required int signedPreKeyId, + required Uint8List signedPreKey, + required Uint8List signedPreKeySignature, + Value createdAt = const Value.absent(), + }) => + SignalContactSignedPreKeysCompanion.insert( + contactId: contactId, + signedPreKeyId: signedPreKeyId, + signedPreKey: signedPreKey, + signedPreKeySignature: signedPreKeySignature, + createdAt: createdAt, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$SignalContactSignedPreKeysTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({contactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: $$SignalContactSignedPreKeysTableReferences + ._contactIdTable(db), + referencedColumn: + $$SignalContactSignedPreKeysTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$SignalContactSignedPreKeysTableProcessedTableManager + = ProcessedTableManager< + _$TwonlyDB, + $SignalContactSignedPreKeysTable, + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableFilterComposer, + $$SignalContactSignedPreKeysTableOrderingComposer, + $$SignalContactSignedPreKeysTableAnnotationComposer, + $$SignalContactSignedPreKeysTableCreateCompanionBuilder, + $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, + ( + SignalContactSignedPreKey, + $$SignalContactSignedPreKeysTableReferences + ), + SignalContactSignedPreKey, + PrefetchHooks Function({bool contactId})>; +typedef $$MessageActionsTableCreateCompanionBuilder = MessageActionsCompanion + Function({ + required String messageId, + required int contactId, + required MessageActionType type, + Value actionAt, + Value rowid, +}); +typedef $$MessageActionsTableUpdateCompanionBuilder = MessageActionsCompanion + Function({ + Value messageId, + Value contactId, + Value type, + Value actionAt, + Value rowid, +}); + +final class $$MessageActionsTableReferences + extends BaseReferences<_$TwonlyDB, $MessageActionsTable, MessageAction> { + $$MessageActionsTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _messageIdTable(_$TwonlyDB db) => + db.messages.createAlias($_aliasNameGenerator( + db.messageActions.messageId, db.messages.messageId)); + + $$MessagesTableProcessedTableManager get messageId { + final $_column = $_itemColumn('message_id')!; + + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.messageId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$MessageActionsTableFilterComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnFilters(column)); + + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableOrderingComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get contactId => $composableBuilder( + column: $table.contactId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnOrderings(column)); + + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableAnnotationComposer + extends Composer<_$TwonlyDB, $MessageActionsTable> { + $$MessageActionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get contactId => + $composableBuilder(column: $table.contactId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get actionAt => + $composableBuilder(column: $table.actionAt, builder: (column) => column); + + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$MessageActionsTableTableManager extends RootTableManager< + _$TwonlyDB, + $MessageActionsTable, + MessageAction, + $$MessageActionsTableFilterComposer, + $$MessageActionsTableOrderingComposer, + $$MessageActionsTableAnnotationComposer, + $$MessageActionsTableCreateCompanionBuilder, + $$MessageActionsTableUpdateCompanionBuilder, + (MessageAction, $$MessageActionsTableReferences), + MessageAction, + PrefetchHooks Function({bool messageId})> { + $$MessageActionsTableTableManager(_$TwonlyDB db, $MessageActionsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MessageActionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MessageActionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MessageActionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value messageId = const Value.absent(), + Value contactId = const Value.absent(), + Value type = const Value.absent(), + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessageActionsCompanion( + messageId: messageId, + contactId: contactId, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String messageId, + required int contactId, + required MessageActionType type, + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MessageActionsCompanion.insert( + messageId: messageId, + contactId: contactId, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$MessageActionsTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({messageId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (messageId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: + $$MessageActionsTableReferences._messageIdTable(db), + referencedColumn: $$MessageActionsTableReferences + ._messageIdTable(db) + .messageId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$MessageActionsTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $MessageActionsTable, + MessageAction, + $$MessageActionsTableFilterComposer, + $$MessageActionsTableOrderingComposer, + $$MessageActionsTableAnnotationComposer, + $$MessageActionsTableCreateCompanionBuilder, + $$MessageActionsTableUpdateCompanionBuilder, + (MessageAction, $$MessageActionsTableReferences), + MessageAction, + PrefetchHooks Function({bool messageId})>; +typedef $$GroupHistoriesTableCreateCompanionBuilder = GroupHistoriesCompanion + Function({ + required String groupHistoryId, + required String groupId, + Value contactId, + Value affectedContactId, + Value oldGroupName, + Value newGroupName, + Value newDeleteMessagesAfterMilliseconds, + required GroupActionType type, + Value actionAt, + Value rowid, +}); +typedef $$GroupHistoriesTableUpdateCompanionBuilder = GroupHistoriesCompanion + Function({ + Value groupHistoryId, + Value groupId, + Value contactId, + Value affectedContactId, + Value oldGroupName, + Value newGroupName, + Value newDeleteMessagesAfterMilliseconds, + Value type, + Value actionAt, + Value rowid, +}); + +final class $$GroupHistoriesTableReferences + extends BaseReferences<_$TwonlyDB, $GroupHistoriesTable, GroupHistory> { + $$GroupHistoriesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $GroupsTable _groupIdTable(_$TwonlyDB db) => db.groups.createAlias( + $_aliasNameGenerator(db.groupHistories.groupId, db.groups.groupId)); + + $$GroupsTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupsTableTableManager($_db, $_db.groups) + .filter((f) => f.groupId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _contactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.groupHistories.contactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get contactId { + final $_column = $_itemColumn('contact_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_contactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static $ContactsTable _affectedContactIdTable(_$TwonlyDB db) => + db.contacts.createAlias($_aliasNameGenerator( + db.groupHistories.affectedContactId, db.contacts.userId)); + + $$ContactsTableProcessedTableManager? get affectedContactId { + final $_column = $_itemColumn('affected_contact_id'); + if ($_column == null) return null; + final manager = $$ContactsTableTableManager($_db, $_db.contacts) + .filter((f) => f.userId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_affectedContactIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$GroupHistoriesTableFilterComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get oldGroupName => $composableBuilder( + column: $table.oldGroupName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get newGroupName => $composableBuilder( + column: $table.newGroupName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnFilters(column)); + + $$GroupsTableFilterComposer get groupId { + final $$GroupsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableFilterComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get contactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableFilterComposer get affectedContactId { + final $$ContactsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableFilterComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableOrderingComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get oldGroupName => $composableBuilder( + column: $table.oldGroupName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get newGroupName => $composableBuilder( + column: $table.newGroupName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get actionAt => $composableBuilder( + column: $table.actionAt, builder: (column) => ColumnOrderings(column)); + + $$GroupsTableOrderingComposer get groupId { + final $$GroupsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableOrderingComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get contactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableOrderingComposer get affectedContactId { + final $$ContactsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableOrderingComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableAnnotationComposer + extends Composer<_$TwonlyDB, $GroupHistoriesTable> { + $$GroupHistoriesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get groupHistoryId => $composableBuilder( + column: $table.groupHistoryId, builder: (column) => column); + + GeneratedColumn get oldGroupName => $composableBuilder( + column: $table.oldGroupName, builder: (column) => column); + + GeneratedColumn get newGroupName => $composableBuilder( + column: $table.newGroupName, builder: (column) => column); + + GeneratedColumn get newDeleteMessagesAfterMilliseconds => + $composableBuilder( + column: $table.newDeleteMessagesAfterMilliseconds, + builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get actionAt => + $composableBuilder(column: $table.actionAt, builder: (column) => column); + + $$GroupsTableAnnotationComposer get groupId { + final $$GroupsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groups, + getReferencedColumn: (t) => t.groupId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$GroupsTableAnnotationComposer( + $db: $db, + $table: $db.groups, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get contactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.contactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + $$ContactsTableAnnotationComposer get affectedContactId { + final $$ContactsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.affectedContactId, + referencedTable: $db.contacts, + getReferencedColumn: (t) => t.userId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ContactsTableAnnotationComposer( + $db: $db, + $table: $db.contacts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$GroupHistoriesTableTableManager extends RootTableManager< + _$TwonlyDB, + $GroupHistoriesTable, + GroupHistory, + $$GroupHistoriesTableFilterComposer, + $$GroupHistoriesTableOrderingComposer, + $$GroupHistoriesTableAnnotationComposer, + $$GroupHistoriesTableCreateCompanionBuilder, + $$GroupHistoriesTableUpdateCompanionBuilder, + (GroupHistory, $$GroupHistoriesTableReferences), + GroupHistory, + PrefetchHooks Function( + {bool groupId, bool contactId, bool affectedContactId})> { + $$GroupHistoriesTableTableManager(_$TwonlyDB db, $GroupHistoriesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupHistoriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupHistoriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupHistoriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value groupHistoryId = const Value.absent(), + Value groupId = const Value.absent(), + Value contactId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = + const Value.absent(), + Value type = const Value.absent(), + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupHistoriesCompanion( + groupHistoryId: groupHistoryId, + groupId: groupId, + contactId: contactId, + affectedContactId: affectedContactId, + oldGroupName: oldGroupName, + newGroupName: newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + createCompanionCallback: ({ + required String groupHistoryId, + required String groupId, + Value contactId = const Value.absent(), + Value affectedContactId = const Value.absent(), + Value oldGroupName = const Value.absent(), + Value newGroupName = const Value.absent(), + Value newDeleteMessagesAfterMilliseconds = + const Value.absent(), + required GroupActionType type, + Value actionAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => + GroupHistoriesCompanion.insert( + groupHistoryId: groupHistoryId, + groupId: groupId, + contactId: contactId, + affectedContactId: affectedContactId, + oldGroupName: oldGroupName, + newGroupName: newGroupName, + newDeleteMessagesAfterMilliseconds: + newDeleteMessagesAfterMilliseconds, + type: type, + actionAt: actionAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + $$GroupHistoriesTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ( + {groupId = false, contactId = false, affectedContactId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (groupId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$GroupHistoriesTableReferences._groupIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._groupIdTable(db) + .groupId, + ) as T; + } + if (contactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.contactId, + referencedTable: + $$GroupHistoriesTableReferences._contactIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._contactIdTable(db) + .userId, + ) as T; + } + if (affectedContactId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.affectedContactId, + referencedTable: $$GroupHistoriesTableReferences + ._affectedContactIdTable(db), + referencedColumn: $$GroupHistoriesTableReferences + ._affectedContactIdTable(db) + .userId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$GroupHistoriesTableProcessedTableManager = ProcessedTableManager< + _$TwonlyDB, + $GroupHistoriesTable, + GroupHistory, + $$GroupHistoriesTableFilterComposer, + $$GroupHistoriesTableOrderingComposer, + $$GroupHistoriesTableAnnotationComposer, + $$GroupHistoriesTableCreateCompanionBuilder, + $$GroupHistoriesTableUpdateCompanionBuilder, + (GroupHistory, $$GroupHistoriesTableReferences), + GroupHistory, + PrefetchHooks Function( + {bool groupId, bool contactId, bool affectedContactId})>; + +class $TwonlyDBManager { + final _$TwonlyDB _db; + $TwonlyDBManager(this._db); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db, _db.contacts); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db, _db.groups); + $$MediaFilesTableTableManager get mediaFiles => + $$MediaFilesTableTableManager(_db, _db.mediaFiles); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db, _db.messages); + $$MessageHistoriesTableTableManager get messageHistories => + $$MessageHistoriesTableTableManager(_db, _db.messageHistories); + $$ReactionsTableTableManager get reactions => + $$ReactionsTableTableManager(_db, _db.reactions); + $$GroupMembersTableTableManager get groupMembers => + $$GroupMembersTableTableManager(_db, _db.groupMembers); + $$ReceiptsTableTableManager get receipts => + $$ReceiptsTableTableManager(_db, _db.receipts); + $$ReceivedReceiptsTableTableManager get receivedReceipts => + $$ReceivedReceiptsTableTableManager(_db, _db.receivedReceipts); + $$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores => + $$SignalIdentityKeyStoresTableTableManager( + _db, _db.signalIdentityKeyStores); + $$SignalPreKeyStoresTableTableManager get signalPreKeyStores => + $$SignalPreKeyStoresTableTableManager(_db, _db.signalPreKeyStores); + $$SignalSenderKeyStoresTableTableManager get signalSenderKeyStores => + $$SignalSenderKeyStoresTableTableManager(_db, _db.signalSenderKeyStores); + $$SignalSessionStoresTableTableManager get signalSessionStores => + $$SignalSessionStoresTableTableManager(_db, _db.signalSessionStores); + $$SignalContactPreKeysTableTableManager get signalContactPreKeys => + $$SignalContactPreKeysTableTableManager(_db, _db.signalContactPreKeys); + $$SignalContactSignedPreKeysTableTableManager + get signalContactSignedPreKeys => + $$SignalContactSignedPreKeysTableTableManager( + _db, _db.signalContactSignedPreKeys); + $$MessageActionsTableTableManager get messageActions => + $$MessageActionsTableTableManager(_db, _db.messageActions); + $$GroupHistoriesTableTableManager get groupHistories => + $$GroupHistoriesTableTableManager(_db, _db.groupHistories); +} diff --git a/lib/src/database/twonly_database.dart b/lib/src/database/twonly_database_old.dart similarity index 80% rename from lib/src/database/twonly_database.dart rename to lib/src/database/twonly_database_old.dart index 65a88ec..1125a21 100644 --- a/lib/src/database/twonly_database.dart +++ b/lib/src/database/twonly_database_old.dart @@ -2,25 +2,20 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; import 'package:path_provider/path_provider.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/daos/media_uploads_dao.dart'; -import 'package:twonly/src/database/daos/message_retransmissions.dao.dart'; -import 'package:twonly/src/database/daos/messages_dao.dart'; -import 'package:twonly/src/database/daos/signal_dao.dart'; -import 'package:twonly/src/database/tables/contacts_table.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/message_retransmissions.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; -import 'package:twonly/src/database/tables/signal_identity_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_pre_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_sender_key_store_table.dart'; -import 'package:twonly/src/database/tables/signal_session_store_table.dart'; -import 'package:twonly/src/database/twonly_database.steps.dart'; +import 'package:twonly/src/database/tables_old/contacts_table.dart'; +import 'package:twonly/src/database/tables_old/media_uploads_table.dart'; +import 'package:twonly/src/database/tables_old/message_retransmissions.dart'; +import 'package:twonly/src/database/tables_old/messages_table.dart'; +import 'package:twonly/src/database/tables_old/signal_contact_prekey_table.dart'; +import 'package:twonly/src/database/tables_old/signal_contact_signed_prekey_table.dart'; +import 'package:twonly/src/database/tables_old/signal_identity_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_pre_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_sender_key_store_table.dart'; +import 'package:twonly/src/database/tables_old/signal_session_store_table.dart'; +import 'package:twonly/src/database/twonly_database_old.steps.dart'; import 'package:twonly/src/utils/log.dart'; -part 'twonly_database.g.dart'; +part 'twonly_database_old.g.dart'; // You can then create a database class that includes this table @DriftDatabase( @@ -36,22 +31,16 @@ part 'twonly_database.g.dart'; SignalContactSignedPreKeys, MessageRetransmissions, ], - daos: [ - MessagesDao, - ContactsDao, - MediaUploadsDao, - SignalDao, - MessageRetransmissionDao, - ], + daos: [], ) -class TwonlyDatabase extends _$TwonlyDatabase { - TwonlyDatabase([QueryExecutor? e]) +class TwonlyDatabaseOld extends _$TwonlyDatabaseOld { + TwonlyDatabaseOld([QueryExecutor? e]) : super( e ?? _openConnection(), ); // ignore: matching_super_parameters - TwonlyDatabase.forTesting(DatabaseConnection super.connection); + TwonlyDatabaseOld.forTesting(DatabaseConnection super.connection); @override int get schemaVersion => 17; diff --git a/lib/src/database/twonly_database.g.dart b/lib/src/database/twonly_database_old.g.dart similarity index 98% rename from lib/src/database/twonly_database.g.dart rename to lib/src/database/twonly_database_old.g.dart index bef787d..ed59521 100644 --- a/lib/src/database/twonly_database.g.dart +++ b/lib/src/database/twonly_database_old.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'twonly_database.dart'; +part of 'twonly_database_old.dart'; // ignore_for_file: type=lint class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { @@ -4443,9 +4443,9 @@ class MessageRetransmissionsCompanion } } -abstract class _$TwonlyDatabase extends GeneratedDatabase { - _$TwonlyDatabase(QueryExecutor e) : super(e); - $TwonlyDatabaseManager get managers => $TwonlyDatabaseManager(this); +abstract class _$TwonlyDatabaseOld extends GeneratedDatabase { + _$TwonlyDatabaseOld(QueryExecutor e) : super(e); + $TwonlyDatabaseOldManager get managers => $TwonlyDatabaseOldManager(this); late final $ContactsTable contacts = $ContactsTable(this); late final $MessagesTable messages = $MessagesTable(this); late final $MediaUploadsTable mediaUploads = $MediaUploadsTable(this); @@ -4463,13 +4463,6 @@ abstract class _$TwonlyDatabase extends GeneratedDatabase { $SignalContactSignedPreKeysTable(this); late final $MessageRetransmissionsTable messageRetransmissions = $MessageRetransmissionsTable(this); - late final MessagesDao messagesDao = MessagesDao(this as TwonlyDatabase); - late final ContactsDao contactsDao = ContactsDao(this as TwonlyDatabase); - late final MediaUploadsDao mediaUploadsDao = - MediaUploadsDao(this as TwonlyDatabase); - late final SignalDao signalDao = SignalDao(this as TwonlyDatabase); - late final MessageRetransmissionDao messageRetransmissionDao = - MessageRetransmissionDao(this as TwonlyDatabase); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -4559,11 +4552,11 @@ typedef $$ContactsTableUpdateCompanionBuilder = ContactsCompanion Function({ }); final class $$ContactsTableReferences - extends BaseReferences<_$TwonlyDatabase, $ContactsTable, Contact> { + extends BaseReferences<_$TwonlyDatabaseOld, $ContactsTable, Contact> { $$ContactsTableReferences(super.$_db, super.$_table, super.$_typedResult); static MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messages, aliasName: $_aliasNameGenerator(db.contacts.userId, db.messages.contactId)); @@ -4579,7 +4572,7 @@ final class $$ContactsTableReferences static MultiTypedResultKey<$MessageRetransmissionsTable, List> _messageRetransmissionsRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messageRetransmissions, aliasName: $_aliasNameGenerator( db.contacts.userId, db.messageRetransmissions.contactId)); @@ -4599,7 +4592,7 @@ final class $$ContactsTableReferences } class $$ContactsTableFilterComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableFilterComposer({ required super.$db, required super.$table, @@ -4730,7 +4723,7 @@ class $$ContactsTableFilterComposer } class $$ContactsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableOrderingComposer({ required super.$db, required super.$table, @@ -4819,7 +4812,7 @@ class $$ContactsTableOrderingComposer } class $$ContactsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $ContactsTable> { + extends Composer<_$TwonlyDatabaseOld, $ContactsTable> { $$ContactsTableAnnotationComposer({ required super.$db, required super.$table, @@ -4942,7 +4935,7 @@ class $$ContactsTableAnnotationComposer } class $$ContactsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $ContactsTable, Contact, $$ContactsTableFilterComposer, @@ -4954,7 +4947,7 @@ class $$ContactsTableTableManager extends RootTableManager< Contact, PrefetchHooks Function( {bool messagesRefs, bool messageRetransmissionsRefs})> { - $$ContactsTableTableManager(_$TwonlyDatabase db, $ContactsTable table) + $$ContactsTableTableManager(_$TwonlyDatabaseOld db, $ContactsTable table) : super(TableManagerState( db: db, table: table, @@ -5112,7 +5105,7 @@ class $$ContactsTableTableManager extends RootTableManager< } typedef $$ContactsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $ContactsTable, Contact, $$ContactsTableFilterComposer, @@ -5166,10 +5159,10 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ }); final class $$MessagesTableReferences - extends BaseReferences<_$TwonlyDatabase, $MessagesTable, Message> { + extends BaseReferences<_$TwonlyDatabaseOld, $MessagesTable, Message> { $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ContactsTable _contactIdTable(_$TwonlyDatabase db) => + static $ContactsTable _contactIdTable(_$TwonlyDatabaseOld db) => db.contacts.createAlias( $_aliasNameGenerator(db.messages.contactId, db.contacts.userId)); @@ -5186,7 +5179,7 @@ final class $$MessagesTableReferences static MultiTypedResultKey<$MessageRetransmissionsTable, List> _messageRetransmissionsRefsTable( - _$TwonlyDatabase db) => + _$TwonlyDatabaseOld db) => MultiTypedResultKey.fromTable(db.messageRetransmissions, aliasName: $_aliasNameGenerator( db.messages.messageId, db.messageRetransmissions.messageId)); @@ -5206,7 +5199,7 @@ final class $$MessagesTableReferences } class $$MessagesTableFilterComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableFilterComposer({ required super.$db, required super.$table, @@ -5324,7 +5317,7 @@ class $$MessagesTableFilterComposer } class $$MessagesTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableOrderingComposer({ required super.$db, required super.$table, @@ -5415,7 +5408,7 @@ class $$MessagesTableOrderingComposer } class $$MessagesTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MessagesTable> { + extends Composer<_$TwonlyDatabaseOld, $MessagesTable> { $$MessagesTableAnnotationComposer({ required super.$db, required super.$table, @@ -5521,7 +5514,7 @@ class $$MessagesTableAnnotationComposer } class $$MessagesTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessagesTable, Message, $$MessagesTableFilterComposer, @@ -5532,7 +5525,7 @@ class $$MessagesTableTableManager extends RootTableManager< (Message, $$MessagesTableReferences), Message, PrefetchHooks Function({bool contactId, bool messageRetransmissionsRefs})> { - $$MessagesTableTableManager(_$TwonlyDatabase db, $MessagesTable table) + $$MessagesTableTableManager(_$TwonlyDatabaseOld db, $MessagesTable table) : super(TableManagerState( db: db, table: table, @@ -5684,7 +5677,7 @@ class $$MessagesTableTableManager extends RootTableManager< } typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessagesTable, Message, $$MessagesTableFilterComposer, @@ -5713,7 +5706,7 @@ typedef $$MediaUploadsTableUpdateCompanionBuilder = MediaUploadsCompanion }); class $$MediaUploadsTableFilterComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableFilterComposer({ required super.$db, required super.$table, @@ -5748,7 +5741,7 @@ class $$MediaUploadsTableFilterComposer } class $$MediaUploadsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableOrderingComposer({ required super.$db, required super.$table, @@ -5775,7 +5768,7 @@ class $$MediaUploadsTableOrderingComposer } class $$MediaUploadsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MediaUploadsTable> { + extends Composer<_$TwonlyDatabaseOld, $MediaUploadsTable> { $$MediaUploadsTableAnnotationComposer({ required super.$db, required super.$table, @@ -5802,7 +5795,7 @@ class $$MediaUploadsTableAnnotationComposer } class $$MediaUploadsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload, $$MediaUploadsTableFilterComposer, @@ -5812,11 +5805,12 @@ class $$MediaUploadsTableTableManager extends RootTableManager< $$MediaUploadsTableUpdateCompanionBuilder, ( MediaUpload, - BaseReferences<_$TwonlyDatabase, $MediaUploadsTable, MediaUpload> + BaseReferences<_$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload> ), MediaUpload, PrefetchHooks Function()> { - $$MediaUploadsTableTableManager(_$TwonlyDatabase db, $MediaUploadsTable table) + $$MediaUploadsTableTableManager( + _$TwonlyDatabaseOld db, $MediaUploadsTable table) : super(TableManagerState( db: db, table: table, @@ -5862,7 +5856,7 @@ class $$MediaUploadsTableTableManager extends RootTableManager< } typedef $$MediaUploadsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload, $$MediaUploadsTableFilterComposer, @@ -5872,7 +5866,7 @@ typedef $$MediaUploadsTableProcessedTableManager = ProcessedTableManager< $$MediaUploadsTableUpdateCompanionBuilder, ( MediaUpload, - BaseReferences<_$TwonlyDatabase, $MediaUploadsTable, MediaUpload> + BaseReferences<_$TwonlyDatabaseOld, $MediaUploadsTable, MediaUpload> ), MediaUpload, PrefetchHooks Function()>; @@ -5894,7 +5888,7 @@ typedef $$SignalIdentityKeyStoresTableUpdateCompanionBuilder }); class $$SignalIdentityKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -5916,7 +5910,7 @@ class $$SignalIdentityKeyStoresTableFilterComposer } class $$SignalIdentityKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -5938,7 +5932,7 @@ class $$SignalIdentityKeyStoresTableOrderingComposer } class $$SignalIdentityKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalIdentityKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable> { $$SignalIdentityKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -5960,7 +5954,7 @@ class $$SignalIdentityKeyStoresTableAnnotationComposer } class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore, $$SignalIdentityKeyStoresTableFilterComposer, @@ -5970,13 +5964,13 @@ class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, ( SignalIdentityKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalIdentityKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore> ), SignalIdentityKeyStore, PrefetchHooks Function()> { $$SignalIdentityKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalIdentityKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalIdentityKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6026,7 +6020,7 @@ class $$SignalIdentityKeyStoresTableTableManager extends RootTableManager< typedef $$SignalIdentityKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore, $$SignalIdentityKeyStoresTableFilterComposer, @@ -6036,7 +6030,7 @@ typedef $$SignalIdentityKeyStoresTableProcessedTableManager $$SignalIdentityKeyStoresTableUpdateCompanionBuilder, ( SignalIdentityKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalIdentityKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalIdentityKeyStoresTable, SignalIdentityKeyStore> ), SignalIdentityKeyStore, @@ -6055,7 +6049,7 @@ typedef $$SignalPreKeyStoresTableUpdateCompanionBuilder }); class $$SignalPreKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6074,7 +6068,7 @@ class $$SignalPreKeyStoresTableFilterComposer } class $$SignalPreKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6093,7 +6087,7 @@ class $$SignalPreKeyStoresTableOrderingComposer } class $$SignalPreKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalPreKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable> { $$SignalPreKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6112,7 +6106,7 @@ class $$SignalPreKeyStoresTableAnnotationComposer } class $$SignalPreKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore, $$SignalPreKeyStoresTableFilterComposer, @@ -6122,13 +6116,13 @@ class $$SignalPreKeyStoresTableTableManager extends RootTableManager< $$SignalPreKeyStoresTableUpdateCompanionBuilder, ( SignalPreKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalPreKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore> ), SignalPreKeyStore, PrefetchHooks Function()> { $$SignalPreKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalPreKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalPreKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6167,7 +6161,7 @@ class $$SignalPreKeyStoresTableTableManager extends RootTableManager< } typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore, $$SignalPreKeyStoresTableFilterComposer, @@ -6177,7 +6171,7 @@ typedef $$SignalPreKeyStoresTableProcessedTableManager = ProcessedTableManager< $$SignalPreKeyStoresTableUpdateCompanionBuilder, ( SignalPreKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalPreKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalPreKeyStoresTable, SignalPreKeyStore> ), SignalPreKeyStore, @@ -6196,7 +6190,7 @@ typedef $$SignalSenderKeyStoresTableUpdateCompanionBuilder }); class $$SignalSenderKeyStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6212,7 +6206,7 @@ class $$SignalSenderKeyStoresTableFilterComposer } class $$SignalSenderKeyStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6229,7 +6223,7 @@ class $$SignalSenderKeyStoresTableOrderingComposer } class $$SignalSenderKeyStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalSenderKeyStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable> { $$SignalSenderKeyStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6245,7 +6239,7 @@ class $$SignalSenderKeyStoresTableAnnotationComposer } class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore, $$SignalSenderKeyStoresTableFilterComposer, @@ -6255,13 +6249,13 @@ class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< $$SignalSenderKeyStoresTableUpdateCompanionBuilder, ( SignalSenderKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalSenderKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore> ), SignalSenderKeyStore, PrefetchHooks Function()> { $$SignalSenderKeyStoresTableTableManager( - _$TwonlyDatabase db, $SignalSenderKeyStoresTable table) + _$TwonlyDatabaseOld db, $SignalSenderKeyStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6303,7 +6297,7 @@ class $$SignalSenderKeyStoresTableTableManager extends RootTableManager< typedef $$SignalSenderKeyStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore, $$SignalSenderKeyStoresTableFilterComposer, @@ -6313,7 +6307,7 @@ typedef $$SignalSenderKeyStoresTableProcessedTableManager $$SignalSenderKeyStoresTableUpdateCompanionBuilder, ( SignalSenderKeyStore, - BaseReferences<_$TwonlyDatabase, $SignalSenderKeyStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSenderKeyStoresTable, SignalSenderKeyStore> ), SignalSenderKeyStore, @@ -6336,7 +6330,7 @@ typedef $$SignalSessionStoresTableUpdateCompanionBuilder }); class $$SignalSessionStoresTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableFilterComposer({ required super.$db, required super.$table, @@ -6358,7 +6352,7 @@ class $$SignalSessionStoresTableFilterComposer } class $$SignalSessionStoresTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableOrderingComposer({ required super.$db, required super.$table, @@ -6381,7 +6375,7 @@ class $$SignalSessionStoresTableOrderingComposer } class $$SignalSessionStoresTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalSessionStoresTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalSessionStoresTable> { $$SignalSessionStoresTableAnnotationComposer({ required super.$db, required super.$table, @@ -6403,7 +6397,7 @@ class $$SignalSessionStoresTableAnnotationComposer } class $$SignalSessionStoresTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore, $$SignalSessionStoresTableFilterComposer, @@ -6413,13 +6407,13 @@ class $$SignalSessionStoresTableTableManager extends RootTableManager< $$SignalSessionStoresTableUpdateCompanionBuilder, ( SignalSessionStore, - BaseReferences<_$TwonlyDatabase, $SignalSessionStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore> ), SignalSessionStore, PrefetchHooks Function()> { $$SignalSessionStoresTableTableManager( - _$TwonlyDatabase db, $SignalSessionStoresTable table) + _$TwonlyDatabaseOld db, $SignalSessionStoresTable table) : super(TableManagerState( db: db, table: table, @@ -6467,7 +6461,7 @@ class $$SignalSessionStoresTableTableManager extends RootTableManager< } typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore, $$SignalSessionStoresTableFilterComposer, @@ -6477,7 +6471,7 @@ typedef $$SignalSessionStoresTableProcessedTableManager = ProcessedTableManager< $$SignalSessionStoresTableUpdateCompanionBuilder, ( SignalSessionStore, - BaseReferences<_$TwonlyDatabase, $SignalSessionStoresTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalSessionStoresTable, SignalSessionStore> ), SignalSessionStore, @@ -6500,7 +6494,7 @@ typedef $$SignalContactPreKeysTableUpdateCompanionBuilder }); class $$SignalContactPreKeysTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableFilterComposer({ required super.$db, required super.$table, @@ -6522,7 +6516,7 @@ class $$SignalContactPreKeysTableFilterComposer } class $$SignalContactPreKeysTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableOrderingComposer({ required super.$db, required super.$table, @@ -6544,7 +6538,7 @@ class $$SignalContactPreKeysTableOrderingComposer } class $$SignalContactPreKeysTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalContactPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactPreKeysTable> { $$SignalContactPreKeysTableAnnotationComposer({ required super.$db, required super.$table, @@ -6566,7 +6560,7 @@ class $$SignalContactPreKeysTableAnnotationComposer } class $$SignalContactPreKeysTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey, $$SignalContactPreKeysTableFilterComposer, @@ -6576,13 +6570,13 @@ class $$SignalContactPreKeysTableTableManager extends RootTableManager< $$SignalContactPreKeysTableUpdateCompanionBuilder, ( SignalContactPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey> ), SignalContactPreKey, PrefetchHooks Function()> { $$SignalContactPreKeysTableTableManager( - _$TwonlyDatabase db, $SignalContactPreKeysTable table) + _$TwonlyDatabaseOld db, $SignalContactPreKeysTable table) : super(TableManagerState( db: db, table: table, @@ -6631,7 +6625,7 @@ class $$SignalContactPreKeysTableTableManager extends RootTableManager< typedef $$SignalContactPreKeysTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey, $$SignalContactPreKeysTableFilterComposer, @@ -6641,7 +6635,7 @@ typedef $$SignalContactPreKeysTableProcessedTableManager $$SignalContactPreKeysTableUpdateCompanionBuilder, ( SignalContactPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactPreKeysTable, SignalContactPreKey> ), SignalContactPreKey, @@ -6664,7 +6658,7 @@ typedef $$SignalContactSignedPreKeysTableUpdateCompanionBuilder }); class $$SignalContactSignedPreKeysTableFilterComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableFilterComposer({ required super.$db, required super.$table, @@ -6691,7 +6685,7 @@ class $$SignalContactSignedPreKeysTableFilterComposer } class $$SignalContactSignedPreKeysTableOrderingComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableOrderingComposer({ required super.$db, required super.$table, @@ -6719,7 +6713,7 @@ class $$SignalContactSignedPreKeysTableOrderingComposer } class $$SignalContactSignedPreKeysTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $SignalContactSignedPreKeysTable> { + extends Composer<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable> { $$SignalContactSignedPreKeysTableAnnotationComposer({ required super.$db, required super.$table, @@ -6744,7 +6738,7 @@ class $$SignalContactSignedPreKeysTableAnnotationComposer } class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableFilterComposer, @@ -6754,13 +6748,13 @@ class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, ( SignalContactSignedPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactSignedPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> ), SignalContactSignedPreKey, PrefetchHooks Function()> { $$SignalContactSignedPreKeysTableTableManager( - _$TwonlyDatabase db, $SignalContactSignedPreKeysTable table) + _$TwonlyDatabaseOld db, $SignalContactSignedPreKeysTable table) : super(TableManagerState( db: db, table: table, @@ -6810,7 +6804,7 @@ class $$SignalContactSignedPreKeysTableTableManager extends RootTableManager< typedef $$SignalContactSignedPreKeysTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey, $$SignalContactSignedPreKeysTableFilterComposer, @@ -6820,7 +6814,7 @@ typedef $$SignalContactSignedPreKeysTableProcessedTableManager $$SignalContactSignedPreKeysTableUpdateCompanionBuilder, ( SignalContactSignedPreKey, - BaseReferences<_$TwonlyDatabase, $SignalContactSignedPreKeysTable, + BaseReferences<_$TwonlyDatabaseOld, $SignalContactSignedPreKeysTable, SignalContactSignedPreKey> ), SignalContactSignedPreKey, @@ -6851,11 +6845,11 @@ typedef $$MessageRetransmissionsTableUpdateCompanionBuilder }); final class $$MessageRetransmissionsTableReferences extends BaseReferences< - _$TwonlyDatabase, $MessageRetransmissionsTable, MessageRetransmission> { + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission> { $$MessageRetransmissionsTableReferences( super.$_db, super.$_table, super.$_typedResult); - static $ContactsTable _contactIdTable(_$TwonlyDatabase db) => + static $ContactsTable _contactIdTable(_$TwonlyDatabaseOld db) => db.contacts.createAlias($_aliasNameGenerator( db.messageRetransmissions.contactId, db.contacts.userId)); @@ -6870,7 +6864,7 @@ final class $$MessageRetransmissionsTableReferences extends BaseReferences< manager.$state.copyWith(prefetchedData: [item])); } - static $MessagesTable _messageIdTable(_$TwonlyDatabase db) => + static $MessagesTable _messageIdTable(_$TwonlyDatabaseOld db) => db.messages.createAlias($_aliasNameGenerator( db.messageRetransmissions.messageId, db.messages.messageId)); @@ -6887,7 +6881,7 @@ final class $$MessageRetransmissionsTableReferences extends BaseReferences< } class $$MessageRetransmissionsTableFilterComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableFilterComposer({ required super.$db, required super.$table, @@ -6961,7 +6955,7 @@ class $$MessageRetransmissionsTableFilterComposer } class $$MessageRetransmissionsTableOrderingComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableOrderingComposer({ required super.$db, required super.$table, @@ -7036,7 +7030,7 @@ class $$MessageRetransmissionsTableOrderingComposer } class $$MessageRetransmissionsTableAnnotationComposer - extends Composer<_$TwonlyDatabase, $MessageRetransmissionsTable> { + extends Composer<_$TwonlyDatabaseOld, $MessageRetransmissionsTable> { $$MessageRetransmissionsTableAnnotationComposer({ required super.$db, required super.$table, @@ -7107,7 +7101,7 @@ class $$MessageRetransmissionsTableAnnotationComposer } class $$MessageRetransmissionsTableTableManager extends RootTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission, $$MessageRetransmissionsTableFilterComposer, @@ -7119,7 +7113,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager< MessageRetransmission, PrefetchHooks Function({bool contactId, bool messageId})> { $$MessageRetransmissionsTableTableManager( - _$TwonlyDatabase db, $MessageRetransmissionsTable table) + _$TwonlyDatabaseOld db, $MessageRetransmissionsTable table) : super(TableManagerState( db: db, table: table, @@ -7234,7 +7228,7 @@ class $$MessageRetransmissionsTableTableManager extends RootTableManager< typedef $$MessageRetransmissionsTableProcessedTableManager = ProcessedTableManager< - _$TwonlyDatabase, + _$TwonlyDatabaseOld, $MessageRetransmissionsTable, MessageRetransmission, $$MessageRetransmissionsTableFilterComposer, @@ -7246,9 +7240,9 @@ typedef $$MessageRetransmissionsTableProcessedTableManager MessageRetransmission, PrefetchHooks Function({bool contactId, bool messageId})>; -class $TwonlyDatabaseManager { - final _$TwonlyDatabase _db; - $TwonlyDatabaseManager(this._db); +class $TwonlyDatabaseOldManager { + final _$TwonlyDatabaseOld _db; + $TwonlyDatabaseOldManager(this._db); $$ContactsTableTableManager get contacts => $$ContactsTableTableManager(_db, _db.contacts); $$MessagesTableTableManager get messages => diff --git a/lib/src/database/twonly_database.steps.dart b/lib/src/database/twonly_database_old.steps.dart similarity index 100% rename from lib/src/database/twonly_database.steps.dart rename to lib/src/database/twonly_database_old.steps.dart diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 88444e2..5e7bba5 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -1,54 +1,102 @@ { "@@locale": "de", "registerTitle": "Willkommen bei twonly!", + "@registerTitle": {}, "registerSlogan": "twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.", + "@registerSlogan": {}, "onboardingWelcomeTitle": "Willkommen bei twonly!", + "@onboardingWelcomeTitle": {}, "onboardingWelcomeBody": "Erlebe eine private und sichere Möglichkeit mit Freunden in Kontakt zu bleiben, indem du spontane Bilder teilst.", + "@onboardingWelcomeBody": {}, "onboardingE2eTitle": "Unbekümmert teilen", + "@onboardingE2eTitle": {}, "onboardingE2eBody": "Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.", + "@onboardingE2eBody": {}, "onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten", + "@onboardingFocusTitle": {}, "onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.", + "@onboardingFocusBody": {}, "onboardingSendTwonliesTitle": "twonlies senden", + "@onboardingSendTwonliesTitle": {}, "onboardingSendTwonliesBody": "Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!", + "@onboardingSendTwonliesBody": {}, "onboardingNotProductTitle": "Du bist nicht das Produkt!", + "@onboardingNotProductTitle": {}, "onboardingNotProductBody": "twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.", + "@onboardingNotProductBody": {}, "onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei", + "@onboardingBuyOneGetTwoTitle": {}, "onboardingBuyOneGetTwoBody": "twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.", + "@onboardingBuyOneGetTwoBody": {}, "onboardingGetStartedTitle": "Auf geht's", + "@onboardingGetStartedTitle": {}, "onboardingGetStartedBody": "Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.", + "@onboardingGetStartedBody": {}, "onboardingTryForFree": "Jetzt registrieren", + "@onboardingTryForFree": {}, "registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!", + "@registerUsernameSlogan": {}, "registerUsernameDecoration": "Benutzername", + "@registerUsernameDecoration": {}, "registerUsernameLimits": "Der Benutzername muss mindestens 3 Zeichen lang sein.", + "@registerUsernameLimits": {}, "registerSubmitButton": "Jetzt registrieren!", + "@registerSubmitButton": {}, "registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!", + "@registerTwonlyCodeText": {}, "registerTwonlyCodeLabel": "twonly-Code", + "@registerTwonlyCodeLabel": {}, "newMessageTitle": "Neue Nachricht", + "@newMessageTitle": {}, "chatsTapToSend": "Klicke, um dein erstes Bild zu teilen.", + "@chatsTapToSend": {}, "cameraPreviewSendTo": "Senden an", + "@cameraPreviewSendTo": {}, "shareImageTitle": "Teilen mit", + "@shareImageTitle": {}, "shareImageBestFriends": "Beste Freunde", + "@shareImageBestFriends": {}, "shareImagePinnedContacts": "Angeheftet", + "@shareImagePinnedContacts": {}, "shareImagedEditorSendImage": "Senden", + "@shareImagedEditorSendImage": {}, "shareImagedEditorShareWith": "Teilen mit", + "@shareImagedEditorShareWith": {}, "shareImagedEditorSaveImage": "Speichern", + "@shareImagedEditorSaveImage": {}, "shareImagedEditorSavedImage": "Gespeichert", + "@shareImagedEditorSavedImage": {}, "shareImagedSelectAll": "Alle auswählen", + "@shareImagedSelectAll": {}, "shareImageAllUsers": "Alle Kontakte", + "@shareImageAllUsers": {}, "shareImageAllTwonlyWarning": "twonlies können nur an verifizierte Kontakte gesendet werden!", + "@shareImageAllTwonlyWarning": {}, "shareImageSearchAllContacts": "Alle Kontakte durchsuchen", "@shareImageSearchAllContacts": {}, "shareImageUserNotVerified": "Benutzer ist nicht verifiziert", + "@shareImageUserNotVerified": {}, "shareImageUserNotVerifiedDesc": "twonlies können nur an verifizierte Nutzer gesendet werden. Um einen Nutzer zu verifizieren, gehe auf sein Profil und auf „Sicherheitsnummer verifizieren“.", + "@shareImageUserNotVerifiedDesc": {}, "shareImageShowArchived": "Archivierte Benutzer anzeigen", + "@shareImageShowArchived": {}, + "startNewChatSearchHint": "Name, Benutzername oder Gruppenname", "searchUsernameInput": "Benutzername", + "@searchUsernameInput": {}, "searchUsernameTitle": "Benutzernamen suchen", + "@searchUsernameTitle": {}, "searchUserNamePreview": "Um dich und andere twonly Benutzer vor Spam und Missbrauch zu schützen, ist es nicht möglich, im Preview-Modus nach anderen Personen zu suchen. Andere Benutzer können dich finden und deren Anfragen werden dann hier angezeigt!", + "@searchUserNamePreview": {}, "selectSubscription": "Abo auswählen", + "@selectSubscription": {}, "searchUsernameNotFound": "Benutzername nicht gefunden", + "@searchUsernameNotFound": {}, "searchUsernameNotFoundBody": "Es wurde kein Benutzer mit dem Benutzernamen \"{username}\" gefunden.", + "@searchUsernameNotFoundBody": {}, "searchUsernameNewFollowerTitle": "Folgeanfragen", + "@searchUsernameNewFollowerTitle": {}, "searchUsernameQrCodeBtn": "QR-Code scannen", + "@searchUsernameQrCodeBtn": {}, "searchUserNamePending": "Ausstehend", "@searchUserNamePending": {}, "searchUserNameBlockUserTooltip": "Benutzer ohne Benachrichtigung blockieren.", @@ -62,10 +110,15 @@ "userFoundBody": "Möchtest du eine Folgeanfrage stellen?", "@userFoundBody": {}, "chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!", + "@chatListViewSearchUserNameBtn": {}, "chatListViewSendFirstTwonly": "Sende dein erstes twonly!", + "@chatListViewSendFirstTwonly": {}, "chatListDetailInput": "Nachricht eingeben", + "@chatListDetailInput": {}, "userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.", + "@userDeletedAccount": {}, "contextMenuUserProfile": "Userprofil", + "@contextMenuUserProfile": {}, "contextMenuVerifyUser": "Verifizieren", "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archivieren", @@ -79,261 +132,691 @@ "startNewChatYourContacts": "Deine Kontakte", "@startNewChatYourContacts": {}, "contextMenuOpenChat": "Chat", + "@contextMenuOpenChat": {}, "contextMenuPin": "Anheften", + "@contextMenuPin": {}, "contextMenuUnpin": "Lösen", + "@contextMenuUnpin": {}, "mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!", + "@mediaViewerAuthReason": {}, "mediaViewerTwonlyTapToOpen": "Tippe um den twonly zu öffnen!", + "@mediaViewerTwonlyTapToOpen": {}, "messageSendState_Received": "Empfangen", + "@messageSendState_Received": {}, "messageSendState_Opened": "Geöffnet", + "@messageSendState_Opened": {}, "messageSendState_Send": "Gesendet", + "@messageSendState_Send": {}, "messageSendState_Sending": "Wird gesendet", + "@messageSendState_Sending": {}, "messageSendState_TapToLoad": "Tippe zum Laden", + "@messageSendState_TapToLoad": {}, "messageSendState_Loading": "Herunterladen", + "@messageSendState_Loading": {}, "messageStoredInGallery": "Gespeichert", + "@messageStoredInGallery": {}, "messageReopened": "Erneut geöffnet", "@messageReopened": {}, "imageEditorDrawOk": "Zeichnung machen", + "@imageEditorDrawOk": {}, "settingsTitle": "Einstellungen", + "@settingsTitle": {}, "settingsChats": "Chats", + "@settingsChats": {}, "settingsStorageData": "Daten und Speicher", + "@settingsStorageData": {}, "settingsStorageDataStoreInGTitle": "In der Galerie speichern", + "@settingsStorageDataStoreInGTitle": {}, "settingsStorageDataStoreInGSubtitle": "Speichere Bilder zusätzlich in der Systemgalerie.", + "@settingsStorageDataStoreInGSubtitle": {}, "settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload", + "@settingsStorageDataMediaAutoDownload": {}, "settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten", + "@settingsStorageDataAutoDownMobile": {}, "settingsStorageDataAutoDownWifi": "Bei Nutzung von WLAN", + "@settingsStorageDataAutoDownWifi": {}, "settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis", + "@settingsPreSelectedReactions": {}, "settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.", + "@settingsPreSelectedReactionsError": {}, "settingsProfile": "Profil", + "@settingsProfile": {}, "settingsProfileCustomizeAvatar": "Avatar anpassen", + "@settingsProfileCustomizeAvatar": {}, "settingsProfileEditDisplayName": "Anzeigename", + "@settingsProfileEditDisplayName": {}, "settingsProfileEditDisplayNameNew": "Neuer Anzeigename", + "@settingsProfileEditDisplayNameNew": {}, "settingsAccount": "Konto", + "@settingsAccount": {}, "settingsSubscription": "Abonnement", + "@settingsSubscription": {}, "settingsAppearance": "Erscheinungsbild", + "@settingsAppearance": {}, "settingsPrivacy": "Datenschutz", + "@settingsPrivacy": {}, "settingsPrivacyBlockUsers": "Benutzer blockieren", + "@settingsPrivacyBlockUsers": {}, "settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.", + "@settingsPrivacyBlockUsersDesc": {}, "settingsPrivacyBlockUsersCount": "{len} Kontakt(e)", + "@settingsPrivacyBlockUsersCount": {}, "settingsNotification": "Benachrichtigung", + "@settingsNotification": {}, "settingsNotifyTroubleshooting": "Fehlersuche", + "@settingsNotifyTroubleshooting": {}, "settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.", + "@settingsNotifyTroubleshootingDesc": {}, "settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt", + "@settingsNotifyTroubleshootingNoProblem": {}, "settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.", + "@settingsNotifyTroubleshootingNoProblemDesc": {}, "settingsHelp": "Hilfe", + "@settingsHelp": {}, "settingsHelpFAQ": "FAQ", + "@settingsHelpFAQ": {}, "feedbackTooltip": "Feedback zur Verbesserung von twonly geben.", + "@feedbackTooltip": {}, "settingsHelpContactUs": "Kontaktiere uns", + "@settingsHelpContactUs": {}, "contactUsFaq": "FAQ schon gelesen?", + "@contactUsFaq": {}, "contactUsEmojis": "Wie fühlst du dich? (optional)", + "@contactUsEmojis": {}, "contactUsSelectOption": "Bitte wähle eine Option", + "@contactUsSelectOption": {}, "contactUsReason": "Sag uns, warum du uns kontaktierst", + "@contactUsReason": {}, "contactUsMessage": "Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.", + "@contactUsMessage": {}, "contactUsYourMessage": "Deine Nachricht", + "@contactUsYourMessage": {}, "contactUsMessageTitle": "Erzähl uns, was los ist", + "@contactUsMessageTitle": {}, "contactUsReasonNotWorking": "Etwas funktioniert nicht", + "@contactUsReasonNotWorking": {}, "contactUsReasonFeatureRequest": "Funktionsanfrage", + "@contactUsReasonFeatureRequest": {}, "contactUsReasonQuestion": "Frage", + "@contactUsReasonQuestion": {}, "contactUsReasonFeedback": "Feedback", + "@contactUsReasonFeedback": {}, "contactUsReasonOther": "Sonstiges", + "@contactUsReasonOther": {}, "contactUsIncludeLog": "Debug-Protokoll anhängen.", + "@contactUsIncludeLog": {}, "contactUsWhatsThat": "Was ist das?", + "@contactUsWhatsThat": {}, "contactUsLastWarning": "Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.", + "@contactUsLastWarning": {}, "contactUsSuccess": "Feedback erfolgreich übermittelt!", + "@contactUsSuccess": {}, "contactUsShortcut": "Feedback-Symbol ausblenden", + "@contactUsShortcut": {}, "settingsHelpDiagnostics": "Diagnoseprotokoll", + "@settingsHelpDiagnostics": {}, "settingsHelpVersion": "Version", + "@settingsHelpVersion": {}, "settingsHelpLicenses": "Lizenzen (Source-Code)", + "@settingsHelpLicenses": {}, "settingsHelpCredits": "Lizenzen (Bilder)", + "@settingsHelpCredits": {}, "settingsHelpImprint": "Impressum & Datenschutzrichtlinie", + "@settingsHelpImprint": {}, "settingsHelpTerms": "Nutzungsbedingungen", + "@settingsHelpTerms": {}, "settingsAppearanceTheme": "Theme", + "@settingsAppearanceTheme": {}, "settingsAccountDeleteAccount": "Konto löschen", + "@settingsAccountDeleteAccount": {}, "settingsAccountDeleteAccountWithBallance": "Im nächsten Schritt kannst du auswählen, was du mit dem Restguthaben ({credit}) machen willst.", + "@settingsAccountDeleteAccountWithBallance": {}, "settingsAccountDeleteAccountNoInternet": "Zum Löschen deines Accounts ist eine Internetverbindung erforderlich.", + "@settingsAccountDeleteAccountNoInternet": {}, "settingsAccountDeleteAccountNoBallance": "Wenn du dein Konto gelöscht hast, gibt es keinen Weg zurück.", + "@settingsAccountDeleteAccountNoBallance": {}, "settingsAccountDeleteModalTitle": "Bist du sicher?", + "@settingsAccountDeleteModalTitle": {}, "settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.", + "@settingsAccountDeleteModalBody": {}, "contactVerifyNumberTitle": "Sicherheitsnummer verifizieren", + "@contactVerifyNumberTitle": {}, "contactVerifyNumberTapToScan": "Zum Scannen tippen", + "@contactVerifyNumberTapToScan": {}, "contactVerifyNumberMarkAsVerified": "Als verifiziert markieren", + "@contactVerifyNumberMarkAsVerified": {}, "contactVerifyNumberClearVerification": "Verifizierung aufheben", + "@contactVerifyNumberClearVerification": {}, "contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.", + "@contactVerifyNumberLongDesc": {}, "contactNickname": "Spitzname", + "@contactNickname": {}, "contactNicknameNew": "Neuer Spitzname", + "@contactNicknameNew": {}, "contactBlock": "Blockieren", + "@contactBlock": {}, "contactRemove": "Benutzer löschen", + "@contactRemove": {}, "contactRemoveTitle": "{username} löschen?", + "@contactRemoveTitle": {}, "contactRemoveBody": "Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.", + "@contactRemoveBody": {}, "deleteAllContactMessages": "Textnachrichten löschen", + "@deleteAllContactMessages": {}, "deleteAllContactMessagesBody": "Dadurch werden alle Nachrichten, ausgenommen gespeicherte Mediendateien, in deinem Chat mit {username} gelöscht. Dies löscht NICHT die auf dem Gerät von {username} gespeicherten Nachrichten!", + "@deleteAllContactMessagesBody": {}, "contactBlockTitle": "Blockiere {username}", + "@contactBlockTitle": {}, "contactBlockBody": "Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und sein Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.", + "@contactBlockBody": {}, "undo": "Rückgängig", + "@undo": {}, "redo": "Wiederholen", + "@redo": {}, "next": "Weiter", + "@next": {}, "submit": "Abschicken", + "@submit": {}, "close": "Schließen", + "@close": {}, "cancel": "Abbrechen", + "@cancel": {}, + "edit": "Bearbeiten", + "@edit": {}, "ok": "Ok", + "@ok": {}, + "now": "Jetzt", + "@now": {}, + "you": "Du", + "@you": {}, + "minutesShort": "Min.", + "@minutesShort": {}, + "image": "Bild", + "@image": {}, + "video": "Video", + "@video": {}, "react": "Reagieren", + "@react": {}, "reply": "Antworten", + "@reply": {}, "copy": "Kopieren", + "@copy": {}, "delete": "Löschen", + "@delete": {}, "info": "Info", + "@info": {}, "disable": "Deaktiviern", + "@disable": {}, "enable": "Aktivieren", + "@enable": {}, "switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.", + "@switchFrontAndBackCamera": {}, "addTextItem": "Text", + "@addTextItem": {}, "protectAsARealTwonly": "Als echtes twonly senden!", + "@protectAsARealTwonly": {}, "addDrawing": "Zeichnung", + "@addDrawing": {}, "addEmoji": "Emoji", + "@addEmoji": {}, "toggleFlashLight": "Taschenlampe umschalten", + "@toggleFlashLight": {}, "toggleHighQuality": "Bessere Auflösung umschalten", + "@toggleHighQuality": {}, "searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.", + "@searchUsernameNotFoundLong": {}, "errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.", + "@errorUnknown": {}, "errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.", + "@errorBadRequest": {}, "errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.", + "@errorTooManyRequests": {}, "errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.", + "@errorInternalError": {}, "errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.", - "errorUsernameAlreadyTaken": "Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.", + "@errorInvalidInvitationCode": {}, + "errorUsernameAlreadyTaken": "Der Benutzername ist bereits vergeben.", + "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.", + "@errorSignatureNotValid": {}, "errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.", + "@errorUsernameNotFound": {}, "errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.", + "@errorUsernameNotValid": {}, "errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.", + "@errorInvalidPublicKey": {}, "errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.", + "@errorSessionAlreadyAuthenticated": {}, "errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.", + "@errorSessionNotAuthenticated": {}, "errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.", + "@errorOnlyOneSessionAllowed": {}, "upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.", + "@upgradeToPaidPlan": {}, "upgradeToPaidPlanButton": "Auf {planId} upgraden", + "@upgradeToPaidPlanButton": {}, "partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!", + "@partOfPaidPlanOf": {}, "errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.", + "@errorNotEnoughCredit": {}, "errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.", + "@errorPlanLimitReached": {}, "errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.", + "@errorPlanNotAllowed": {}, "errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.", + "@errorVoucherInvalid": {}, "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.", + "@errorPlanUpgradeNotYearly": {}, "proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", + "@proFeature1": {}, "proFeature2": "1 zusätzlicher Plus Benutzer", - "proFeature3": "Zusatzfunktionen (coming-soon)", + "@proFeature2": {}, + "proFeature3": "Flammen wiederherstellen", + "@proFeature3": {}, "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", + "@proFeature4": {}, "year": "year", + "@year": {}, "month": "month", + "@month": {}, "familyFeature1": "✓ Alles von Pro", + "@familyFeature1": {}, "familyFeature2": "4 zusätzliche Plus Benutzer", + "@familyFeature2": {}, "redeemUserInviteCode": "Oder löse einen twonly-Code ein.", + "@redeemUserInviteCode": {}, "freeFeature1": "10 Medien-Datei-Uploads pro Tag", + "@freeFeature1": {}, "plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", + "@plusFeature1": {}, "plusFeature2": "Zusatzfunktionen (coming-soon)", + "@plusFeature2": {}, "transactionHistory": "Transaktionshistorie", + "@transactionHistory": {}, "currentBalance": "Dein Guthaben", + "@currentBalance": {}, "manageAdditionalUsers": "Zusätzliche Benutzer verwalten", + "@manageAdditionalUsers": {}, "manageSubscription": "Abonnement verwalten", + "@manageSubscription": {}, "nextPayment": "Nächste Zahlung", + "@nextPayment": {}, "open": "Offene", + "@open": {}, "buy": "Kaufen", + "@buy": {}, "createOrRedeemVoucher": "Gutschein erstellen oder einlösen", + "@createOrRedeemVoucher": {}, "subscriptionRefund": "Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von {refund} für dein aktuelles Abonnement.", + "@subscriptionRefund": {}, "createVoucher": "Gutschein kaufen", + "@createVoucher": {}, "createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.", + "@createVoucherDesc": {}, "redeemVoucher": "Gutschein einlösen", + "@redeemVoucher": {}, "redeemUserInviteCodeTitle": "twonly-Code einlösen", + "@redeemUserInviteCodeTitle": {}, "redeemUserInviteCodeSuccess": "Dein Plan wurde erfolgreich angepasst.", + "@redeemUserInviteCodeSuccess": {}, "voucherCreated": "Gutschein wurde erstellt", + "@voucherCreated": {}, "openVouchers": "Offene Gutscheine", + "@openVouchers": {}, "enterVoucherCode": "Gutschein Code eingeben", + "@enterVoucherCode": {}, "voucherRedeemed": "Gutschein eingelöst", + "@voucherRedeemed": {}, "requestedVouchers": "Beantragte Gutscheine", + "@requestedVouchers": {}, "redeemedVouchers": "Eingelöste Gutscheine", + "@redeemedVouchers": {}, "transactionCash": "Bargeldtransaktion", + "@transactionCash": {}, "transactionPlanUpgrade": "Planupgrade", + "@transactionPlanUpgrade": {}, "transactionRefund": "Rückerstattung", + "@transactionRefund": {}, "transactionAutoRenewal": "Automatische Verlängerung", + "@transactionAutoRenewal": {}, "refund": "Rückerstattung", + "@refund": {}, "transactionThanksForTesting": "Danke fürs Testen", + "@transactionThanksForTesting": {}, "transactionUnknown": "Unbekannte Transaktion", + "@transactionUnknown": {}, "transactionVoucherCreated": "Gutschein erstellt", + "@transactionVoucherCreated": {}, "transactionVoucherRedeemed": "Gutschein eingelöst", + "@transactionVoucherRedeemed": {}, "checkoutOptions": "Optionen", + "@checkoutOptions": {}, "checkoutPayYearly": "Jährlich bezahlen", + "@checkoutPayYearly": {}, "checkoutTotal": "Gesamt", + "@checkoutTotal": {}, "selectPaymentMethod": "Zahlungsmethode auswählen", + "@selectPaymentMethod": {}, "twonlyCredit": "twonly-Guthaben", + "@twonlyCredit": {}, "notEnoughCredit": "Du hast nicht genügend Guthaben!", + "@notEnoughCredit": {}, "chargeCredit": "Guthaben aufladen", + "@chargeCredit": {}, "autoRenewal": "Automatische Verlängerung", + "@autoRenewal": {}, "autoRenewalDesc": "Du kannst dies jederzeit ändern.", + "@autoRenewalDesc": {}, "autoRenewalLongDesc": "Wenn dein Abonnement ausläuft, wirst du automatisch auf den Preview-Plan zurückgestuft. Wenn du die automatische Verlängerung aktivierst, vergewissere dich bitte, dass du über genügend Guthaben für die automatische Erneuerung verfügst. Wir werden dich rechtzeitig vor der automatischen Erneuerung benachrichtigen.", + "@autoRenewalLongDesc": {}, "planSuccessUpgraded": "Dein Plan wurde erfolgreich aktualisiert.", + "@planSuccessUpgraded": {}, "checkoutSubmit": "Kostenpflichtig bestellen", + "@checkoutSubmit": {}, "additionalUsersList": "Ihre zusätzlichen Benutzer", + "@additionalUsersList": {}, "additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer", + "@additionalUsersPlusTokens": {}, "additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer", + "@additionalUsersFreeTokens": {}, "planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", + "@planNotAllowed": {}, "planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", + "@planLimitReached": {}, "galleryDelete": "Datei löschen", + "@galleryDelete": {}, "galleryExport": "In Galerie exportieren", + "@galleryExport": {}, "galleryExportSuccess": "Erfolgreich in der Gallery gespeichert.", + "@galleryExportSuccess": {}, "galleryDetails": "Details anzeigen", + "@galleryDetails": {}, "settingsResetTutorials": "Tutorials erneut anzeigen", + "@settingsResetTutorials": {}, "settingsResetTutorialsDesc": "Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.", + "@settingsResetTutorialsDesc": {}, "settingsResetTutorialsSuccess": "Tutorials werden erneut angezeigt.", + "@settingsResetTutorialsSuccess": {}, "tutorialChatListSearchUsersTitle": "Freunde finden und Freundschaftsanfragen verwalten", + "@tutorialChatListSearchUsersTitle": {}, "tutorialChatListSearchUsersDesc": "Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.", + "@tutorialChatListSearchUsersDesc": {}, "tutorialChatListContextMenuTitle": "Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.", + "@tutorialChatListContextMenuTitle": {}, "tutorialChatListContextMenuDesc": "Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.", + "@tutorialChatListContextMenuDesc": {}, "tutorialChatMessagesVerifyShieldTitle": "Verifiziere deine Kontakte!", + "@tutorialChatMessagesVerifyShieldTitle": {}, "tutorialChatMessagesVerifyShieldDesc": "twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.", + "@tutorialChatMessagesVerifyShieldDesc": {}, "tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen", + "@tutorialChatMessagesReopenMessageTitle": {}, "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", + "@tutorialChatMessagesReopenMessageDesc": {}, "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", + "@memoriesEmpty": {}, "deleteTitle": "Bist du dir sicher?", - "deleteOkBtn": "Für mich löschen", + "@deleteTitle": {}, + "deleteOkBtnForAll": "Für alle löschen", + "@deleteOkBtnForAll": {}, + "deleteOkBtnForMe": "Für mich löschen", + "@deleteOkBtnForMe": {}, "deleteImageTitle": "Bist du dir sicher?", + "@deleteImageTitle": {}, "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", + "@deleteImageBody": {}, "backupNoticeTitle": "Kein Backup konfiguriert", + "@backupNoticeTitle": {}, "backupNoticeDesc": "Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.", + "@backupNoticeDesc": {}, "backupNoticeLater": "Später erinnern", + "@backupNoticeLater": {}, "backupNoticeOpenBackup": "Backup erstellen", + "@backupNoticeOpenBackup": {}, "backupPending": "Ausstehend", + "@backupPending": {}, "backupFailed": "Fehlgeschlagen", + "@backupFailed": {}, "backupSuccess": "Erfolgreich", + "@backupSuccess": {}, "backupTwonlySafeDesc": "Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.", + "@backupTwonlySafeDesc": {}, "backupServer": "Server", + "@backupServer": {}, "backupMaxBackupSize": "max. Backup-Größe", + "@backupMaxBackupSize": {}, "backupStorageRetention": "Speicheraufbewahrung", + "@backupStorageRetention": {}, "backupLastBackupDate": "Letztes Backup", + "@backupLastBackupDate": {}, "backupLastBackupSize": "Backup-Größe", + "@backupLastBackupSize": {}, "backupLastBackupResult": "Ergebnis", + "@backupLastBackupResult": {}, "deleteBackupTitle": "Bist du sicher?", + "@deleteBackupTitle": {}, + "backupNoPasswordRecovery": "Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.", + "@backupNoPasswordRecovery": {}, "deleteBackupBody": "Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.", + "@deleteBackupBody": {}, "backupData": "Daten-Backup", + "@backupData": {}, "backupDataDesc": "Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.", + "@backupDataDesc": {}, "backupInsecurePassword": "Unsicheres Passwort", + "@backupInsecurePassword": {}, "backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.", + "@backupInsecurePasswordDesc": {}, "backupInsecurePasswordOk": "Trotzdem fortfahren", + "@backupInsecurePasswordOk": {}, "backupInsecurePasswordCancel": "Erneut versuchen", - "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", - "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Safe-Backup wiederherstellen möchtest.", + "@backupInsecurePasswordCancel": {}, + "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", + "@backupTwonlySafeLongDesc": {}, + "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.", + "@backupSelectStrongPassword": {}, "password": "Passwort", + "@password": {}, "passwordRepeated": "Passwort wiederholen", + "@passwordRepeated": {}, "passwordRepeatedNotEqual": "Passwörter stimmen nicht überein.", + "@passwordRepeatedNotEqual": {}, "backupPasswordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein.", + "@backupPasswordRequirement": {}, "backupExpertSettings": "Experteneinstellungen", + "@backupExpertSettings": {}, "backupEnableBackup": "Automatische Sicherung aktivieren", - "backupOwnServerDesc": "Speichere dein twonly Safe-Backups auf einem Server deiner Wahl.", + "@backupEnableBackup": {}, + "backupOwnServerDesc": "Speichere dein twonly Backup auf einem Server deiner Wahl.", + "@backupOwnServerDesc": {}, "backupUseOwnServer": "Server verwenden", + "@backupUseOwnServer": {}, "backupResetServer": "Standardserver verwenden", + "@backupResetServer": {}, "backupTwonlySaveNow": "Jetzt speichern", + "@backupTwonlySaveNow": {}, + "backupChangePassword": "Password ändern", + "@backupChangePassword": {}, "inviteFriends": "Freunde einladen", + "@inviteFriends": {}, "inviteFriendsShareBtn": "Teilen", + "@inviteFriendsShareBtn": {}, "inviteFriendsShareText": "Wechseln wir zu twonly: {url}", + "@inviteFriendsShareText": {}, "appOutdated": "Deine Version von twonly ist veraltet.", + "@appOutdated": {}, "appOutdatedBtn": "Jetzt aktualisieren.", + "@appOutdatedBtn": {}, "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", + "uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.", + "@doubleClickToReopen": {}, "retransmissionRequested": "Wird erneut versucht.", + "@retransmissionRequested": {}, "testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!", + "@testPaymentMethod": {}, "openChangeLog": "Changelog automatisch öffnen", + "@openChangeLog": {}, "reportUserTitle": "Melde {username}", + "@reportUserTitle": {}, "reportUserReason": "Meldegrund", + "@reportUserReason": {}, "reportUser": "Benutzer melden", - "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet." + "@reportUser": {}, + "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", + "@newDeviceRegistered": {}, + "tabToRemoveEmoji": "Tippen um zu entfernen", + "@tabToRemoveEmoji": {}, + "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", + "@quotedMessageWasDeleted": {}, + "messageWasDeleted": "Nachricht wurde gelöscht.", + "@messageWasDeleted": {}, + "messageWasDeletedShort": "Gelöscht", + "@messageWasDeletedShort": {}, + "sent": "Versendet", + "@sent": {}, + "sentTo": "Zugestellt an", + "@sentTo": {}, + "received": "Empfangen", + "@received": {}, + "opened": "Geöffnet", + "@opened": {}, + "waitingForInternet": "Warten auf Internet", + "@waitingForInternet": {}, + "editHistory": "Bearbeitungshistorie", + "@editHistory": {}, + "archivedChats": "Archivierte Chats", + "@archivedChats": {}, + "durationShortSecond": "Sek.", + "@durationShortSecond": {}, + "durationShortMinute": "Min.", + "@durationShortMinute": {}, + "durationShortHour": "Std.", + "@durationShortHour": {}, + "durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}", + "@durationShortDays": {}, + "contacts": "Kontakte", + "groups": "Gruppen", + "@groups": {}, + "newGroup": "Neue Gruppe", + "@newGroup": {}, + "selectMembers": "Mitglieder auswählen", + "@selectMembers": {}, + "selectGroupName": "Gruppennamen wählen", + "@selectGroupName": {}, + "groupNameInput": "Gruppennamen", + "@groupNameInput": {}, + "groupMembers": "Mitglieder", + "@groupMembers": {}, + "createGroup": "Gruppe erstellen", + "@createGroup": {}, + "addMember": "Mitglied hinzufügen", + "@addMember": {}, + "leaveGroup": "Gruppe verlassen", + "@leaveGroup": {}, + "createContactRequest": "Kontaktanfrage erstellen", + "@createContactRequest": {}, + "contactRequestSend": "Kontakanfrage gesendet", + "makeAdmin": "Zum Admin machen", + "@makeAdmin": {}, + "removeAdmin": "Als Admin entfernen", + "@removeAdmin": {}, + "removeFromGroup": "Aus Gruppe entfernen", + "@removeFromGroup": {}, + "admin": "Admin", + "@admin": {}, + "revokeAdminRightsTitle": "Adminrechte von {username} entfernen?", + "@revokeAdminRightsTitle": {}, + "revokeAdminRightsOkBtn": "Als Admin entfernen", + "@revokeAdminRightsOkBtn": {}, + "makeAdminRightsTitle": "{username} zum Admin machen?", + "@makeAdminRightsTitle": {}, + "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", + "@makeAdminRightsBody": {}, + "makeAdminRightsOkBtn": "Zum Admin machen", + "@makeAdminRightsOkBtn": {}, + "updateGroup": "Gruppe aktualisieren", + "@updateGroup": {}, + "alreadyInGroup": "Bereits Mitglied", + "@alreadyInGroup": {}, + "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?", + "@removeContactFromGroupTitle": {}, + "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", + "@youChangedGroupName": {}, + "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", + "@makerChangedGroupName": {}, + "youCreatedGroup": "Du hast die Gruppe erstellt.", + "@youCreatedGroup": {}, + "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", + "@makerCreatedGroup": {}, + "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", + "@youRemovedMember": {}, + "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", + "@makerRemovedMember": {}, + "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", + "@youAddedMember": {}, + "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", + "@makerAddedMember": {}, + "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", + "@youMadeAdmin": {}, + "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", + "@makerMadeAdmin": {}, + "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", + "@youRevokedAdminRights": {}, + "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", + "@makerRevokedAdminRights": {}, + "youLeftGroup": "Du hast die Gruppe verlassen.", + "@youLeftGroup": {}, + "makerLeftGroup": "{maker} hat die Gruppe verlassen.", + "@makerLeftGroup": {}, + "groupActionYou": "dich", + "@groupActionYou": {}, + "groupActionYour": "deine", + "@groupActionYour": {}, + "settingsBackup": "Backup", + "@settingsBackup": {}, + "twonlySafeRecoverTitle": "Recovery", + "@twonlySafeRecoverTitle": {}, + "twonlySafeRecoverDesc": "Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.", + "@twonlySafeRecoverDesc": {}, + "twonlySafeRecoverBtn": "Backup wiederherstellen", + "@twonlySafeRecoverBtn": {}, + "notificationFillerIn": "in", + "notificationText": "hat eine Nachricht{inGroup} gesendet.", + "notificationTwonly": "hat ein twonly{inGroup} gesendet.", + "notificationVideo": "hat ein Video{inGroup} gesendet.", + "notificationImage": "hat ein Bild{inGroup} gesendet.", + "notificationAudio": "hat eine Sprachnachricht{inGroup} gesendet.", + "notificationAddedToGroup": "hat dich zu \"{groupname}\" hinzugefügt.", + "notificationContactRequest": "möchte sich mit dir vernetzen.", + "notificationAcceptRequest": "ist jetzt mit dir vernetzt.", + "notificationStoredMediaFile": "hat dein Bild gespeichert.", + "notificationReaction": "hat auf dein Bild reagiert.", + "notificationReopenedMedia": "hat dein Bild erneut geöffnet.", + "notificationReactionToVideo": "hat mit {reaction} auf dein Video reagiert.", + "notificationReactionToText": "hat mit {reaction} auf deine Nachricht reagiert.", + "notificationReactionToImage": "hat mit {reaction} auf dein Bild reagiert.", + "notificationReactionToAudio": "hat mit {reaction} auf deine Sprachnachricht reagiert.", + "notificationResponse": "hat dir{inGroup} geantwortet.", + "notificationTitleUnknownUser": "Jemand", + "notificationCategoryMessageTitle": "Nachrichten", + "notificationCategoryMessageDesc": "Nachrichten von anderen Benutzern.", + "groupContextMenuDeleteGroup": "Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.", + "groupYouAreNowLongerAMember": "Du bist nicht mehr Mitglied dieser Gruppe.", + "groupNetworkIssue": "Netzwerkproblem. Bitte probiere es später noch einmal.", + "leaveGroupSelectOtherAdminTitle": "Einen Admin auswählen", + "leaveGroupSelectOtherAdminBody": "Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.", + "leaveGroupSureTitle": "Gruppe verlassen", + "leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?", + "leaveGroupSureOkBtn": "Gruppe verlassen", + "changeDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht ({username}).", + "youChangedDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht.", + "userGotReported": "Benutzer wurde gemeldet.", + "deleteChatAfter": "Chat löschen nach...", + "deleteChatAfterAnHour": "einer Stunde.", + "deleteChatAfterADay": "einem Tag.", + "deleteChatAfterAWeek": "einer Woche.", + "deleteChatAfterAMonth": "einem Monat.", + "deleteChatAfterAYear": "einem Jahr.", + "yourTwonlyScore": "Dein twonly-Score", + "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 63ccda9..cb78dfd 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -66,6 +66,7 @@ "@shareImagedEditorSavedImage": {}, "shareImageSearchAllContacts": "Search all contacts", "@shareImageSearchAllContacts": {}, + "startNewChatSearchHint": "Name, username or groupname", "shareImagedSelectAll": "Select all", "@shareImagedSelectAll": {}, "startNewChatTitle": "Select Contact", @@ -299,9 +300,15 @@ "disable": "Disable", "enable": "Enable", "cancel": "Cancel", + "now": "Now", + "you": "You", + "minutesShort": "min.", + "image": "Image", + "video": "Video", "react": "React", "reply": "Reply", "copy": "Copy", + "edit": "Edit", "delete": "Delete", "info": "Info", "ok": "Ok", @@ -339,7 +346,7 @@ "@errorInternalError": {}, "errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.", "@errorInvalidInvitationCode": {}, - "errorUsernameAlreadyTaken": "The username you want to use is already taken. Please choose a different username.", + "errorUsernameAlreadyTaken": "The username is already taken.", "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.", "@errorSignatureNotValid": {}, @@ -438,7 +445,8 @@ "tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.", "memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.", "deleteTitle": "Are you sure?", - "deleteOkBtn": "Delete for me", + "deleteOkBtnForAll": "Delete for all", + "deleteOkBtnForMe": "Delete for me", "deleteImageTitle": "Are you sure?", "deleteImageBody": "The image will be irrevocably deleted.", "settingsBackup": "Backup", @@ -450,6 +458,7 @@ "backupFailed": "Failed", "backupSuccess": "Success", "backupTwonlySafeDesc": "Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.", + "backupNoPasswordRecovery": "Due to twonly's security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.", "backupServer": "Server", "backupMaxBackupSize": "max. backup size", "backupStorageRetention": "Storage retention", @@ -464,20 +473,21 @@ "backupInsecurePasswordDesc": "The chosen password is very insecure and can therefore easily be guessed by attackers. Please choose a secure password.", "backupInsecurePasswordOk": "Continue anyway", "backupInsecurePasswordCancel": "Try again", - "backupTwonlySafeLongDesc": "twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.", - "backupSelectStrongPassword": "Choose a secure password. This is required if you want to restore your twonly Safe backup.", + "backupTwonlySafeLongDesc": "twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.", + "backupSelectStrongPassword": "Choose a secure password. This is required if you want to restore your twonly Backup.", "password": "Password", "passwordRepeated": "Repeat password", "passwordRepeatedNotEqual": "Passwords do not match.", "backupPasswordRequirement": "Password must be at least 8 characters long.", "backupExpertSettings": "Expert settings", "backupEnableBackup": "Activate automatic backup", - "backupOwnServerDesc": "Save your twonly safe backups at twonly or on any server of your choice.", + "backupOwnServerDesc": "Save your twonly Backup at twonly or on any server of your choice.", "backupUseOwnServer": "Use server", "backupResetServer": "Use standard server", "backupTwonlySaveNow": "Save now", + "backupChangePassword": "Change password", "twonlySafeRecoverTitle": "Recovery", - "twonlySafeRecoverDesc": "If you have created a backup with twonly Safe, you can restore it here.", + "twonlySafeRecoverDesc": "If you have created a backup with twonly Backup, you can restore it here.", "twonlySafeRecoverBtn": "Restore backup", "inviteFriends": "Invite your friends", "inviteFriendsShareBtn": "Share", @@ -485,11 +495,106 @@ "appOutdated": "Your version of twonly is out of date.", "appOutdatedBtn": "Update Now", "doubleClickToReopen": "Double-click\nto open again", + "uploadLimitReached": "The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.", "retransmissionRequested": "Retransmission requested", "testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!", "openChangeLog": "Open changelog automatically", "reportUserTitle": "Report {username}", "reportUserReason": "Reporting reason", "reportUser": "Report user", - "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here." + "newDeviceRegistered": "You have logged in on another device. You have therefore been logged out here.", + "tabToRemoveEmoji": "Tab to remove", + "quotedMessageWasDeleted": "The quoted message has been deleted.", + "messageWasDeleted": "Message has been deleted.", + "messageWasDeletedShort": "Deleted", + "sent": "Delivered", + "sentTo": "Delivered to", + "received": "Received", + "opened": "Opened", + "waitingForInternet": "Waiting for internet", + "editHistory": "Edit history", + "archivedChats": "Archived chats", + "durationShortSecond": "Sec.", + "durationShortMinute": "Min.", + "durationShortHour": "Hrs.", + "durationShortDays": "{count, plural, =1{1 Day} other{{count} Days}}", + "contacts": "Contacts", + "groups": "Groups", + "newGroup": "New group", + "selectMembers": "Select members", + "selectGroupName": "Select group name", + "groupNameInput": "Group name", + "groupMembers": "Members", + "addMember": "Add member", + "createGroup": "Create group", + "leaveGroup": "Leave group", + "createContactRequest": "Create contact request", + "contactRequestSend": "Contact request send", + "makeAdmin": "Make admin", + "removeAdmin": "Remove as admin", + "removeFromGroup": "Remove from group", + "admin": "Admin", + "revokeAdminRightsTitle": "Revoke {username}'s admin rights?", + "revokeAdminRightsOkBtn": "Remove as admin", + "makeAdminRightsTitle": "Make {username} an admin?", + "makeAdminRightsBody": "{username} will be able to edit this group and its members.", + "makeAdminRightsOkBtn": "Make admin", + "updateGroup": "Update group", + "alreadyInGroup": "Already in Group", + "removeContactFromGroupTitle": "Remove {username} from this group?", + "youChangedGroupName": "You have changed the group name to \"{newGroupName}\".", + "makerChangedGroupName": "{maker} has changed the group name to \"{newGroupName}\".", + "youCreatedGroup": "You have created the group.", + "makerCreatedGroup": "{maker} has created the group.", + "youRemovedMember": "You have removed {affected} from the group.", + "makerRemovedMember": "{maker} has removed {affected} from the group.", + "youAddedMember": "You have added {affected} to the group.", + "makerAddedMember": "{maker} has added {affected} to the group.", + "youMadeAdmin": "You made {affected} an admin.", + "makerMadeAdmin": "{maker} made {affected} an admin.", + "youRevokedAdminRights": "You revoked {affectedR} admin rights.", + "makerRevokedAdminRights": "{maker} revoked {affectedR} admin rights.", + "youLeftGroup": "You have left the group.", + "makerLeftGroup": "{maker} has left the group.", + "groupActionYou": "you", + "groupActionYour": "your", + "notificationFillerIn": "in", + "notificationText": "sent a message{inGroup}.", + "notificationTwonly": "sent a twonly{inGroup}.", + "notificationVideo": "sent a video{inGroup}.", + "notificationImage": "sent a image{inGroup}.", + "notificationAudio": "sent a voice message{inGroup}.", + "notificationAddedToGroup": "has added you to \"{groupname}\"", + "notificationContactRequest": "wants to connect with you.", + "notificationAcceptRequest": "is now connected with you.", + "notificationStoredMediaFile": "has stored your image.", + "notificationReaction": "has reacted to your image.", + "notificationReopenedMedia": "has reopened your image.", + "notificationReactionToVideo": "has reacted with {reaction} to your video.", + "notificationReactionToText": "has reacted with {reaction} to your message.", + "notificationReactionToImage": "has reacted with {reaction} to your image.", + "notificationReactionToAudio": "has reacted with {reaction} to your audio message.", + "notificationResponse": "has responded{inGroup}.", + "notificationTitleUnknownUser": "Someone", + "notificationCategoryMessageTitle": "Messages", + "notificationCategoryMessageDesc": "Messages from other users.", + "groupContextMenuDeleteGroup": "This will permanently delete all messages in this chat.", + "groupYouAreNowLongerAMember": "You are no longer part of this group.", + "groupNetworkIssue": "Network issue. Try again later.", + "leaveGroupSelectOtherAdminTitle": "Select another admin", + "leaveGroupSelectOtherAdminBody": "To leave the group, you must first select a new administrator.", + "leaveGroupSureTitle": "Leave group", + "leaveGroupSureBody": "Do you really want to leave the group?", + "leaveGroupSureOkBtn": "Leave group", + "changeDisplayMaxTime": "Chats will now be deleted after {time} ({username}).", + "youChangedDisplayMaxTime": "Chats will now be deleted after {time}.", + "userGotReported": "User has been reported.", + "deleteChatAfter": "Delete chat after...", + "deleteChatAfterAnHour": "one hour.", + "deleteChatAfterADay": "one day.", + "deleteChatAfterAWeek": "one week.", + "deleteChatAfterAMonth": "one month.", + "deleteChatAfterAYear": "one year.", + "yourTwonlyScore": "Your twonly-Score", + "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 5f34009..feedd96 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -302,6 +302,12 @@ abstract class AppLocalizations { /// **'Search all contacts'** String get shareImageSearchAllContacts; + /// No description provided for @startNewChatSearchHint. + /// + /// In en, this message translates to: + /// **'Name, username or groupname'** + String get startNewChatSearchHint; + /// No description provided for @shareImagedSelectAll. /// /// In en, this message translates to: @@ -1058,6 +1064,36 @@ abstract class AppLocalizations { /// **'Cancel'** String get cancel; + /// No description provided for @now. + /// + /// In en, this message translates to: + /// **'Now'** + String get now; + + /// No description provided for @you. + /// + /// In en, this message translates to: + /// **'You'** + String get you; + + /// No description provided for @minutesShort. + /// + /// In en, this message translates to: + /// **'min.'** + String get minutesShort; + + /// No description provided for @image. + /// + /// In en, this message translates to: + /// **'Image'** + String get image; + + /// No description provided for @video. + /// + /// In en, this message translates to: + /// **'Video'** + String get video; + /// No description provided for @react. /// /// In en, this message translates to: @@ -1076,6 +1112,12 @@ abstract class AppLocalizations { /// **'Copy'** String get copy; + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + /// No description provided for @delete. /// /// In en, this message translates to: @@ -1187,7 +1229,7 @@ abstract class AppLocalizations { /// No description provided for @errorUsernameAlreadyTaken. /// /// In en, this message translates to: - /// **'The username you want to use is already taken. Please choose a different username.'** + /// **'The username is already taken.'** String get errorUsernameAlreadyTaken; /// No description provided for @errorSignatureNotValid. @@ -1730,11 +1772,17 @@ abstract class AppLocalizations { /// **'Are you sure?'** String get deleteTitle; - /// No description provided for @deleteOkBtn. + /// No description provided for @deleteOkBtnForAll. + /// + /// In en, this message translates to: + /// **'Delete for all'** + String get deleteOkBtnForAll; + + /// No description provided for @deleteOkBtnForMe. /// /// In en, this message translates to: /// **'Delete for me'** - String get deleteOkBtn; + String get deleteOkBtnForMe; /// No description provided for @deleteImageTitle. /// @@ -1802,6 +1850,12 @@ abstract class AppLocalizations { /// **'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'** String get backupTwonlySafeDesc; + /// No description provided for @backupNoPasswordRecovery. + /// + /// In en, this message translates to: + /// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'** + String get backupNoPasswordRecovery; + /// No description provided for @backupServer. /// /// In en, this message translates to: @@ -1889,13 +1943,13 @@ abstract class AppLocalizations { /// No description provided for @backupTwonlySafeLongDesc. /// /// In en, this message translates to: - /// **'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'** + /// **'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'** String get backupTwonlySafeLongDesc; /// No description provided for @backupSelectStrongPassword. /// /// In en, this message translates to: - /// **'Choose a secure password. This is required if you want to restore your twonly Safe backup.'** + /// **'Choose a secure password. This is required if you want to restore your twonly Backup.'** String get backupSelectStrongPassword; /// No description provided for @password. @@ -1937,7 +1991,7 @@ abstract class AppLocalizations { /// No description provided for @backupOwnServerDesc. /// /// In en, this message translates to: - /// **'Save your twonly safe backups at twonly or on any server of your choice.'** + /// **'Save your twonly Backup at twonly or on any server of your choice.'** String get backupOwnServerDesc; /// No description provided for @backupUseOwnServer. @@ -1958,6 +2012,12 @@ abstract class AppLocalizations { /// **'Save now'** String get backupTwonlySaveNow; + /// No description provided for @backupChangePassword. + /// + /// In en, this message translates to: + /// **'Change password'** + String get backupChangePassword; + /// No description provided for @twonlySafeRecoverTitle. /// /// In en, this message translates to: @@ -1967,7 +2027,7 @@ abstract class AppLocalizations { /// No description provided for @twonlySafeRecoverDesc. /// /// In en, this message translates to: - /// **'If you have created a backup with twonly Safe, you can restore it here.'** + /// **'If you have created a backup with twonly Backup, you can restore it here.'** String get twonlySafeRecoverDesc; /// No description provided for @twonlySafeRecoverBtn. @@ -2012,6 +2072,12 @@ abstract class AppLocalizations { /// **'Double-click\nto open again'** String get doubleClickToReopen; + /// No description provided for @uploadLimitReached. + /// + /// In en, this message translates to: + /// **'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.'** + String get uploadLimitReached; + /// No description provided for @retransmissionRequested. /// /// In en, this message translates to: @@ -2053,6 +2119,570 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'You have logged in on another device. You have therefore been logged out here.'** String get newDeviceRegistered; + + /// No description provided for @tabToRemoveEmoji. + /// + /// In en, this message translates to: + /// **'Tab to remove'** + String get tabToRemoveEmoji; + + /// No description provided for @quotedMessageWasDeleted. + /// + /// In en, this message translates to: + /// **'The quoted message has been deleted.'** + String get quotedMessageWasDeleted; + + /// No description provided for @messageWasDeleted. + /// + /// In en, this message translates to: + /// **'Message has been deleted.'** + String get messageWasDeleted; + + /// No description provided for @messageWasDeletedShort. + /// + /// In en, this message translates to: + /// **'Deleted'** + String get messageWasDeletedShort; + + /// No description provided for @sent. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get sent; + + /// No description provided for @sentTo. + /// + /// In en, this message translates to: + /// **'Delivered to'** + String get sentTo; + + /// No description provided for @received. + /// + /// In en, this message translates to: + /// **'Received'** + String get received; + + /// No description provided for @opened. + /// + /// In en, this message translates to: + /// **'Opened'** + String get opened; + + /// No description provided for @waitingForInternet. + /// + /// In en, this message translates to: + /// **'Waiting for internet'** + String get waitingForInternet; + + /// No description provided for @editHistory. + /// + /// In en, this message translates to: + /// **'Edit history'** + String get editHistory; + + /// No description provided for @archivedChats. + /// + /// In en, this message translates to: + /// **'Archived chats'** + String get archivedChats; + + /// No description provided for @durationShortSecond. + /// + /// In en, this message translates to: + /// **'Sec.'** + String get durationShortSecond; + + /// No description provided for @durationShortMinute. + /// + /// In en, this message translates to: + /// **'Min.'** + String get durationShortMinute; + + /// No description provided for @durationShortHour. + /// + /// In en, this message translates to: + /// **'Hrs.'** + String get durationShortHour; + + /// No description provided for @durationShortDays. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 Day} other{{count} Days}}'** + String durationShortDays(num count); + + /// No description provided for @contacts. + /// + /// In en, this message translates to: + /// **'Contacts'** + String get contacts; + + /// No description provided for @groups. + /// + /// In en, this message translates to: + /// **'Groups'** + String get groups; + + /// No description provided for @newGroup. + /// + /// In en, this message translates to: + /// **'New group'** + String get newGroup; + + /// No description provided for @selectMembers. + /// + /// In en, this message translates to: + /// **'Select members'** + String get selectMembers; + + /// No description provided for @selectGroupName. + /// + /// In en, this message translates to: + /// **'Select group name'** + String get selectGroupName; + + /// No description provided for @groupNameInput. + /// + /// In en, this message translates to: + /// **'Group name'** + String get groupNameInput; + + /// No description provided for @groupMembers. + /// + /// In en, this message translates to: + /// **'Members'** + String get groupMembers; + + /// No description provided for @addMember. + /// + /// In en, this message translates to: + /// **'Add member'** + String get addMember; + + /// No description provided for @createGroup. + /// + /// In en, this message translates to: + /// **'Create group'** + String get createGroup; + + /// No description provided for @leaveGroup. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroup; + + /// No description provided for @createContactRequest. + /// + /// In en, this message translates to: + /// **'Create contact request'** + String get createContactRequest; + + /// No description provided for @contactRequestSend. + /// + /// In en, this message translates to: + /// **'Contact request send'** + String get contactRequestSend; + + /// No description provided for @makeAdmin. + /// + /// In en, this message translates to: + /// **'Make admin'** + String get makeAdmin; + + /// No description provided for @removeAdmin. + /// + /// In en, this message translates to: + /// **'Remove as admin'** + String get removeAdmin; + + /// No description provided for @removeFromGroup. + /// + /// In en, this message translates to: + /// **'Remove from group'** + String get removeFromGroup; + + /// No description provided for @admin. + /// + /// In en, this message translates to: + /// **'Admin'** + String get admin; + + /// No description provided for @revokeAdminRightsTitle. + /// + /// In en, this message translates to: + /// **'Revoke {username}\'s admin rights?'** + String revokeAdminRightsTitle(Object username); + + /// No description provided for @revokeAdminRightsOkBtn. + /// + /// In en, this message translates to: + /// **'Remove as admin'** + String get revokeAdminRightsOkBtn; + + /// No description provided for @makeAdminRightsTitle. + /// + /// In en, this message translates to: + /// **'Make {username} an admin?'** + String makeAdminRightsTitle(Object username); + + /// No description provided for @makeAdminRightsBody. + /// + /// In en, this message translates to: + /// **'{username} will be able to edit this group and its members.'** + String makeAdminRightsBody(Object username); + + /// No description provided for @makeAdminRightsOkBtn. + /// + /// In en, this message translates to: + /// **'Make admin'** + String get makeAdminRightsOkBtn; + + /// No description provided for @updateGroup. + /// + /// In en, this message translates to: + /// **'Update group'** + String get updateGroup; + + /// No description provided for @alreadyInGroup. + /// + /// In en, this message translates to: + /// **'Already in Group'** + String get alreadyInGroup; + + /// No description provided for @removeContactFromGroupTitle. + /// + /// In en, this message translates to: + /// **'Remove {username} from this group?'** + String removeContactFromGroupTitle(Object username); + + /// No description provided for @youChangedGroupName. + /// + /// In en, this message translates to: + /// **'You have changed the group name to \"{newGroupName}\".'** + String youChangedGroupName(Object newGroupName); + + /// No description provided for @makerChangedGroupName. + /// + /// In en, this message translates to: + /// **'{maker} has changed the group name to \"{newGroupName}\".'** + String makerChangedGroupName(Object maker, Object newGroupName); + + /// No description provided for @youCreatedGroup. + /// + /// In en, this message translates to: + /// **'You have created the group.'** + String get youCreatedGroup; + + /// No description provided for @makerCreatedGroup. + /// + /// In en, this message translates to: + /// **'{maker} has created the group.'** + String makerCreatedGroup(Object maker); + + /// No description provided for @youRemovedMember. + /// + /// In en, this message translates to: + /// **'You have removed {affected} from the group.'** + String youRemovedMember(Object affected); + + /// No description provided for @makerRemovedMember. + /// + /// In en, this message translates to: + /// **'{maker} has removed {affected} from the group.'** + String makerRemovedMember(Object affected, Object maker); + + /// No description provided for @youAddedMember. + /// + /// In en, this message translates to: + /// **'You have added {affected} to the group.'** + String youAddedMember(Object affected); + + /// No description provided for @makerAddedMember. + /// + /// In en, this message translates to: + /// **'{maker} has added {affected} to the group.'** + String makerAddedMember(Object affected, Object maker); + + /// No description provided for @youMadeAdmin. + /// + /// In en, this message translates to: + /// **'You made {affected} an admin.'** + String youMadeAdmin(Object affected); + + /// No description provided for @makerMadeAdmin. + /// + /// In en, this message translates to: + /// **'{maker} made {affected} an admin.'** + String makerMadeAdmin(Object affected, Object maker); + + /// No description provided for @youRevokedAdminRights. + /// + /// In en, this message translates to: + /// **'You revoked {affectedR} admin rights.'** + String youRevokedAdminRights(Object affectedR); + + /// No description provided for @makerRevokedAdminRights. + /// + /// In en, this message translates to: + /// **'{maker} revoked {affectedR} admin rights.'** + String makerRevokedAdminRights(Object affectedR, Object maker); + + /// No description provided for @youLeftGroup. + /// + /// In en, this message translates to: + /// **'You have left the group.'** + String get youLeftGroup; + + /// No description provided for @makerLeftGroup. + /// + /// In en, this message translates to: + /// **'{maker} has left the group.'** + String makerLeftGroup(Object maker); + + /// No description provided for @groupActionYou. + /// + /// In en, this message translates to: + /// **'you'** + String get groupActionYou; + + /// No description provided for @groupActionYour. + /// + /// In en, this message translates to: + /// **'your'** + String get groupActionYour; + + /// No description provided for @notificationFillerIn. + /// + /// In en, this message translates to: + /// **'in'** + String get notificationFillerIn; + + /// No description provided for @notificationText. + /// + /// In en, this message translates to: + /// **'sent a message{inGroup}.'** + String notificationText(Object inGroup); + + /// No description provided for @notificationTwonly. + /// + /// In en, this message translates to: + /// **'sent a twonly{inGroup}.'** + String notificationTwonly(Object inGroup); + + /// No description provided for @notificationVideo. + /// + /// In en, this message translates to: + /// **'sent a video{inGroup}.'** + String notificationVideo(Object inGroup); + + /// No description provided for @notificationImage. + /// + /// In en, this message translates to: + /// **'sent a image{inGroup}.'** + String notificationImage(Object inGroup); + + /// No description provided for @notificationAudio. + /// + /// In en, this message translates to: + /// **'sent a voice message{inGroup}.'** + String notificationAudio(Object inGroup); + + /// No description provided for @notificationAddedToGroup. + /// + /// In en, this message translates to: + /// **'has added you to \"{groupname}\"'** + String notificationAddedToGroup(Object groupname); + + /// No description provided for @notificationContactRequest. + /// + /// In en, this message translates to: + /// **'wants to connect with you.'** + String get notificationContactRequest; + + /// No description provided for @notificationAcceptRequest. + /// + /// In en, this message translates to: + /// **'is now connected with you.'** + String get notificationAcceptRequest; + + /// No description provided for @notificationStoredMediaFile. + /// + /// In en, this message translates to: + /// **'has stored your image.'** + String get notificationStoredMediaFile; + + /// No description provided for @notificationReaction. + /// + /// In en, this message translates to: + /// **'has reacted to your image.'** + String get notificationReaction; + + /// No description provided for @notificationReopenedMedia. + /// + /// In en, this message translates to: + /// **'has reopened your image.'** + String get notificationReopenedMedia; + + /// No description provided for @notificationReactionToVideo. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your video.'** + String notificationReactionToVideo(Object reaction); + + /// No description provided for @notificationReactionToText. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your message.'** + String notificationReactionToText(Object reaction); + + /// No description provided for @notificationReactionToImage. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your image.'** + String notificationReactionToImage(Object reaction); + + /// No description provided for @notificationReactionToAudio. + /// + /// In en, this message translates to: + /// **'has reacted with {reaction} to your audio message.'** + String notificationReactionToAudio(Object reaction); + + /// No description provided for @notificationResponse. + /// + /// In en, this message translates to: + /// **'has responded{inGroup}.'** + String notificationResponse(Object inGroup); + + /// No description provided for @notificationTitleUnknownUser. + /// + /// In en, this message translates to: + /// **'Someone'** + String get notificationTitleUnknownUser; + + /// No description provided for @notificationCategoryMessageTitle. + /// + /// In en, this message translates to: + /// **'Messages'** + String get notificationCategoryMessageTitle; + + /// No description provided for @notificationCategoryMessageDesc. + /// + /// In en, this message translates to: + /// **'Messages from other users.'** + String get notificationCategoryMessageDesc; + + /// No description provided for @groupContextMenuDeleteGroup. + /// + /// In en, this message translates to: + /// **'This will permanently delete all messages in this chat.'** + String get groupContextMenuDeleteGroup; + + /// No description provided for @groupYouAreNowLongerAMember. + /// + /// In en, this message translates to: + /// **'You are no longer part of this group.'** + String get groupYouAreNowLongerAMember; + + /// No description provided for @groupNetworkIssue. + /// + /// In en, this message translates to: + /// **'Network issue. Try again later.'** + String get groupNetworkIssue; + + /// No description provided for @leaveGroupSelectOtherAdminTitle. + /// + /// In en, this message translates to: + /// **'Select another admin'** + String get leaveGroupSelectOtherAdminTitle; + + /// No description provided for @leaveGroupSelectOtherAdminBody. + /// + /// In en, this message translates to: + /// **'To leave the group, you must first select a new administrator.'** + String get leaveGroupSelectOtherAdminBody; + + /// No description provided for @leaveGroupSureTitle. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroupSureTitle; + + /// No description provided for @leaveGroupSureBody. + /// + /// In en, this message translates to: + /// **'Do you really want to leave the group?'** + String get leaveGroupSureBody; + + /// No description provided for @leaveGroupSureOkBtn. + /// + /// In en, this message translates to: + /// **'Leave group'** + String get leaveGroupSureOkBtn; + + /// No description provided for @changeDisplayMaxTime. + /// + /// In en, this message translates to: + /// **'Chats will now be deleted after {time} ({username}).'** + String changeDisplayMaxTime(Object time, Object username); + + /// No description provided for @youChangedDisplayMaxTime. + /// + /// In en, this message translates to: + /// **'Chats will now be deleted after {time}.'** + String youChangedDisplayMaxTime(Object time); + + /// No description provided for @userGotReported. + /// + /// In en, this message translates to: + /// **'User has been reported.'** + String get userGotReported; + + /// No description provided for @deleteChatAfter. + /// + /// In en, this message translates to: + /// **'Delete chat after...'** + String get deleteChatAfter; + + /// No description provided for @deleteChatAfterAnHour. + /// + /// In en, this message translates to: + /// **'one hour.'** + String get deleteChatAfterAnHour; + + /// No description provided for @deleteChatAfterADay. + /// + /// In en, this message translates to: + /// **'one day.'** + String get deleteChatAfterADay; + + /// No description provided for @deleteChatAfterAWeek. + /// + /// In en, this message translates to: + /// **'one week.'** + String get deleteChatAfterAWeek; + + /// No description provided for @deleteChatAfterAMonth. + /// + /// In en, this message translates to: + /// **'one month.'** + String get deleteChatAfterAMonth; + + /// No description provided for @deleteChatAfterAYear. + /// + /// In en, this message translates to: + /// **'one year.'** + String get deleteChatAfterAYear; + + /// No description provided for @yourTwonlyScore. + /// + /// In en, this message translates to: + /// **'Your twonly-Score'** + String get yourTwonlyScore; + + /// No description provided for @registrationClosed. + /// + /// In en, this message translates to: + /// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'** + String get registrationClosed; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 598a761..fe61244 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -122,6 +122,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get shareImageSearchAllContacts => 'Alle Kontakte durchsuchen'; + @override + String get startNewChatSearchHint => 'Name, Benutzername oder Gruppenname'; + @override String get shareImagedSelectAll => 'Alle auswählen'; @@ -536,6 +539,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get cancel => 'Abbrechen'; + @override + String get now => 'Jetzt'; + + @override + String get you => 'Du'; + + @override + String get minutesShort => 'Min.'; + + @override + String get image => 'Bild'; + + @override + String get video => 'Video'; + @override String get react => 'Reagieren'; @@ -545,6 +563,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get copy => 'Kopieren'; + @override + String get edit => 'Bearbeiten'; + @override String get delete => 'Löschen'; @@ -609,7 +630,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get errorUsernameAlreadyTaken => - 'Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.'; + 'Der Benutzername ist bereits vergeben.'; @override String get errorSignatureNotValid => @@ -684,7 +705,7 @@ class AppLocalizationsDe extends AppLocalizations { String get proFeature2 => '1 zusätzlicher Plus Benutzer'; @override - String get proFeature3 => 'Zusatzfunktionen (coming-soon)'; + String get proFeature3 => 'Flammen wiederherstellen'; @override String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)'; @@ -915,7 +936,10 @@ class AppLocalizationsDe extends AppLocalizations { String get deleteTitle => 'Bist du dir sicher?'; @override - String get deleteOkBtn => 'Für mich löschen'; + String get deleteOkBtnForAll => 'Für alle löschen'; + + @override + String get deleteOkBtnForMe => 'Für mich löschen'; @override String get deleteImageTitle => 'Bist du dir sicher?'; @@ -952,6 +976,10 @@ class AppLocalizationsDe extends AppLocalizations { String get backupTwonlySafeDesc => 'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.'; + @override + String get backupNoPasswordRecovery => + 'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.'; + @override String get backupServer => 'Server'; @@ -999,11 +1027,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupTwonlySafeLongDesc => - 'twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.'; + 'twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.'; @override String get backupSelectStrongPassword => - 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Safe-Backup wiederherstellen möchtest.'; + 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.'; @override String get password => 'Passwort'; @@ -1026,7 +1054,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupOwnServerDesc => - 'Speichere dein twonly Safe-Backups auf einem Server deiner Wahl.'; + 'Speichere dein twonly Backup auf einem Server deiner Wahl.'; @override String get backupUseOwnServer => 'Server verwenden'; @@ -1037,15 +1065,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupTwonlySaveNow => 'Jetzt speichern'; + @override + String get backupChangePassword => 'Password ändern'; + @override String get twonlySafeRecoverTitle => 'Recovery'; @override String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Safe, you can restore it here.'; + 'Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.'; @override - String get twonlySafeRecoverBtn => 'Restore backup'; + String get twonlySafeRecoverBtn => 'Backup wiederherstellen'; @override String get inviteFriends => 'Freunde einladen'; @@ -1067,6 +1098,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get doubleClickToReopen => 'Doppelklicken zum\nerneuten Öffnen.'; + @override + String get uploadLimitReached => + 'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.'; + @override String get retransmissionRequested => 'Wird erneut versucht.'; @@ -1091,4 +1126,359 @@ class AppLocalizationsDe extends AppLocalizations { @override String get newDeviceRegistered => 'Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.'; + + @override + String get tabToRemoveEmoji => 'Tippen um zu entfernen'; + + @override + String get quotedMessageWasDeleted => + 'Die zitierte Nachricht wurde gelöscht.'; + + @override + String get messageWasDeleted => 'Nachricht wurde gelöscht.'; + + @override + String get messageWasDeletedShort => 'Gelöscht'; + + @override + String get sent => 'Versendet'; + + @override + String get sentTo => 'Zugestellt an'; + + @override + String get received => 'Empfangen'; + + @override + String get opened => 'Geöffnet'; + + @override + String get waitingForInternet => 'Warten auf Internet'; + + @override + String get editHistory => 'Bearbeitungshistorie'; + + @override + String get archivedChats => 'Archivierte Chats'; + + @override + String get durationShortSecond => 'Sek.'; + + @override + String get durationShortMinute => 'Min.'; + + @override + String get durationShortHour => 'Std.'; + + @override + String durationShortDays(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Tage', + one: '1 Tag', + ); + return '$_temp0'; + } + + @override + String get contacts => 'Kontakte'; + + @override + String get groups => 'Gruppen'; + + @override + String get newGroup => 'Neue Gruppe'; + + @override + String get selectMembers => 'Mitglieder auswählen'; + + @override + String get selectGroupName => 'Gruppennamen wählen'; + + @override + String get groupNameInput => 'Gruppennamen'; + + @override + String get groupMembers => 'Mitglieder'; + + @override + String get addMember => 'Mitglied hinzufügen'; + + @override + String get createGroup => 'Gruppe erstellen'; + + @override + String get leaveGroup => 'Gruppe verlassen'; + + @override + String get createContactRequest => 'Kontaktanfrage erstellen'; + + @override + String get contactRequestSend => 'Kontakanfrage gesendet'; + + @override + String get makeAdmin => 'Zum Admin machen'; + + @override + String get removeAdmin => 'Als Admin entfernen'; + + @override + String get removeFromGroup => 'Aus Gruppe entfernen'; + + @override + String get admin => 'Admin'; + + @override + String revokeAdminRightsTitle(Object username) { + return 'Adminrechte von $username entfernen?'; + } + + @override + String get revokeAdminRightsOkBtn => 'Als Admin entfernen'; + + @override + String makeAdminRightsTitle(Object username) { + return '$username zum Admin machen?'; + } + + @override + String makeAdminRightsBody(Object username) { + return '$username wird diese Gruppe und ihre Mitglieder bearbeiten können.'; + } + + @override + String get makeAdminRightsOkBtn => 'Zum Admin machen'; + + @override + String get updateGroup => 'Gruppe aktualisieren'; + + @override + String get alreadyInGroup => 'Bereits Mitglied'; + + @override + String removeContactFromGroupTitle(Object username) { + return '$username aus dieser Gruppe entfernen?'; + } + + @override + String youChangedGroupName(Object newGroupName) { + return 'Du hast den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String makerChangedGroupName(Object maker, Object newGroupName) { + return '$maker hat den Gruppennamen zu „$newGroupName“ geändert.'; + } + + @override + String get youCreatedGroup => 'Du hast die Gruppe erstellt.'; + + @override + String makerCreatedGroup(Object maker) { + return '$maker hat die Gruppe erstellt.'; + } + + @override + String youRemovedMember(Object affected) { + return 'Du hast $affected aus der Gruppe entfernt.'; + } + + @override + String makerRemovedMember(Object affected, Object maker) { + return '$maker hat $affected aus der Gruppe entfernt.'; + } + + @override + String youAddedMember(Object affected) { + return 'Du hast $affected zur Gruppe hinzugefügt.'; + } + + @override + String makerAddedMember(Object affected, Object maker) { + return '$maker hat $affected zur Gruppe hinzugefügt.'; + } + + @override + String youMadeAdmin(Object affected) { + return 'Du hast $affected zum Administrator gemacht.'; + } + + @override + String makerMadeAdmin(Object affected, Object maker) { + return '$maker hat $affected zum Administrator gemacht.'; + } + + @override + String youRevokedAdminRights(Object affectedR) { + return 'Du hast $affectedR die Administratorrechte entzogen.'; + } + + @override + String makerRevokedAdminRights(Object affectedR, Object maker) { + return '$maker hat $affectedR die Administratorrechte entzogen.'; + } + + @override + String get youLeftGroup => 'Du hast die Gruppe verlassen.'; + + @override + String makerLeftGroup(Object maker) { + return '$maker hat die Gruppe verlassen.'; + } + + @override + String get groupActionYou => 'dich'; + + @override + String get groupActionYour => 'deine'; + + @override + String get notificationFillerIn => 'in'; + + @override + String notificationText(Object inGroup) { + return 'hat eine Nachricht$inGroup gesendet.'; + } + + @override + String notificationTwonly(Object inGroup) { + return 'hat ein twonly$inGroup gesendet.'; + } + + @override + String notificationVideo(Object inGroup) { + return 'hat ein Video$inGroup gesendet.'; + } + + @override + String notificationImage(Object inGroup) { + return 'hat ein Bild$inGroup gesendet.'; + } + + @override + String notificationAudio(Object inGroup) { + return 'hat eine Sprachnachricht$inGroup gesendet.'; + } + + @override + String notificationAddedToGroup(Object groupname) { + return 'hat dich zu \"$groupname\" hinzugefügt.'; + } + + @override + String get notificationContactRequest => 'möchte sich mit dir vernetzen.'; + + @override + String get notificationAcceptRequest => 'ist jetzt mit dir vernetzt.'; + + @override + String get notificationStoredMediaFile => 'hat dein Bild gespeichert.'; + + @override + String get notificationReaction => 'hat auf dein Bild reagiert.'; + + @override + String get notificationReopenedMedia => 'hat dein Bild erneut geöffnet.'; + + @override + String notificationReactionToVideo(Object reaction) { + return 'hat mit $reaction auf dein Video reagiert.'; + } + + @override + String notificationReactionToText(Object reaction) { + return 'hat mit $reaction auf deine Nachricht reagiert.'; + } + + @override + String notificationReactionToImage(Object reaction) { + return 'hat mit $reaction auf dein Bild reagiert.'; + } + + @override + String notificationReactionToAudio(Object reaction) { + return 'hat mit $reaction auf deine Sprachnachricht reagiert.'; + } + + @override + String notificationResponse(Object inGroup) { + return 'hat dir$inGroup geantwortet.'; + } + + @override + String get notificationTitleUnknownUser => 'Jemand'; + + @override + String get notificationCategoryMessageTitle => 'Nachrichten'; + + @override + String get notificationCategoryMessageDesc => + 'Nachrichten von anderen Benutzern.'; + + @override + String get groupContextMenuDeleteGroup => + 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; + + @override + String get groupYouAreNowLongerAMember => + 'Du bist nicht mehr Mitglied dieser Gruppe.'; + + @override + String get groupNetworkIssue => + 'Netzwerkproblem. Bitte probiere es später noch einmal.'; + + @override + String get leaveGroupSelectOtherAdminTitle => 'Einen Admin auswählen'; + + @override + String get leaveGroupSelectOtherAdminBody => + 'Um die Gruppe zu verlassen, musst du zuerst einen neuen Administrator auswählen.'; + + @override + String get leaveGroupSureTitle => 'Gruppe verlassen'; + + @override + String get leaveGroupSureBody => 'Willst du die Gruppe wirklich verlassen?'; + + @override + String get leaveGroupSureOkBtn => 'Gruppe verlassen'; + + @override + String changeDisplayMaxTime(Object time, Object username) { + return 'Chats werden ab jetzt nach $time gelöscht ($username).'; + } + + @override + String youChangedDisplayMaxTime(Object time) { + return 'Chats werden ab jetzt nach $time gelöscht.'; + } + + @override + String get userGotReported => 'Benutzer wurde gemeldet.'; + + @override + String get deleteChatAfter => 'Chat löschen nach...'; + + @override + String get deleteChatAfterAnHour => 'einer Stunde.'; + + @override + String get deleteChatAfterADay => 'einem Tag.'; + + @override + String get deleteChatAfterAWeek => 'einer Woche.'; + + @override + String get deleteChatAfterAMonth => 'einem Monat.'; + + @override + String get deleteChatAfterAYear => 'einem Jahr.'; + + @override + String get yourTwonlyScore => 'Dein twonly-Score'; + + @override + String get registrationClosed => + 'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index e199353..790bd19 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -121,6 +121,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get shareImageSearchAllContacts => 'Search all contacts'; + @override + String get startNewChatSearchHint => 'Name, username or groupname'; + @override String get shareImagedSelectAll => 'Select all'; @@ -531,6 +534,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cancel => 'Cancel'; + @override + String get now => 'Now'; + + @override + String get you => 'You'; + + @override + String get minutesShort => 'min.'; + + @override + String get image => 'Image'; + + @override + String get video => 'Video'; + @override String get react => 'React'; @@ -540,6 +558,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copy => 'Copy'; + @override + String get edit => 'Edit'; + @override String get delete => 'Delete'; @@ -603,8 +624,7 @@ class AppLocalizationsEn extends AppLocalizations { 'The invitation code you provided is invalid. Please check the code and try again.'; @override - String get errorUsernameAlreadyTaken => - 'The username you want to use is already taken. Please choose a different username.'; + String get errorUsernameAlreadyTaken => 'The username is already taken.'; @override String get errorSignatureNotValid => @@ -909,7 +929,10 @@ class AppLocalizationsEn extends AppLocalizations { String get deleteTitle => 'Are you sure?'; @override - String get deleteOkBtn => 'Delete for me'; + String get deleteOkBtnForAll => 'Delete for all'; + + @override + String get deleteOkBtnForMe => 'Delete for me'; @override String get deleteImageTitle => 'Are you sure?'; @@ -946,6 +969,10 @@ class AppLocalizationsEn extends AppLocalizations { String get backupTwonlySafeDesc => 'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'; + @override + String get backupNoPasswordRecovery => + 'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'; + @override String get backupServer => 'Server'; @@ -993,11 +1020,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupTwonlySafeLongDesc => - 'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'; + 'twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen username so that others can find you.\n\ntwonly Backup regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device.'; @override String get backupSelectStrongPassword => - 'Choose a secure password. This is required if you want to restore your twonly Safe backup.'; + 'Choose a secure password. This is required if you want to restore your twonly Backup.'; @override String get password => 'Password'; @@ -1020,7 +1047,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupOwnServerDesc => - 'Save your twonly safe backups at twonly or on any server of your choice.'; + 'Save your twonly Backup at twonly or on any server of your choice.'; @override String get backupUseOwnServer => 'Use server'; @@ -1031,12 +1058,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupTwonlySaveNow => 'Save now'; + @override + String get backupChangePassword => 'Change password'; + @override String get twonlySafeRecoverTitle => 'Recovery'; @override String get twonlySafeRecoverDesc => - 'If you have created a backup with twonly Safe, you can restore it here.'; + 'If you have created a backup with twonly Backup, you can restore it here.'; @override String get twonlySafeRecoverBtn => 'Restore backup'; @@ -1061,6 +1091,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get doubleClickToReopen => 'Double-click\nto open again'; + @override + String get uploadLimitReached => + 'The upload limit has\been reached. Upgrade to Pro\nor wait until tomorrow.'; + @override String get retransmissionRequested => 'Retransmission requested'; @@ -1085,4 +1119,356 @@ class AppLocalizationsEn extends AppLocalizations { @override String get newDeviceRegistered => 'You have logged in on another device. You have therefore been logged out here.'; + + @override + String get tabToRemoveEmoji => 'Tab to remove'; + + @override + String get quotedMessageWasDeleted => 'The quoted message has been deleted.'; + + @override + String get messageWasDeleted => 'Message has been deleted.'; + + @override + String get messageWasDeletedShort => 'Deleted'; + + @override + String get sent => 'Delivered'; + + @override + String get sentTo => 'Delivered to'; + + @override + String get received => 'Received'; + + @override + String get opened => 'Opened'; + + @override + String get waitingForInternet => 'Waiting for internet'; + + @override + String get editHistory => 'Edit history'; + + @override + String get archivedChats => 'Archived chats'; + + @override + String get durationShortSecond => 'Sec.'; + + @override + String get durationShortMinute => 'Min.'; + + @override + String get durationShortHour => 'Hrs.'; + + @override + String durationShortDays(num count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Days', + one: '1 Day', + ); + return '$_temp0'; + } + + @override + String get contacts => 'Contacts'; + + @override + String get groups => 'Groups'; + + @override + String get newGroup => 'New group'; + + @override + String get selectMembers => 'Select members'; + + @override + String get selectGroupName => 'Select group name'; + + @override + String get groupNameInput => 'Group name'; + + @override + String get groupMembers => 'Members'; + + @override + String get addMember => 'Add member'; + + @override + String get createGroup => 'Create group'; + + @override + String get leaveGroup => 'Leave group'; + + @override + String get createContactRequest => 'Create contact request'; + + @override + String get contactRequestSend => 'Contact request send'; + + @override + String get makeAdmin => 'Make admin'; + + @override + String get removeAdmin => 'Remove as admin'; + + @override + String get removeFromGroup => 'Remove from group'; + + @override + String get admin => 'Admin'; + + @override + String revokeAdminRightsTitle(Object username) { + return 'Revoke $username\'s admin rights?'; + } + + @override + String get revokeAdminRightsOkBtn => 'Remove as admin'; + + @override + String makeAdminRightsTitle(Object username) { + return 'Make $username an admin?'; + } + + @override + String makeAdminRightsBody(Object username) { + return '$username will be able to edit this group and its members.'; + } + + @override + String get makeAdminRightsOkBtn => 'Make admin'; + + @override + String get updateGroup => 'Update group'; + + @override + String get alreadyInGroup => 'Already in Group'; + + @override + String removeContactFromGroupTitle(Object username) { + return 'Remove $username from this group?'; + } + + @override + String youChangedGroupName(Object newGroupName) { + return 'You have changed the group name to \"$newGroupName\".'; + } + + @override + String makerChangedGroupName(Object maker, Object newGroupName) { + return '$maker has changed the group name to \"$newGroupName\".'; + } + + @override + String get youCreatedGroup => 'You have created the group.'; + + @override + String makerCreatedGroup(Object maker) { + return '$maker has created the group.'; + } + + @override + String youRemovedMember(Object affected) { + return 'You have removed $affected from the group.'; + } + + @override + String makerRemovedMember(Object affected, Object maker) { + return '$maker has removed $affected from the group.'; + } + + @override + String youAddedMember(Object affected) { + return 'You have added $affected to the group.'; + } + + @override + String makerAddedMember(Object affected, Object maker) { + return '$maker has added $affected to the group.'; + } + + @override + String youMadeAdmin(Object affected) { + return 'You made $affected an admin.'; + } + + @override + String makerMadeAdmin(Object affected, Object maker) { + return '$maker made $affected an admin.'; + } + + @override + String youRevokedAdminRights(Object affectedR) { + return 'You revoked $affectedR admin rights.'; + } + + @override + String makerRevokedAdminRights(Object affectedR, Object maker) { + return '$maker revoked $affectedR admin rights.'; + } + + @override + String get youLeftGroup => 'You have left the group.'; + + @override + String makerLeftGroup(Object maker) { + return '$maker has left the group.'; + } + + @override + String get groupActionYou => 'you'; + + @override + String get groupActionYour => 'your'; + + @override + String get notificationFillerIn => 'in'; + + @override + String notificationText(Object inGroup) { + return 'sent a message$inGroup.'; + } + + @override + String notificationTwonly(Object inGroup) { + return 'sent a twonly$inGroup.'; + } + + @override + String notificationVideo(Object inGroup) { + return 'sent a video$inGroup.'; + } + + @override + String notificationImage(Object inGroup) { + return 'sent a image$inGroup.'; + } + + @override + String notificationAudio(Object inGroup) { + return 'sent a voice message$inGroup.'; + } + + @override + String notificationAddedToGroup(Object groupname) { + return 'has added you to \"$groupname\"'; + } + + @override + String get notificationContactRequest => 'wants to connect with you.'; + + @override + String get notificationAcceptRequest => 'is now connected with you.'; + + @override + String get notificationStoredMediaFile => 'has stored your image.'; + + @override + String get notificationReaction => 'has reacted to your image.'; + + @override + String get notificationReopenedMedia => 'has reopened your image.'; + + @override + String notificationReactionToVideo(Object reaction) { + return 'has reacted with $reaction to your video.'; + } + + @override + String notificationReactionToText(Object reaction) { + return 'has reacted with $reaction to your message.'; + } + + @override + String notificationReactionToImage(Object reaction) { + return 'has reacted with $reaction to your image.'; + } + + @override + String notificationReactionToAudio(Object reaction) { + return 'has reacted with $reaction to your audio message.'; + } + + @override + String notificationResponse(Object inGroup) { + return 'has responded$inGroup.'; + } + + @override + String get notificationTitleUnknownUser => 'Someone'; + + @override + String get notificationCategoryMessageTitle => 'Messages'; + + @override + String get notificationCategoryMessageDesc => 'Messages from other users.'; + + @override + String get groupContextMenuDeleteGroup => + 'This will permanently delete all messages in this chat.'; + + @override + String get groupYouAreNowLongerAMember => + 'You are no longer part of this group.'; + + @override + String get groupNetworkIssue => 'Network issue. Try again later.'; + + @override + String get leaveGroupSelectOtherAdminTitle => 'Select another admin'; + + @override + String get leaveGroupSelectOtherAdminBody => + 'To leave the group, you must first select a new administrator.'; + + @override + String get leaveGroupSureTitle => 'Leave group'; + + @override + String get leaveGroupSureBody => 'Do you really want to leave the group?'; + + @override + String get leaveGroupSureOkBtn => 'Leave group'; + + @override + String changeDisplayMaxTime(Object time, Object username) { + return 'Chats will now be deleted after $time ($username).'; + } + + @override + String youChangedDisplayMaxTime(Object time) { + return 'Chats will now be deleted after $time.'; + } + + @override + String get userGotReported => 'User has been reported.'; + + @override + String get deleteChatAfter => 'Delete chat after...'; + + @override + String get deleteChatAfterAnHour => 'one hour.'; + + @override + String get deleteChatAfterADay => 'one day.'; + + @override + String get deleteChatAfterAWeek => 'one week.'; + + @override + String get deleteChatAfterAMonth => 'one month.'; + + @override + String get deleteChatAfterAYear => 'one year.'; + + @override + String get yourTwonlyScore => 'Your twonly-Score'; + + @override + String get registrationClosed => + 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; } diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart deleted file mode 100644 index 7a115fb..0000000 --- a/lib/src/model/json/message.dart +++ /dev/null @@ -1,331 +0,0 @@ -// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods - -import 'package:flutter/material.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Color getMessageColorFromType(MessageContent content, BuildContext context) { - Color color; - - if (content is TextMessageContent) { - color = Colors.blueAccent; - } else { - if (content is MediaMessageContent) { - if (content.isRealTwonly) { - color = context.color.primary; - } else { - if (content.isVideo) { - color = const Color.fromARGB(255, 243, 33, 208); - } else { - color = Colors.redAccent; - } - } - } else { - return (isDarkMode(context)) ? Colors.white : Colors.black; - } - } - return color; -} - -extension MessageKindExtension on MessageKind { - String get name => toString().split('.').last; - - static MessageKind fromString(String name) { - return MessageKind.values.firstWhere((e) => e.name == name); - } -} - -class MessageJson { - MessageJson({ - required this.kind, - required this.content, - required this.timestamp, - this.messageReceiverId, - this.messageSenderId, - this.retransId, - }); - final MessageKind kind; - final MessageContent? content; - final int? messageReceiverId; - final int? messageSenderId; - int? retransId; - DateTime timestamp; - - @override - String toString() { - return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; - } - - static MessageJson fromJson(Map json) { - final kind = MessageKindExtension.fromString(json['kind'] as String); - - return MessageJson( - kind: kind, - messageReceiverId: (json['messageReceiverId'] as num?)?.toInt(), - messageSenderId: (json['messageSenderId'] as num?)?.toInt(), - retransId: (json['retransId'] as num?)?.toInt(), - content: MessageContent.fromJson( - kind, - json['content'] as Map, - ), - timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int), - ); - } - - Map toJson() => { - 'kind': kind.name, - 'content': content?.toJson(), - 'messageReceiverId': messageReceiverId, - 'messageSenderId': messageSenderId, - 'retransId': retransId, - 'timestamp': timestamp.toUtc().millisecondsSinceEpoch, - }; -} - -class MessageContent { - MessageContent(); - - static MessageContent? fromJson(MessageKind kind, Map json) { - switch (kind) { - case MessageKind.media: - return MediaMessageContent.fromJson(json); - case MessageKind.textMessage: - return TextMessageContent.fromJson(json); - case MessageKind.profileChange: - return ProfileContent.fromJson(json); - case MessageKind.pushKey: - return PushKeyContent.fromJson(json); - case MessageKind.reopenedMedia: - return ReopenedMediaFileContent.fromJson(json); - case MessageKind.flameSync: - return FlameSyncContent.fromJson(json); - case MessageKind.ack: - return AckContent.fromJson(json); - case MessageKind.signalDecryptError: - return SignalDecryptErrorContent.fromJson(json); - case MessageKind.storedMediaFile: - case MessageKind.contactRequest: - case MessageKind.rejectRequest: - case MessageKind.acceptRequest: - case MessageKind.opened: - case MessageKind.requestPushKey: - case MessageKind.receiveMediaError: - } - return null; - } - - Map toJson() { - return {}; - } -} - -class MediaMessageContent extends MessageContent { - MediaMessageContent({ - required this.maxShowTime, - required this.isRealTwonly, - required this.isVideo, - required this.mirrorVideo, - this.downloadToken, - this.encryptionKey, - this.encryptionMac, - this.encryptionNonce, - }); - final int maxShowTime; - final bool isRealTwonly; - final bool isVideo; - final bool mirrorVideo; - final List? downloadToken; - final List? encryptionKey; - final List? encryptionMac; - final List? encryptionNonce; - - static MediaMessageContent fromJson(Map json) { - return MediaMessageContent( - downloadToken: json['downloadToken'] == null - ? null - : List.from(json['downloadToken'] as List), - encryptionKey: json['encryptionKey'] == null - ? null - : List.from(json['encryptionKey'] as List), - encryptionMac: json['encryptionMac'] == null - ? null - : List.from(json['encryptionMac'] as List), - encryptionNonce: json['encryptionNonce'] == null - ? null - : List.from(json['encryptionNonce'] as List), - maxShowTime: json['maxShowTime'] as int, - isRealTwonly: json['isRealTwonly'] as bool, - isVideo: json['isVideo'] as bool? ?? false, - mirrorVideo: json['mirrorVideo'] as bool? ?? false, - ); - } - - @override - Map toJson() { - return { - 'downloadToken': downloadToken, - 'encryptionKey': encryptionKey, - 'encryptionMac': encryptionMac, - 'encryptionNonce': encryptionNonce, - 'isRealTwonly': isRealTwonly, - 'maxShowTime': maxShowTime, - 'isVideo': isVideo, - 'mirrorVideo': mirrorVideo, - }; - } -} - -class TextMessageContent extends MessageContent { - TextMessageContent({ - required this.text, - this.responseToMessageId, - this.responseToOtherMessageId, - }); - String text; - int? responseToMessageId; - int? responseToOtherMessageId; - - static TextMessageContent fromJson(Map json) { - return TextMessageContent( - text: json['text'] as String, - responseToOtherMessageId: json.containsKey('responseToOtherMessageId') - ? json['responseToOtherMessageId'] as int? - : null, - responseToMessageId: json.containsKey('responseToMessageId') - ? json['responseToMessageId'] as int? - : null, - ); - } - - @override - Map toJson() { - return { - 'text': text, - 'responseToMessageId': responseToMessageId, - 'responseToOtherMessageId': responseToOtherMessageId, - }; - } -} - -class ReopenedMediaFileContent extends MessageContent { - ReopenedMediaFileContent({required this.messageId}); - int messageId; - - static ReopenedMediaFileContent fromJson(Map json) { - return ReopenedMediaFileContent(messageId: json['messageId'] as int); - } - - @override - Map toJson() { - return {'messageId': messageId}; - } -} - -class SignalDecryptErrorContent extends MessageContent { - SignalDecryptErrorContent({required this.encryptedHash}); - List encryptedHash; - - static SignalDecryptErrorContent fromJson(Map json) { - return SignalDecryptErrorContent( - encryptedHash: List.from(json['encryptedHash'] as List), - ); - } - - @override - Map toJson() { - return { - 'encryptedHash': encryptedHash, - }; - } -} - -class AckContent extends MessageContent { - AckContent({required this.messageIdToAck, required this.retransIdToAck}); - int? messageIdToAck; - int retransIdToAck; - - static AckContent fromJson(Map json) { - return AckContent( - messageIdToAck: json['messageIdToAck'] as int?, - retransIdToAck: json['retransIdToAck'] as int, - ); - } - - @override - Map toJson() { - return { - 'messageIdToAck': messageIdToAck, - 'retransIdToAck': retransIdToAck, - }; - } -} - -class ProfileContent extends MessageContent { - ProfileContent({required this.avatarSvg, required this.displayName}); - String avatarSvg; - String displayName; - - static ProfileContent fromJson(Map json) { - return ProfileContent( - avatarSvg: json['avatarSvg'] as String, - displayName: json['displayName'] as String, - ); - } - - @override - Map toJson() { - return {'avatarSvg': avatarSvg, 'displayName': displayName}; - } -} - -class PushKeyContent extends MessageContent { - PushKeyContent({required this.keyId, required this.key}); - int keyId; - List key; - - static PushKeyContent fromJson(Map json) { - return PushKeyContent( - keyId: json['keyId'] as int, - key: List.from(json['key'] as List), - ); - } - - @override - Map toJson() { - return { - 'keyId': keyId, - 'key': key, - }; - } -} - -class FlameSyncContent extends MessageContent { - FlameSyncContent({ - required this.flameCounter, - required this.bestFriend, - required this.lastFlameCounterChange, - }); - int flameCounter; - DateTime lastFlameCounterChange; - bool bestFriend; - - static FlameSyncContent fromJson(Map json) { - return FlameSyncContent( - flameCounter: json['flameCounter'] as int, - bestFriend: json['bestFriend'] as bool, - lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch( - json['lastFlameCounterChange'] as int, - ), - ); - } - - @override - Map toJson() { - return { - 'flameCounter': flameCounter, - 'bestFriend': bestFriend, - 'lastFlameCounterChange': - lastFlameCounterChange.toUtc().millisecondsSinceEpoch, - }; - } -} diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 987ff9f..604c273 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -9,16 +9,12 @@ class UserData { required this.username, required this.displayName, required this.subscriptionPlan, - required this.isDemoUser, }); factory UserData.fromJson(Map json) => _$UserDataFromJson(json); final int userId; - @JsonKey(defaultValue: false) - bool isDemoUser = false; - // -- USER PROFILE -- String username; @@ -26,6 +22,9 @@ class UserData { String? avatarSvg; String? avatarJson; + @JsonKey(defaultValue: 0) + int appVersion = 0; + @JsonKey(defaultValue: 0) int avatarCounter = 0; @@ -65,14 +64,12 @@ class UserData { @JsonKey(defaultValue: false) bool storeMediaFilesInGallery = false; - List? lastUsedEditorEmojis; - String? lastPlanBallance; String? additionalUserInvites; List? tutorialDisplayed; - int? myBestFriendContactId; + String? myBestFriendGroupId; DateTime? signalLastSignedPreKeyUpdated; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index a73b3ec..6de5cbd 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -11,10 +11,10 @@ UserData _$UserDataFromJson(Map json) => UserData( username: json['username'] as String, displayName: json['displayName'] as String, subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', - isDemoUser: json['isDemoUser'] as bool? ?? false, ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? + ..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..isDeveloper = json['isDeveloper'] as bool? ?? false ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 @@ -40,15 +40,12 @@ UserData _$UserDataFromJson(Map json) => UserData( ) ..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool? ?? false - ..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List?) - ?.map((e) => e as String) - .toList() ..lastPlanBallance = json['lastPlanBallance'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String? ..tutorialDisplayed = (json['tutorialDisplayed'] as List?) ?.map((e) => e as String) .toList() - ..myBestFriendContactId = (json['myBestFriendContactId'] as num?)?.toInt() + ..myBestFriendGroupId = json['myBestFriendGroupId'] as String? ..signalLastSignedPreKeyUpdated = json['signalLastSignedPreKeyUpdated'] == null ? null @@ -74,11 +71,11 @@ UserData _$UserDataFromJson(Map json) => UserData( Map _$UserDataToJson(UserData instance) => { 'userId': instance.userId, - 'isDemoUser': instance.isDemoUser, 'username': instance.username, 'displayName': instance.displayName, 'avatarSvg': instance.avatarSvg, 'avatarJson': instance.avatarJson, + 'appVersion': instance.appVersion, 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, 'deviceId': instance.deviceId, @@ -93,11 +90,10 @@ Map _$UserDataToJson(UserData instance) => { 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, - 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastPlanBallance': instance.lastPlanBallance, 'additionalUserInvites': instance.additionalUserInvites, 'tutorialDisplayed': instance.tutorialDisplayed, - 'myBestFriendContactId': instance.myBestFriendContactId, + 'myBestFriendGroupId': instance.myBestFriendGroupId, 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated?.toIso8601String(), 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index 8e57d55..f0a9532 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -1,88 +1,34 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:drift/drift.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; class MemoryItem { MemoryItem({ - required this.id, + required this.mediaService, required this.messages, - required this.date, - required this.mirrorVideo, - required this.thumbnailPath, - this.imagePath, - this.videoPath, }); - final int id; - final bool mirrorVideo; final List messages; - final DateTime date; - final File thumbnailPath; - final File? imagePath; - final File? videoPath; + final MediaFileService mediaService; - static Future> convertFromMessages( + static Future> convertFromMessages( List messages, ) async { - final items = {}; + final items = {}; for (final message in messages) { - final isSend = message.messageOtherId == null; - final id = message.mediaUploadId ?? message.messageId; - final basePath = await send.getMediaFilePath( - id, - isSend ? 'send' : 'received', - ); - File? imagePath; - late File thumbnailFile; - File? videoPath; - if (File('$basePath.mp4').existsSync()) { - videoPath = File('$basePath.mp4'); - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else if (File('$basePath.png').existsSync()) { - imagePath = File('$basePath.png'); - thumbnailFile = getThumbnailPath(imagePath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForImage(imagePath); - } - } else { - if (message.mediaStored) { - /// media file was deleted, ... remove the file - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - mediaStored: Value(false), - ), - ); - } + if (message.mediaId == null) continue; + + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService == null) continue; + + if (!mediaService.imagePreviewAvailable) { continue; } - var mirrorVideo = false; - if (videoPath != null) { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - mirrorVideo = content.mirrorVideo; - } items .putIfAbsent( - id, + message.mediaId!, () => MemoryItem( - id: id, + mediaService: mediaService, messages: [], - date: message.sendAt, - mirrorVideo: mirrorVideo, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, ), ) .messages diff --git a/lib/src/model/protobuf/api/http/http_requests.pb.dart b/lib/src/model/protobuf/api/http/http_requests.pb.dart index ad378f0..cb5f517 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pb.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pb.dart @@ -158,6 +158,575 @@ class UploadRequest extends $pb.GeneratedMessage { $core.List get messagesOnSuccess => $_getList(2); } +class UpdateGroupState_UpdateTBS extends $pb.GeneratedMessage { + factory UpdateGroupState_UpdateTBS({ + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? publicKey, + $core.List<$core.int>? removeAdmin, + $core.List<$core.int>? addAdmin, + $core.List<$core.int>? nonce, + }) { + final $result = create(); + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + if (removeAdmin != null) { + $result.removeAdmin = removeAdmin; + } + if (addAdmin != null) { + $result.addAdmin = addAdmin; + } + if (nonce != null) { + $result.nonce = nonce; + } + return $result; + } + UpdateGroupState_UpdateTBS._() : super(); + factory UpdateGroupState_UpdateTBS.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UpdateGroupState_UpdateTBS.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UpdateGroupState.UpdateTBS', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'removeAdmin', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'addAdmin', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UpdateGroupState_UpdateTBS clone() => UpdateGroupState_UpdateTBS()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UpdateGroupState_UpdateTBS copyWith(void Function(UpdateGroupState_UpdateTBS) updates) => super.copyWith((message) => updates(message as UpdateGroupState_UpdateTBS)) as UpdateGroupState_UpdateTBS; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UpdateGroupState_UpdateTBS create() => UpdateGroupState_UpdateTBS._(); + UpdateGroupState_UpdateTBS createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UpdateGroupState_UpdateTBS getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UpdateGroupState_UpdateTBS? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get versionId => $_getI64(0); + @$pb.TagNumber(1) + set versionId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasVersionId() => $_has(0); + @$pb.TagNumber(1) + void clearVersionId() => clearField(1); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(3) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(3) + void clearEncryptedGroupState() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get publicKey => $_getN(2); + @$pb.TagNumber(4) + set publicKey($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(4) + $core.bool hasPublicKey() => $_has(2); + @$pb.TagNumber(4) + void clearPublicKey() => clearField(4); + + /// public group key + @$pb.TagNumber(5) + $core.List<$core.int> get removeAdmin => $_getN(3); + @$pb.TagNumber(5) + set removeAdmin($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(5) + $core.bool hasRemoveAdmin() => $_has(3); + @$pb.TagNumber(5) + void clearRemoveAdmin() => clearField(5); + + @$pb.TagNumber(6) + $core.List<$core.int> get addAdmin => $_getN(4); + @$pb.TagNumber(6) + set addAdmin($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(6) + $core.bool hasAddAdmin() => $_has(4); + @$pb.TagNumber(6) + void clearAddAdmin() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get nonce => $_getN(5); + @$pb.TagNumber(7) + set nonce($core.List<$core.int> v) { $_setBytes(5, v); } + @$pb.TagNumber(7) + $core.bool hasNonce() => $_has(5); + @$pb.TagNumber(7) + void clearNonce() => clearField(7); +} + +/// plaintext message send to the server +class UpdateGroupState extends $pb.GeneratedMessage { + factory UpdateGroupState({ + UpdateGroupState_UpdateTBS? update, + $core.List<$core.int>? signature, + }) { + final $result = create(); + if (update != null) { + $result.update = update; + } + if (signature != null) { + $result.signature = signature; + } + return $result; + } + UpdateGroupState._() : super(); + factory UpdateGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory UpdateGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'UpdateGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'update', subBuilder: UpdateGroupState_UpdateTBS.create) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + UpdateGroupState clone() => UpdateGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + UpdateGroupState copyWith(void Function(UpdateGroupState) updates) => super.copyWith((message) => updates(message as UpdateGroupState)) as UpdateGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static UpdateGroupState create() => UpdateGroupState._(); + UpdateGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static UpdateGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static UpdateGroupState? _defaultInstance; + + @$pb.TagNumber(1) + UpdateGroupState_UpdateTBS get update => $_getN(0); + @$pb.TagNumber(1) + set update(UpdateGroupState_UpdateTBS v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasUpdate() => $_has(0); + @$pb.TagNumber(1) + void clearUpdate() => clearField(1); + @$pb.TagNumber(1) + UpdateGroupState_UpdateTBS ensureUpdate() => $_ensure(0); + + @$pb.TagNumber(2) + $core.List<$core.int> get signature => $_getN(1); + @$pb.TagNumber(2) + set signature($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasSignature() => $_has(1); + @$pb.TagNumber(2) + void clearSignature() => clearField(2); +} + +class NewGroupState extends $pb.GeneratedMessage { + factory NewGroupState({ + $core.String? groupId, + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? publicKey, + }) { + final $result = create(); + if (groupId != null) { + $result.groupId = groupId; + } + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + return $result; + } + NewGroupState._() : super(); + factory NewGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory NewGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'NewGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'groupId') + ..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NewGroupState clone() => NewGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NewGroupState copyWith(void Function(NewGroupState) updates) => super.copyWith((message) => updates(message as NewGroupState)) as NewGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NewGroupState create() => NewGroupState._(); + NewGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NewGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static NewGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get groupId => $_getSZ(0); + @$pb.TagNumber(1) + set groupId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasGroupId() => $_has(0); + @$pb.TagNumber(1) + void clearGroupId() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get versionId => $_getI64(1); + @$pb.TagNumber(2) + set versionId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasVersionId() => $_has(1); + @$pb.TagNumber(2) + void clearVersionId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedGroupState => $_getN(2); + @$pb.TagNumber(3) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedGroupState() => $_has(2); + @$pb.TagNumber(3) + void clearEncryptedGroupState() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get publicKey => $_getN(3); + @$pb.TagNumber(4) + set publicKey($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasPublicKey() => $_has(3); + @$pb.TagNumber(4) + void clearPublicKey() => clearField(4); +} + +class AppendGroupState_AppendTBS extends $pb.GeneratedMessage { + factory AppendGroupState_AppendTBS({ + $core.List<$core.int>? encryptedGroupStateAppend, + $core.List<$core.int>? publicKey, + $core.String? groupId, + $core.List<$core.int>? nonce, + }) { + final $result = create(); + if (encryptedGroupStateAppend != null) { + $result.encryptedGroupStateAppend = encryptedGroupStateAppend; + } + if (publicKey != null) { + $result.publicKey = publicKey; + } + if (groupId != null) { + $result.groupId = groupId; + } + if (nonce != null) { + $result.nonce = nonce; + } + return $result; + } + AppendGroupState_AppendTBS._() : super(); + factory AppendGroupState_AppendTBS.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupState_AppendTBS.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupState.AppendTBS', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'encryptedGroupStateAppend', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'groupId') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupState_AppendTBS clone() => AppendGroupState_AppendTBS()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupState_AppendTBS copyWith(void Function(AppendGroupState_AppendTBS) updates) => super.copyWith((message) => updates(message as AppendGroupState_AppendTBS)) as AppendGroupState_AppendTBS; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupState_AppendTBS create() => AppendGroupState_AppendTBS._(); + AppendGroupState_AppendTBS createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupState_AppendTBS getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupState_AppendTBS? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get encryptedGroupStateAppend => $_getN(0); + @$pb.TagNumber(1) + set encryptedGroupStateAppend($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasEncryptedGroupStateAppend() => $_has(0); + @$pb.TagNumber(1) + void clearEncryptedGroupStateAppend() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get publicKey => $_getN(1); + @$pb.TagNumber(2) + set publicKey($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasPublicKey() => $_has(1); + @$pb.TagNumber(2) + void clearPublicKey() => clearField(2); + + @$pb.TagNumber(3) + $core.String get groupId => $_getSZ(2); + @$pb.TagNumber(3) + set groupId($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasGroupId() => $_has(2); + @$pb.TagNumber(3) + void clearGroupId() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get nonce => $_getN(3); + @$pb.TagNumber(4) + set nonce($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasNonce() => $_has(3); + @$pb.TagNumber(4) + void clearNonce() => clearField(4); +} + +class AppendGroupState extends $pb.GeneratedMessage { + factory AppendGroupState({ + $core.List<$core.int>? signature, + AppendGroupState_AppendTBS? appendTBS, + $fixnum.Int64? versionId, + }) { + final $result = create(); + if (signature != null) { + $result.signature = signature; + } + if (appendTBS != null) { + $result.appendTBS = appendTBS; + } + if (versionId != null) { + $result.versionId = versionId; + } + return $result; + } + AppendGroupState._() : super(); + factory AppendGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY) + ..aOM(2, _omitFieldNames ? '' : 'appendTBS', protoName: 'appendTBS', subBuilder: AppendGroupState_AppendTBS.create) + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, protoName: 'versionId', defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupState clone() => AppendGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupState copyWith(void Function(AppendGroupState) updates) => super.copyWith((message) => updates(message as AppendGroupState)) as AppendGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupState create() => AppendGroupState._(); + AppendGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get signature => $_getN(0); + @$pb.TagNumber(1) + set signature($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasSignature() => $_has(0); + @$pb.TagNumber(1) + void clearSignature() => clearField(1); + + @$pb.TagNumber(2) + AppendGroupState_AppendTBS get appendTBS => $_getN(1); + @$pb.TagNumber(2) + set appendTBS(AppendGroupState_AppendTBS v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasAppendTBS() => $_has(1); + @$pb.TagNumber(2) + void clearAppendTBS() => clearField(2); + @$pb.TagNumber(2) + AppendGroupState_AppendTBS ensureAppendTBS() => $_ensure(1); + + @$pb.TagNumber(3) + $fixnum.Int64 get versionId => $_getI64(2); + @$pb.TagNumber(3) + set versionId($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasVersionId() => $_has(2); + @$pb.TagNumber(3) + void clearVersionId() => clearField(3); +} + +class GroupState extends $pb.GeneratedMessage { + factory GroupState({ + $fixnum.Int64? versionId, + $core.List<$core.int>? encryptedGroupState, + $core.Iterable? appendedGroupStates, + }) { + final $result = create(); + if (versionId != null) { + $result.versionId = versionId; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (appendedGroupStates != null) { + $result.appendedGroupStates.addAll(appendedGroupStates); + } + return $result; + } + GroupState._() : super(); + factory GroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GroupState', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'versionId', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY) + ..pc(3, _omitFieldNames ? '' : 'appendedGroupStates', $pb.PbFieldType.PM, subBuilder: AppendGroupState.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GroupState clone() => GroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GroupState copyWith(void Function(GroupState) updates) => super.copyWith((message) => updates(message as GroupState)) as GroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GroupState create() => GroupState._(); + GroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GroupState? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get versionId => $_getI64(0); + @$pb.TagNumber(1) + set versionId($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasVersionId() => $_has(0); + @$pb.TagNumber(1) + void clearVersionId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(2) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedGroupState() => clearField(2); + + @$pb.TagNumber(3) + $core.List get appendedGroupStates => $_getList(2); +} + +/// this is just a database helper to store multiple appends +class AppendGroupStateHelper extends $pb.GeneratedMessage { + factory AppendGroupStateHelper({ + $core.Iterable? appendedGroupStates, + }) { + final $result = create(); + if (appendedGroupStates != null) { + $result.appendedGroupStates.addAll(appendedGroupStates); + } + return $result; + } + AppendGroupStateHelper._() : super(); + factory AppendGroupStateHelper.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AppendGroupStateHelper.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AppendGroupStateHelper', package: const $pb.PackageName(_omitMessageNames ? '' : 'http_requests'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'appendedGroupStates', $pb.PbFieldType.PM, subBuilder: AppendGroupState.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AppendGroupStateHelper clone() => AppendGroupStateHelper()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AppendGroupStateHelper copyWith(void Function(AppendGroupStateHelper) updates) => super.copyWith((message) => updates(message as AppendGroupStateHelper)) as AppendGroupStateHelper; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AppendGroupStateHelper create() => AppendGroupStateHelper._(); + AppendGroupStateHelper createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AppendGroupStateHelper getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AppendGroupStateHelper? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get appendedGroupStates => $_getList(0); +} + const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart index d4c43f8..c93b93c 100644 --- a/lib/src/model/protobuf/api/http/http_requests.pbjson.dart +++ b/lib/src/model/protobuf/api/http/http_requests.pbjson.dart @@ -48,3 +48,118 @@ final $typed_data.Uint8List uploadRequestDescriptor = $convert.base64Decode( 'c3VjY2VzcxgDIAMoCzIaLmh0dHBfcmVxdWVzdHMuVGV4dE1lc3NhZ2VSEW1lc3NhZ2VzT25TdW' 'NjZXNz'); +@$core.Deprecated('Use updateGroupStateDescriptor instead') +const UpdateGroupState$json = { + '1': 'UpdateGroupState', + '2': [ + {'1': 'update', '3': 1, '4': 1, '5': 11, '6': '.http_requests.UpdateGroupState.UpdateTBS', '10': 'update'}, + {'1': 'signature', '3': 2, '4': 1, '5': 12, '10': 'signature'}, + ], + '3': [UpdateGroupState_UpdateTBS$json], +}; + +@$core.Deprecated('Use updateGroupStateDescriptor instead') +const UpdateGroupState_UpdateTBS$json = { + '1': 'UpdateTBS', + '2': [ + {'1': 'version_id', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'public_key', '3': 4, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'remove_admin', '3': 5, '4': 1, '5': 12, '9': 0, '10': 'removeAdmin', '17': true}, + {'1': 'add_admin', '3': 6, '4': 1, '5': 12, '9': 1, '10': 'addAdmin', '17': true}, + {'1': 'nonce', '3': 7, '4': 1, '5': 12, '10': 'nonce'}, + ], + '8': [ + {'1': '_remove_admin'}, + {'1': '_add_admin'}, + ], +}; + +/// Descriptor for `UpdateGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List updateGroupStateDescriptor = $convert.base64Decode( + 'ChBVcGRhdGVHcm91cFN0YXRlEkEKBnVwZGF0ZRgBIAEoCzIpLmh0dHBfcmVxdWVzdHMuVXBkYX' + 'RlR3JvdXBTdGF0ZS5VcGRhdGVUQlNSBnVwZGF0ZRIcCglzaWduYXR1cmUYAiABKAxSCXNpZ25h' + 'dHVyZRr8AQoJVXBkYXRlVEJTEh0KCnZlcnNpb25faWQYASABKARSCXZlcnNpb25JZBIyChVlbm' + 'NyeXB0ZWRfZ3JvdXBfc3RhdGUYAyABKAxSE2VuY3J5cHRlZEdyb3VwU3RhdGUSHQoKcHVibGlj' + 'X2tleRgEIAEoDFIJcHVibGljS2V5EiYKDHJlbW92ZV9hZG1pbhgFIAEoDEgAUgtyZW1vdmVBZG' + '1pbogBARIgCglhZGRfYWRtaW4YBiABKAxIAVIIYWRkQWRtaW6IAQESFAoFbm9uY2UYByABKAxS' + 'BW5vbmNlQg8KDV9yZW1vdmVfYWRtaW5CDAoKX2FkZF9hZG1pbg=='); + +@$core.Deprecated('Use newGroupStateDescriptor instead') +const NewGroupState$json = { + '1': 'NewGroupState', + '2': [ + {'1': 'group_id', '3': 1, '4': 1, '5': 9, '10': 'groupId'}, + {'1': 'version_id', '3': 2, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 3, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'public_key', '3': 4, '4': 1, '5': 12, '10': 'publicKey'}, + ], +}; + +/// Descriptor for `NewGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List newGroupStateDescriptor = $convert.base64Decode( + 'Cg1OZXdHcm91cFN0YXRlEhkKCGdyb3VwX2lkGAEgASgJUgdncm91cElkEh0KCnZlcnNpb25faW' + 'QYAiABKARSCXZlcnNpb25JZBIyChVlbmNyeXB0ZWRfZ3JvdXBfc3RhdGUYAyABKAxSE2VuY3J5' + 'cHRlZEdyb3VwU3RhdGUSHQoKcHVibGljX2tleRgEIAEoDFIJcHVibGljS2V5'); + +@$core.Deprecated('Use appendGroupStateDescriptor instead') +const AppendGroupState$json = { + '1': 'AppendGroupState', + '2': [ + {'1': 'signature', '3': 1, '4': 1, '5': 12, '10': 'signature'}, + {'1': 'appendTBS', '3': 2, '4': 1, '5': 11, '6': '.http_requests.AppendGroupState.AppendTBS', '10': 'appendTBS'}, + {'1': 'versionId', '3': 3, '4': 1, '5': 4, '10': 'versionId'}, + ], + '3': [AppendGroupState_AppendTBS$json], +}; + +@$core.Deprecated('Use appendGroupStateDescriptor instead') +const AppendGroupState_AppendTBS$json = { + '1': 'AppendTBS', + '2': [ + {'1': 'encrypted_group_state_append', '3': 1, '4': 1, '5': 12, '10': 'encryptedGroupStateAppend'}, + {'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'}, + {'1': 'group_id', '3': 3, '4': 1, '5': 9, '10': 'groupId'}, + {'1': 'nonce', '3': 4, '4': 1, '5': 12, '10': 'nonce'}, + ], +}; + +/// Descriptor for `AppendGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List appendGroupStateDescriptor = $convert.base64Decode( + 'ChBBcHBlbmRHcm91cFN0YXRlEhwKCXNpZ25hdHVyZRgBIAEoDFIJc2lnbmF0dXJlEkcKCWFwcG' + 'VuZFRCUxgCIAEoCzIpLmh0dHBfcmVxdWVzdHMuQXBwZW5kR3JvdXBTdGF0ZS5BcHBlbmRUQlNS' + 'CWFwcGVuZFRCUxIcCgl2ZXJzaW9uSWQYAyABKARSCXZlcnNpb25JZBqcAQoJQXBwZW5kVEJTEj' + '8KHGVuY3J5cHRlZF9ncm91cF9zdGF0ZV9hcHBlbmQYASABKAxSGWVuY3J5cHRlZEdyb3VwU3Rh' + 'dGVBcHBlbmQSHQoKcHVibGljX2tleRgCIAEoDFIJcHVibGljS2V5EhkKCGdyb3VwX2lkGAMgAS' + 'gJUgdncm91cElkEhQKBW5vbmNlGAQgASgMUgVub25jZQ=='); + +@$core.Deprecated('Use groupStateDescriptor instead') +const GroupState$json = { + '1': 'GroupState', + '2': [ + {'1': 'version_id', '3': 1, '4': 1, '5': 4, '10': 'versionId'}, + {'1': 'encrypted_group_state', '3': 2, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'appended_group_states', '3': 3, '4': 3, '5': 11, '6': '.http_requests.AppendGroupState', '10': 'appendedGroupStates'}, + ], +}; + +/// Descriptor for `GroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List groupStateDescriptor = $convert.base64Decode( + 'CgpHcm91cFN0YXRlEh0KCnZlcnNpb25faWQYASABKARSCXZlcnNpb25JZBIyChVlbmNyeXB0ZW' + 'RfZ3JvdXBfc3RhdGUYAiABKAxSE2VuY3J5cHRlZEdyb3VwU3RhdGUSUwoVYXBwZW5kZWRfZ3Jv' + 'dXBfc3RhdGVzGAMgAygLMh8uaHR0cF9yZXF1ZXN0cy5BcHBlbmRHcm91cFN0YXRlUhNhcHBlbm' + 'RlZEdyb3VwU3RhdGVz'); + +@$core.Deprecated('Use appendGroupStateHelperDescriptor instead') +const AppendGroupStateHelper$json = { + '1': 'AppendGroupStateHelper', + '2': [ + {'1': 'appended_group_states', '3': 1, '4': 3, '5': 11, '6': '.http_requests.AppendGroupState', '10': 'appendedGroupStates'}, + ], +}; + +/// Descriptor for `AppendGroupStateHelper`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List appendGroupStateHelperDescriptor = $convert.base64Decode( + 'ChZBcHBlbmRHcm91cFN0YXRlSGVscGVyElMKFWFwcGVuZGVkX2dyb3VwX3N0YXRlcxgBIAMoCz' + 'IfLmh0dHBfcmVxdWVzdHMuQXBwZW5kR3JvdXBTdGF0ZVITYXBwZW5kZWRHcm91cFN0YXRlcw=='); + diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 5d4ff99..db2849b 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -196,6 +196,38 @@ class V0 extends $pb.GeneratedMessage { Response ensureResponse() => $_ensure(3); } +class Handshake_RequestPOW extends $pb.GeneratedMessage { + factory Handshake_RequestPOW() => create(); + Handshake_RequestPOW._() : super(); + factory Handshake_RequestPOW.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Handshake_RequestPOW.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Handshake.RequestPOW', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Handshake_RequestPOW clone() => Handshake_RequestPOW()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Handshake_RequestPOW copyWith(void Function(Handshake_RequestPOW) updates) => super.copyWith((message) => updates(message as Handshake_RequestPOW)) as Handshake_RequestPOW; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_RequestPOW create() => Handshake_RequestPOW._(); + Handshake_RequestPOW createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Handshake_RequestPOW getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Handshake_RequestPOW? _defaultInstance; +} + class Handshake_Register extends $pb.GeneratedMessage { factory Handshake_Register({ $core.String? username, @@ -206,6 +238,8 @@ class Handshake_Register extends $pb.GeneratedMessage { $fixnum.Int64? signedPrekeyId, $fixnum.Int64? registrationId, $core.bool? isIos, + $core.String? langCode, + $fixnum.Int64? proofOfWork, }) { final $result = create(); if (username != null) { @@ -232,6 +266,12 @@ class Handshake_Register extends $pb.GeneratedMessage { if (isIos != null) { $result.isIos = isIos; } + if (langCode != null) { + $result.langCode = langCode; + } + if (proofOfWork != null) { + $result.proofOfWork = proofOfWork; + } return $result; } Handshake_Register._() : super(); @@ -247,6 +287,8 @@ class Handshake_Register extends $pb.GeneratedMessage { ..aInt64(6, _omitFieldNames ? '' : 'signedPrekeyId') ..aInt64(7, _omitFieldNames ? '' : 'registrationId') ..aOB(8, _omitFieldNames ? '' : 'isIos') + ..aOS(9, _omitFieldNames ? '' : 'langCode') + ..aInt64(10, _omitFieldNames ? '' : 'proofOfWork') ..hasRequiredFields = false ; @@ -342,6 +384,24 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool hasIsIos() => $_has(7); @$pb.TagNumber(8) void clearIsIos() => clearField(8); + + @$pb.TagNumber(9) + $core.String get langCode => $_getSZ(8); + @$pb.TagNumber(9) + set langCode($core.String v) { $_setString(8, v); } + @$pb.TagNumber(9) + $core.bool hasLangCode() => $_has(8); + @$pb.TagNumber(9) + void clearLangCode() => clearField(9); + + @$pb.TagNumber(10) + $fixnum.Int64 get proofOfWork => $_getI64(9); + @$pb.TagNumber(10) + set proofOfWork($fixnum.Int64 v) { $_setInt64(9, v); } + @$pb.TagNumber(10) + $core.bool hasProofOfWork() => $_has(9); + @$pb.TagNumber(10) + void clearProofOfWork() => clearField(10); } class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { @@ -534,32 +594,37 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { enum Handshake_Handshake { register, - getauthchallenge, - getauthtoken, + getAuthChallenge, + getAuthToken, authenticate, + requestPOW, notSet } class Handshake extends $pb.GeneratedMessage { factory Handshake({ Handshake_Register? register, - Handshake_GetAuthChallenge? getauthchallenge, - Handshake_GetAuthToken? getauthtoken, + Handshake_GetAuthChallenge? getAuthChallenge, + Handshake_GetAuthToken? getAuthToken, Handshake_Authenticate? authenticate, + Handshake_RequestPOW? requestPOW, }) { final $result = create(); if (register != null) { $result.register = register; } - if (getauthchallenge != null) { - $result.getauthchallenge = getauthchallenge; + if (getAuthChallenge != null) { + $result.getAuthChallenge = getAuthChallenge; } - if (getauthtoken != null) { - $result.getauthtoken = getauthtoken; + if (getAuthToken != null) { + $result.getAuthToken = getAuthToken; } if (authenticate != null) { $result.authenticate = authenticate; } + if (requestPOW != null) { + $result.requestPOW = requestPOW; + } return $result; } Handshake._() : super(); @@ -568,17 +633,19 @@ class Handshake extends $pb.GeneratedMessage { static const $core.Map<$core.int, Handshake_Handshake> _Handshake_HandshakeByTag = { 1 : Handshake_Handshake.register, - 2 : Handshake_Handshake.getauthchallenge, - 3 : Handshake_Handshake.getauthtoken, + 2 : Handshake_Handshake.getAuthChallenge, + 3 : Handshake_Handshake.getAuthToken, 4 : Handshake_Handshake.authenticate, + 5 : Handshake_Handshake.requestPOW, 0 : Handshake_Handshake.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Handshake', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4]) + ..oo(0, [1, 2, 3, 4, 5]) ..aOM(1, _omitFieldNames ? '' : 'register', subBuilder: Handshake_Register.create) - ..aOM(2, _omitFieldNames ? '' : 'getauthchallenge', subBuilder: Handshake_GetAuthChallenge.create) - ..aOM(3, _omitFieldNames ? '' : 'getauthtoken', subBuilder: Handshake_GetAuthToken.create) + ..aOM(2, _omitFieldNames ? '' : 'getAuthChallenge', protoName: 'getAuthChallenge', subBuilder: Handshake_GetAuthChallenge.create) + ..aOM(3, _omitFieldNames ? '' : 'getAuthToken', protoName: 'getAuthToken', subBuilder: Handshake_GetAuthToken.create) ..aOM(4, _omitFieldNames ? '' : 'authenticate', subBuilder: Handshake_Authenticate.create) + ..aOM(5, _omitFieldNames ? '' : 'requestPOW', protoName: 'requestPOW', subBuilder: Handshake_RequestPOW.create) ..hasRequiredFields = false ; @@ -618,26 +685,26 @@ class Handshake extends $pb.GeneratedMessage { Handshake_Register ensureRegister() => $_ensure(0); @$pb.TagNumber(2) - Handshake_GetAuthChallenge get getauthchallenge => $_getN(1); + Handshake_GetAuthChallenge get getAuthChallenge => $_getN(1); @$pb.TagNumber(2) - set getauthchallenge(Handshake_GetAuthChallenge v) { setField(2, v); } + set getAuthChallenge(Handshake_GetAuthChallenge v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasGetauthchallenge() => $_has(1); + $core.bool hasGetAuthChallenge() => $_has(1); @$pb.TagNumber(2) - void clearGetauthchallenge() => clearField(2); + void clearGetAuthChallenge() => clearField(2); @$pb.TagNumber(2) - Handshake_GetAuthChallenge ensureGetauthchallenge() => $_ensure(1); + Handshake_GetAuthChallenge ensureGetAuthChallenge() => $_ensure(1); @$pb.TagNumber(3) - Handshake_GetAuthToken get getauthtoken => $_getN(2); + Handshake_GetAuthToken get getAuthToken => $_getN(2); @$pb.TagNumber(3) - set getauthtoken(Handshake_GetAuthToken v) { setField(3, v); } + set getAuthToken(Handshake_GetAuthToken v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasGetauthtoken() => $_has(2); + $core.bool hasGetAuthToken() => $_has(2); @$pb.TagNumber(3) - void clearGetauthtoken() => clearField(3); + void clearGetAuthToken() => clearField(3); @$pb.TagNumber(3) - Handshake_GetAuthToken ensureGetauthtoken() => $_ensure(2); + Handshake_GetAuthToken ensureGetAuthToken() => $_ensure(2); @$pb.TagNumber(4) Handshake_Authenticate get authenticate => $_getN(3); @@ -649,6 +716,17 @@ class Handshake extends $pb.GeneratedMessage { void clearAuthenticate() => clearField(4); @$pb.TagNumber(4) Handshake_Authenticate ensureAuthenticate() => $_ensure(3); + + @$pb.TagNumber(5) + Handshake_RequestPOW get requestPOW => $_getN(4); + @$pb.TagNumber(5) + set requestPOW(Handshake_RequestPOW v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasRequestPOW() => $_has(4); + @$pb.TagNumber(5) + void clearRequestPOW() => clearField(5); + @$pb.TagNumber(5) + Handshake_RequestPOW ensureRequestPOW() => $_ensure(4); } class ApplicationData_TextMessage extends $pb.GeneratedMessage { @@ -779,6 +857,56 @@ class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { void clearUsername() => clearField(1); } +class ApplicationData_ChangeUsername extends $pb.GeneratedMessage { + factory ApplicationData_ChangeUsername({ + $core.String? username, + }) { + final $result = create(); + if (username != null) { + $result.username = username; + } + return $result; + } + ApplicationData_ChangeUsername._() : super(); + factory ApplicationData_ChangeUsername.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ApplicationData_ChangeUsername.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData.ChangeUsername', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'username') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ApplicationData_ChangeUsername clone() => ApplicationData_ChangeUsername()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ApplicationData_ChangeUsername copyWith(void Function(ApplicationData_ChangeUsername) updates) => super.copyWith((message) => updates(message as ApplicationData_ChangeUsername)) as ApplicationData_ChangeUsername; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_ChangeUsername create() => ApplicationData_ChangeUsername._(); + ApplicationData_ChangeUsername createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ApplicationData_ChangeUsername getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ApplicationData_ChangeUsername? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get username => $_getSZ(0); + @$pb.TagNumber(1) + set username($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUsername() => $_has(0); + @$pb.TagNumber(1) + void clearUsername() => clearField(1); +} + class ApplicationData_UpdateGoogleFcmToken extends $pb.GeneratedMessage { factory ApplicationData_UpdateGoogleFcmToken({ $core.String? googleFcm, @@ -1692,117 +1820,122 @@ class ApplicationData_DeleteAccount extends $pb.GeneratedMessage { } enum ApplicationData_ApplicationData { - textmessage, - getuserbyusername, - getprekeysbyuserid, - getuserbyid, - updategooglefcmtoken, - getlocation, - getcurrentplaninfos, - redeemvoucher, - getavailableplans, - createvoucher, - getvouchers, - switchtopayedplan, - getaddaccountsinvites, - redeemadditionalcode, - removeadditionaluser, - updateplanoptions, - downloaddone, - getsignedprekeybyuserid, - updatesignedprekey, - deleteaccount, - reportuser, + textMessage, + getUserByUsername, + getPrekeysByUserId, + getUserById, + updateGoogleFcmToken, + getLocation, + getCurrentPlanInfos, + redeemVoucher, + getAvailablePlans, + createVoucher, + getVouchers, + switchtoPayedPlan, + getAddaccountsInvites, + redeemAdditionalCode, + removeAdditionalUser, + updatePlanOptions, + downloadDone, + getSignedPrekeyByUserid, + updateSignedPrekey, + deleteAccount, + reportUser, + changeUsername, notSet } class ApplicationData extends $pb.GeneratedMessage { factory ApplicationData({ - ApplicationData_TextMessage? textmessage, - ApplicationData_GetUserByUsername? getuserbyusername, - ApplicationData_GetPrekeysByUserId? getprekeysbyuserid, - ApplicationData_GetUserById? getuserbyid, - ApplicationData_UpdateGoogleFcmToken? updategooglefcmtoken, - ApplicationData_GetLocation? getlocation, - ApplicationData_GetCurrentPlanInfos? getcurrentplaninfos, - ApplicationData_RedeemVoucher? redeemvoucher, - ApplicationData_GetAvailablePlans? getavailableplans, - ApplicationData_CreateVoucher? createvoucher, - ApplicationData_GetVouchers? getvouchers, - ApplicationData_SwitchToPayedPlan? switchtopayedplan, - ApplicationData_GetAddAccountsInvites? getaddaccountsinvites, - ApplicationData_RedeemAdditionalCode? redeemadditionalcode, - ApplicationData_RemoveAdditionalUser? removeadditionaluser, - ApplicationData_UpdatePlanOptions? updateplanoptions, - ApplicationData_DownloadDone? downloaddone, - ApplicationData_GetSignedPreKeyByUserId? getsignedprekeybyuserid, - ApplicationData_UpdateSignedPreKey? updatesignedprekey, - ApplicationData_DeleteAccount? deleteaccount, - ApplicationData_ReportUser? reportuser, + ApplicationData_TextMessage? textMessage, + ApplicationData_GetUserByUsername? getUserByUsername, + ApplicationData_GetPrekeysByUserId? getPrekeysByUserId, + ApplicationData_GetUserById? getUserById, + ApplicationData_UpdateGoogleFcmToken? updateGoogleFcmToken, + ApplicationData_GetLocation? getLocation, + ApplicationData_GetCurrentPlanInfos? getCurrentPlanInfos, + ApplicationData_RedeemVoucher? redeemVoucher, + ApplicationData_GetAvailablePlans? getAvailablePlans, + ApplicationData_CreateVoucher? createVoucher, + ApplicationData_GetVouchers? getVouchers, + ApplicationData_SwitchToPayedPlan? switchtoPayedPlan, + ApplicationData_GetAddAccountsInvites? getAddaccountsInvites, + ApplicationData_RedeemAdditionalCode? redeemAdditionalCode, + ApplicationData_RemoveAdditionalUser? removeAdditionalUser, + ApplicationData_UpdatePlanOptions? updatePlanOptions, + ApplicationData_DownloadDone? downloadDone, + ApplicationData_GetSignedPreKeyByUserId? getSignedPrekeyByUserid, + ApplicationData_UpdateSignedPreKey? updateSignedPrekey, + ApplicationData_DeleteAccount? deleteAccount, + ApplicationData_ReportUser? reportUser, + ApplicationData_ChangeUsername? changeUsername, }) { final $result = create(); - if (textmessage != null) { - $result.textmessage = textmessage; + if (textMessage != null) { + $result.textMessage = textMessage; } - if (getuserbyusername != null) { - $result.getuserbyusername = getuserbyusername; + if (getUserByUsername != null) { + $result.getUserByUsername = getUserByUsername; } - if (getprekeysbyuserid != null) { - $result.getprekeysbyuserid = getprekeysbyuserid; + if (getPrekeysByUserId != null) { + $result.getPrekeysByUserId = getPrekeysByUserId; } - if (getuserbyid != null) { - $result.getuserbyid = getuserbyid; + if (getUserById != null) { + $result.getUserById = getUserById; } - if (updategooglefcmtoken != null) { - $result.updategooglefcmtoken = updategooglefcmtoken; + if (updateGoogleFcmToken != null) { + $result.updateGoogleFcmToken = updateGoogleFcmToken; } - if (getlocation != null) { - $result.getlocation = getlocation; + if (getLocation != null) { + $result.getLocation = getLocation; } - if (getcurrentplaninfos != null) { - $result.getcurrentplaninfos = getcurrentplaninfos; + if (getCurrentPlanInfos != null) { + $result.getCurrentPlanInfos = getCurrentPlanInfos; } - if (redeemvoucher != null) { - $result.redeemvoucher = redeemvoucher; + if (redeemVoucher != null) { + $result.redeemVoucher = redeemVoucher; } - if (getavailableplans != null) { - $result.getavailableplans = getavailableplans; + if (getAvailablePlans != null) { + $result.getAvailablePlans = getAvailablePlans; } - if (createvoucher != null) { - $result.createvoucher = createvoucher; + if (createVoucher != null) { + $result.createVoucher = createVoucher; } - if (getvouchers != null) { - $result.getvouchers = getvouchers; + if (getVouchers != null) { + $result.getVouchers = getVouchers; } - if (switchtopayedplan != null) { - $result.switchtopayedplan = switchtopayedplan; + if (switchtoPayedPlan != null) { + $result.switchtoPayedPlan = switchtoPayedPlan; } - if (getaddaccountsinvites != null) { - $result.getaddaccountsinvites = getaddaccountsinvites; + if (getAddaccountsInvites != null) { + $result.getAddaccountsInvites = getAddaccountsInvites; } - if (redeemadditionalcode != null) { - $result.redeemadditionalcode = redeemadditionalcode; + if (redeemAdditionalCode != null) { + $result.redeemAdditionalCode = redeemAdditionalCode; } - if (removeadditionaluser != null) { - $result.removeadditionaluser = removeadditionaluser; + if (removeAdditionalUser != null) { + $result.removeAdditionalUser = removeAdditionalUser; } - if (updateplanoptions != null) { - $result.updateplanoptions = updateplanoptions; + if (updatePlanOptions != null) { + $result.updatePlanOptions = updatePlanOptions; } - if (downloaddone != null) { - $result.downloaddone = downloaddone; + if (downloadDone != null) { + $result.downloadDone = downloadDone; } - if (getsignedprekeybyuserid != null) { - $result.getsignedprekeybyuserid = getsignedprekeybyuserid; + if (getSignedPrekeyByUserid != null) { + $result.getSignedPrekeyByUserid = getSignedPrekeyByUserid; } - if (updatesignedprekey != null) { - $result.updatesignedprekey = updatesignedprekey; + if (updateSignedPrekey != null) { + $result.updateSignedPrekey = updateSignedPrekey; } - if (deleteaccount != null) { - $result.deleteaccount = deleteaccount; + if (deleteAccount != null) { + $result.deleteAccount = deleteAccount; } - if (reportuser != null) { - $result.reportuser = reportuser; + if (reportUser != null) { + $result.reportUser = reportUser; + } + if (changeUsername != null) { + $result.changeUsername = changeUsername; } return $result; } @@ -1811,52 +1944,54 @@ class ApplicationData extends $pb.GeneratedMessage { factory ApplicationData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static const $core.Map<$core.int, ApplicationData_ApplicationData> _ApplicationData_ApplicationDataByTag = { - 1 : ApplicationData_ApplicationData.textmessage, - 2 : ApplicationData_ApplicationData.getuserbyusername, - 3 : ApplicationData_ApplicationData.getprekeysbyuserid, - 6 : ApplicationData_ApplicationData.getuserbyid, - 8 : ApplicationData_ApplicationData.updategooglefcmtoken, - 9 : ApplicationData_ApplicationData.getlocation, - 10 : ApplicationData_ApplicationData.getcurrentplaninfos, - 11 : ApplicationData_ApplicationData.redeemvoucher, - 12 : ApplicationData_ApplicationData.getavailableplans, - 13 : ApplicationData_ApplicationData.createvoucher, - 14 : ApplicationData_ApplicationData.getvouchers, - 15 : ApplicationData_ApplicationData.switchtopayedplan, - 16 : ApplicationData_ApplicationData.getaddaccountsinvites, - 17 : ApplicationData_ApplicationData.redeemadditionalcode, - 18 : ApplicationData_ApplicationData.removeadditionaluser, - 19 : ApplicationData_ApplicationData.updateplanoptions, - 20 : ApplicationData_ApplicationData.downloaddone, - 22 : ApplicationData_ApplicationData.getsignedprekeybyuserid, - 23 : ApplicationData_ApplicationData.updatesignedprekey, - 24 : ApplicationData_ApplicationData.deleteaccount, - 25 : ApplicationData_ApplicationData.reportuser, + 1 : ApplicationData_ApplicationData.textMessage, + 2 : ApplicationData_ApplicationData.getUserByUsername, + 3 : ApplicationData_ApplicationData.getPrekeysByUserId, + 6 : ApplicationData_ApplicationData.getUserById, + 8 : ApplicationData_ApplicationData.updateGoogleFcmToken, + 9 : ApplicationData_ApplicationData.getLocation, + 10 : ApplicationData_ApplicationData.getCurrentPlanInfos, + 11 : ApplicationData_ApplicationData.redeemVoucher, + 12 : ApplicationData_ApplicationData.getAvailablePlans, + 13 : ApplicationData_ApplicationData.createVoucher, + 14 : ApplicationData_ApplicationData.getVouchers, + 15 : ApplicationData_ApplicationData.switchtoPayedPlan, + 16 : ApplicationData_ApplicationData.getAddaccountsInvites, + 17 : ApplicationData_ApplicationData.redeemAdditionalCode, + 18 : ApplicationData_ApplicationData.removeAdditionalUser, + 19 : ApplicationData_ApplicationData.updatePlanOptions, + 20 : ApplicationData_ApplicationData.downloadDone, + 22 : ApplicationData_ApplicationData.getSignedPrekeyByUserid, + 23 : ApplicationData_ApplicationData.updateSignedPrekey, + 24 : ApplicationData_ApplicationData.deleteAccount, + 25 : ApplicationData_ApplicationData.reportUser, + 26 : ApplicationData_ApplicationData.changeUsername, 0 : ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ApplicationData', package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25]) - ..aOM(1, _omitFieldNames ? '' : 'textmessage', subBuilder: ApplicationData_TextMessage.create) - ..aOM(2, _omitFieldNames ? '' : 'getuserbyusername', subBuilder: ApplicationData_GetUserByUsername.create) - ..aOM(3, _omitFieldNames ? '' : 'getprekeysbyuserid', subBuilder: ApplicationData_GetPrekeysByUserId.create) - ..aOM(6, _omitFieldNames ? '' : 'getuserbyid', subBuilder: ApplicationData_GetUserById.create) - ..aOM(8, _omitFieldNames ? '' : 'updategooglefcmtoken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) - ..aOM(9, _omitFieldNames ? '' : 'getlocation', subBuilder: ApplicationData_GetLocation.create) - ..aOM(10, _omitFieldNames ? '' : 'getcurrentplaninfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) - ..aOM(11, _omitFieldNames ? '' : 'redeemvoucher', subBuilder: ApplicationData_RedeemVoucher.create) - ..aOM(12, _omitFieldNames ? '' : 'getavailableplans', subBuilder: ApplicationData_GetAvailablePlans.create) - ..aOM(13, _omitFieldNames ? '' : 'createvoucher', subBuilder: ApplicationData_CreateVoucher.create) - ..aOM(14, _omitFieldNames ? '' : 'getvouchers', subBuilder: ApplicationData_GetVouchers.create) - ..aOM(15, _omitFieldNames ? '' : 'Switchtopayedplan', protoName: 'Switchtopayedplan', subBuilder: ApplicationData_SwitchToPayedPlan.create) - ..aOM(16, _omitFieldNames ? '' : 'getaddaccountsinvites', subBuilder: ApplicationData_GetAddAccountsInvites.create) - ..aOM(17, _omitFieldNames ? '' : 'redeemadditionalcode', subBuilder: ApplicationData_RedeemAdditionalCode.create) - ..aOM(18, _omitFieldNames ? '' : 'removeadditionaluser', subBuilder: ApplicationData_RemoveAdditionalUser.create) - ..aOM(19, _omitFieldNames ? '' : 'updateplanoptions', subBuilder: ApplicationData_UpdatePlanOptions.create) - ..aOM(20, _omitFieldNames ? '' : 'downloaddone', subBuilder: ApplicationData_DownloadDone.create) - ..aOM(22, _omitFieldNames ? '' : 'getsignedprekeybyuserid', subBuilder: ApplicationData_GetSignedPreKeyByUserId.create) - ..aOM(23, _omitFieldNames ? '' : 'updatesignedprekey', subBuilder: ApplicationData_UpdateSignedPreKey.create) - ..aOM(24, _omitFieldNames ? '' : 'deleteaccount', subBuilder: ApplicationData_DeleteAccount.create) - ..aOM(25, _omitFieldNames ? '' : 'reportuser', subBuilder: ApplicationData_ReportUser.create) + ..oo(0, [1, 2, 3, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26]) + ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: ApplicationData_TextMessage.create) + ..aOM(2, _omitFieldNames ? '' : 'getUserByUsername', protoName: 'getUserByUsername', subBuilder: ApplicationData_GetUserByUsername.create) + ..aOM(3, _omitFieldNames ? '' : 'getPrekeysByUserId', protoName: 'getPrekeysByUserId', subBuilder: ApplicationData_GetPrekeysByUserId.create) + ..aOM(6, _omitFieldNames ? '' : 'getUserById', protoName: 'getUserById', subBuilder: ApplicationData_GetUserById.create) + ..aOM(8, _omitFieldNames ? '' : 'updateGoogleFcmToken', protoName: 'updateGoogleFcmToken', subBuilder: ApplicationData_UpdateGoogleFcmToken.create) + ..aOM(9, _omitFieldNames ? '' : 'getLocation', protoName: 'getLocation', subBuilder: ApplicationData_GetLocation.create) + ..aOM(10, _omitFieldNames ? '' : 'getCurrentPlanInfos', protoName: 'getCurrentPlanInfos', subBuilder: ApplicationData_GetCurrentPlanInfos.create) + ..aOM(11, _omitFieldNames ? '' : 'redeemVoucher', protoName: 'redeemVoucher', subBuilder: ApplicationData_RedeemVoucher.create) + ..aOM(12, _omitFieldNames ? '' : 'getAvailablePlans', protoName: 'getAvailablePlans', subBuilder: ApplicationData_GetAvailablePlans.create) + ..aOM(13, _omitFieldNames ? '' : 'createVoucher', protoName: 'createVoucher', subBuilder: ApplicationData_CreateVoucher.create) + ..aOM(14, _omitFieldNames ? '' : 'getVouchers', protoName: 'getVouchers', subBuilder: ApplicationData_GetVouchers.create) + ..aOM(15, _omitFieldNames ? '' : 'switchtoPayedPlan', protoName: 'switchtoPayedPlan', subBuilder: ApplicationData_SwitchToPayedPlan.create) + ..aOM(16, _omitFieldNames ? '' : 'getAddaccountsInvites', protoName: 'getAddaccountsInvites', subBuilder: ApplicationData_GetAddAccountsInvites.create) + ..aOM(17, _omitFieldNames ? '' : 'redeemAdditionalCode', protoName: 'redeemAdditionalCode', subBuilder: ApplicationData_RedeemAdditionalCode.create) + ..aOM(18, _omitFieldNames ? '' : 'removeAdditionalUser', protoName: 'removeAdditionalUser', subBuilder: ApplicationData_RemoveAdditionalUser.create) + ..aOM(19, _omitFieldNames ? '' : 'updatePlanOptions', protoName: 'updatePlanOptions', subBuilder: ApplicationData_UpdatePlanOptions.create) + ..aOM(20, _omitFieldNames ? '' : 'downloadDone', protoName: 'downloadDone', subBuilder: ApplicationData_DownloadDone.create) + ..aOM(22, _omitFieldNames ? '' : 'getSignedPrekeyByUserid', protoName: 'getSignedPrekeyByUserid', subBuilder: ApplicationData_GetSignedPreKeyByUserId.create) + ..aOM(23, _omitFieldNames ? '' : 'updateSignedPrekey', protoName: 'updateSignedPrekey', subBuilder: ApplicationData_UpdateSignedPreKey.create) + ..aOM(24, _omitFieldNames ? '' : 'deleteAccount', protoName: 'deleteAccount', subBuilder: ApplicationData_DeleteAccount.create) + ..aOM(25, _omitFieldNames ? '' : 'reportUser', protoName: 'reportUser', subBuilder: ApplicationData_ReportUser.create) + ..aOM(26, _omitFieldNames ? '' : 'changeUsername', protoName: 'changeUsername', subBuilder: ApplicationData_ChangeUsername.create) ..hasRequiredFields = false ; @@ -1885,235 +2020,246 @@ class ApplicationData extends $pb.GeneratedMessage { void clearApplicationData() => clearField($_whichOneof(0)); @$pb.TagNumber(1) - ApplicationData_TextMessage get textmessage => $_getN(0); + ApplicationData_TextMessage get textMessage => $_getN(0); @$pb.TagNumber(1) - set textmessage(ApplicationData_TextMessage v) { setField(1, v); } + set textMessage(ApplicationData_TextMessage v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasTextmessage() => $_has(0); + $core.bool hasTextMessage() => $_has(0); @$pb.TagNumber(1) - void clearTextmessage() => clearField(1); + void clearTextMessage() => clearField(1); @$pb.TagNumber(1) - ApplicationData_TextMessage ensureTextmessage() => $_ensure(0); + ApplicationData_TextMessage ensureTextMessage() => $_ensure(0); @$pb.TagNumber(2) - ApplicationData_GetUserByUsername get getuserbyusername => $_getN(1); + ApplicationData_GetUserByUsername get getUserByUsername => $_getN(1); @$pb.TagNumber(2) - set getuserbyusername(ApplicationData_GetUserByUsername v) { setField(2, v); } + set getUserByUsername(ApplicationData_GetUserByUsername v) { setField(2, v); } @$pb.TagNumber(2) - $core.bool hasGetuserbyusername() => $_has(1); + $core.bool hasGetUserByUsername() => $_has(1); @$pb.TagNumber(2) - void clearGetuserbyusername() => clearField(2); + void clearGetUserByUsername() => clearField(2); @$pb.TagNumber(2) - ApplicationData_GetUserByUsername ensureGetuserbyusername() => $_ensure(1); + ApplicationData_GetUserByUsername ensureGetUserByUsername() => $_ensure(1); @$pb.TagNumber(3) - ApplicationData_GetPrekeysByUserId get getprekeysbyuserid => $_getN(2); + ApplicationData_GetPrekeysByUserId get getPrekeysByUserId => $_getN(2); @$pb.TagNumber(3) - set getprekeysbyuserid(ApplicationData_GetPrekeysByUserId v) { setField(3, v); } + set getPrekeysByUserId(ApplicationData_GetPrekeysByUserId v) { setField(3, v); } @$pb.TagNumber(3) - $core.bool hasGetprekeysbyuserid() => $_has(2); + $core.bool hasGetPrekeysByUserId() => $_has(2); @$pb.TagNumber(3) - void clearGetprekeysbyuserid() => clearField(3); + void clearGetPrekeysByUserId() => clearField(3); @$pb.TagNumber(3) - ApplicationData_GetPrekeysByUserId ensureGetprekeysbyuserid() => $_ensure(2); + ApplicationData_GetPrekeysByUserId ensureGetPrekeysByUserId() => $_ensure(2); @$pb.TagNumber(6) - ApplicationData_GetUserById get getuserbyid => $_getN(3); + ApplicationData_GetUserById get getUserById => $_getN(3); @$pb.TagNumber(6) - set getuserbyid(ApplicationData_GetUserById v) { setField(6, v); } + set getUserById(ApplicationData_GetUserById v) { setField(6, v); } @$pb.TagNumber(6) - $core.bool hasGetuserbyid() => $_has(3); + $core.bool hasGetUserById() => $_has(3); @$pb.TagNumber(6) - void clearGetuserbyid() => clearField(6); + void clearGetUserById() => clearField(6); @$pb.TagNumber(6) - ApplicationData_GetUserById ensureGetuserbyid() => $_ensure(3); + ApplicationData_GetUserById ensureGetUserById() => $_ensure(3); @$pb.TagNumber(8) - ApplicationData_UpdateGoogleFcmToken get updategooglefcmtoken => $_getN(4); + ApplicationData_UpdateGoogleFcmToken get updateGoogleFcmToken => $_getN(4); @$pb.TagNumber(8) - set updategooglefcmtoken(ApplicationData_UpdateGoogleFcmToken v) { setField(8, v); } + set updateGoogleFcmToken(ApplicationData_UpdateGoogleFcmToken v) { setField(8, v); } @$pb.TagNumber(8) - $core.bool hasUpdategooglefcmtoken() => $_has(4); + $core.bool hasUpdateGoogleFcmToken() => $_has(4); @$pb.TagNumber(8) - void clearUpdategooglefcmtoken() => clearField(8); + void clearUpdateGoogleFcmToken() => clearField(8); @$pb.TagNumber(8) - ApplicationData_UpdateGoogleFcmToken ensureUpdategooglefcmtoken() => $_ensure(4); + ApplicationData_UpdateGoogleFcmToken ensureUpdateGoogleFcmToken() => $_ensure(4); @$pb.TagNumber(9) - ApplicationData_GetLocation get getlocation => $_getN(5); + ApplicationData_GetLocation get getLocation => $_getN(5); @$pb.TagNumber(9) - set getlocation(ApplicationData_GetLocation v) { setField(9, v); } + set getLocation(ApplicationData_GetLocation v) { setField(9, v); } @$pb.TagNumber(9) - $core.bool hasGetlocation() => $_has(5); + $core.bool hasGetLocation() => $_has(5); @$pb.TagNumber(9) - void clearGetlocation() => clearField(9); + void clearGetLocation() => clearField(9); @$pb.TagNumber(9) - ApplicationData_GetLocation ensureGetlocation() => $_ensure(5); + ApplicationData_GetLocation ensureGetLocation() => $_ensure(5); @$pb.TagNumber(10) - ApplicationData_GetCurrentPlanInfos get getcurrentplaninfos => $_getN(6); + ApplicationData_GetCurrentPlanInfos get getCurrentPlanInfos => $_getN(6); @$pb.TagNumber(10) - set getcurrentplaninfos(ApplicationData_GetCurrentPlanInfos v) { setField(10, v); } + set getCurrentPlanInfos(ApplicationData_GetCurrentPlanInfos v) { setField(10, v); } @$pb.TagNumber(10) - $core.bool hasGetcurrentplaninfos() => $_has(6); + $core.bool hasGetCurrentPlanInfos() => $_has(6); @$pb.TagNumber(10) - void clearGetcurrentplaninfos() => clearField(10); + void clearGetCurrentPlanInfos() => clearField(10); @$pb.TagNumber(10) - ApplicationData_GetCurrentPlanInfos ensureGetcurrentplaninfos() => $_ensure(6); + ApplicationData_GetCurrentPlanInfos ensureGetCurrentPlanInfos() => $_ensure(6); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher get redeemvoucher => $_getN(7); + ApplicationData_RedeemVoucher get redeemVoucher => $_getN(7); @$pb.TagNumber(11) - set redeemvoucher(ApplicationData_RedeemVoucher v) { setField(11, v); } + set redeemVoucher(ApplicationData_RedeemVoucher v) { setField(11, v); } @$pb.TagNumber(11) - $core.bool hasRedeemvoucher() => $_has(7); + $core.bool hasRedeemVoucher() => $_has(7); @$pb.TagNumber(11) - void clearRedeemvoucher() => clearField(11); + void clearRedeemVoucher() => clearField(11); @$pb.TagNumber(11) - ApplicationData_RedeemVoucher ensureRedeemvoucher() => $_ensure(7); + ApplicationData_RedeemVoucher ensureRedeemVoucher() => $_ensure(7); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans get getavailableplans => $_getN(8); + ApplicationData_GetAvailablePlans get getAvailablePlans => $_getN(8); @$pb.TagNumber(12) - set getavailableplans(ApplicationData_GetAvailablePlans v) { setField(12, v); } + set getAvailablePlans(ApplicationData_GetAvailablePlans v) { setField(12, v); } @$pb.TagNumber(12) - $core.bool hasGetavailableplans() => $_has(8); + $core.bool hasGetAvailablePlans() => $_has(8); @$pb.TagNumber(12) - void clearGetavailableplans() => clearField(12); + void clearGetAvailablePlans() => clearField(12); @$pb.TagNumber(12) - ApplicationData_GetAvailablePlans ensureGetavailableplans() => $_ensure(8); + ApplicationData_GetAvailablePlans ensureGetAvailablePlans() => $_ensure(8); @$pb.TagNumber(13) - ApplicationData_CreateVoucher get createvoucher => $_getN(9); + ApplicationData_CreateVoucher get createVoucher => $_getN(9); @$pb.TagNumber(13) - set createvoucher(ApplicationData_CreateVoucher v) { setField(13, v); } + set createVoucher(ApplicationData_CreateVoucher v) { setField(13, v); } @$pb.TagNumber(13) - $core.bool hasCreatevoucher() => $_has(9); + $core.bool hasCreateVoucher() => $_has(9); @$pb.TagNumber(13) - void clearCreatevoucher() => clearField(13); + void clearCreateVoucher() => clearField(13); @$pb.TagNumber(13) - ApplicationData_CreateVoucher ensureCreatevoucher() => $_ensure(9); + ApplicationData_CreateVoucher ensureCreateVoucher() => $_ensure(9); @$pb.TagNumber(14) - ApplicationData_GetVouchers get getvouchers => $_getN(10); + ApplicationData_GetVouchers get getVouchers => $_getN(10); @$pb.TagNumber(14) - set getvouchers(ApplicationData_GetVouchers v) { setField(14, v); } + set getVouchers(ApplicationData_GetVouchers v) { setField(14, v); } @$pb.TagNumber(14) - $core.bool hasGetvouchers() => $_has(10); + $core.bool hasGetVouchers() => $_has(10); @$pb.TagNumber(14) - void clearGetvouchers() => clearField(14); + void clearGetVouchers() => clearField(14); @$pb.TagNumber(14) - ApplicationData_GetVouchers ensureGetvouchers() => $_ensure(10); + ApplicationData_GetVouchers ensureGetVouchers() => $_ensure(10); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan get switchtopayedplan => $_getN(11); + ApplicationData_SwitchToPayedPlan get switchtoPayedPlan => $_getN(11); @$pb.TagNumber(15) - set switchtopayedplan(ApplicationData_SwitchToPayedPlan v) { setField(15, v); } + set switchtoPayedPlan(ApplicationData_SwitchToPayedPlan v) { setField(15, v); } @$pb.TagNumber(15) - $core.bool hasSwitchtopayedplan() => $_has(11); + $core.bool hasSwitchtoPayedPlan() => $_has(11); @$pb.TagNumber(15) - void clearSwitchtopayedplan() => clearField(15); + void clearSwitchtoPayedPlan() => clearField(15); @$pb.TagNumber(15) - ApplicationData_SwitchToPayedPlan ensureSwitchtopayedplan() => $_ensure(11); + ApplicationData_SwitchToPayedPlan ensureSwitchtoPayedPlan() => $_ensure(11); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites get getaddaccountsinvites => $_getN(12); + ApplicationData_GetAddAccountsInvites get getAddaccountsInvites => $_getN(12); @$pb.TagNumber(16) - set getaddaccountsinvites(ApplicationData_GetAddAccountsInvites v) { setField(16, v); } + set getAddaccountsInvites(ApplicationData_GetAddAccountsInvites v) { setField(16, v); } @$pb.TagNumber(16) - $core.bool hasGetaddaccountsinvites() => $_has(12); + $core.bool hasGetAddaccountsInvites() => $_has(12); @$pb.TagNumber(16) - void clearGetaddaccountsinvites() => clearField(16); + void clearGetAddaccountsInvites() => clearField(16); @$pb.TagNumber(16) - ApplicationData_GetAddAccountsInvites ensureGetaddaccountsinvites() => $_ensure(12); + ApplicationData_GetAddAccountsInvites ensureGetAddaccountsInvites() => $_ensure(12); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode get redeemadditionalcode => $_getN(13); + ApplicationData_RedeemAdditionalCode get redeemAdditionalCode => $_getN(13); @$pb.TagNumber(17) - set redeemadditionalcode(ApplicationData_RedeemAdditionalCode v) { setField(17, v); } + set redeemAdditionalCode(ApplicationData_RedeemAdditionalCode v) { setField(17, v); } @$pb.TagNumber(17) - $core.bool hasRedeemadditionalcode() => $_has(13); + $core.bool hasRedeemAdditionalCode() => $_has(13); @$pb.TagNumber(17) - void clearRedeemadditionalcode() => clearField(17); + void clearRedeemAdditionalCode() => clearField(17); @$pb.TagNumber(17) - ApplicationData_RedeemAdditionalCode ensureRedeemadditionalcode() => $_ensure(13); + ApplicationData_RedeemAdditionalCode ensureRedeemAdditionalCode() => $_ensure(13); @$pb.TagNumber(18) - ApplicationData_RemoveAdditionalUser get removeadditionaluser => $_getN(14); + ApplicationData_RemoveAdditionalUser get removeAdditionalUser => $_getN(14); @$pb.TagNumber(18) - set removeadditionaluser(ApplicationData_RemoveAdditionalUser v) { setField(18, v); } + set removeAdditionalUser(ApplicationData_RemoveAdditionalUser v) { setField(18, v); } @$pb.TagNumber(18) - $core.bool hasRemoveadditionaluser() => $_has(14); + $core.bool hasRemoveAdditionalUser() => $_has(14); @$pb.TagNumber(18) - void clearRemoveadditionaluser() => clearField(18); + void clearRemoveAdditionalUser() => clearField(18); @$pb.TagNumber(18) - ApplicationData_RemoveAdditionalUser ensureRemoveadditionaluser() => $_ensure(14); + ApplicationData_RemoveAdditionalUser ensureRemoveAdditionalUser() => $_ensure(14); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions get updateplanoptions => $_getN(15); + ApplicationData_UpdatePlanOptions get updatePlanOptions => $_getN(15); @$pb.TagNumber(19) - set updateplanoptions(ApplicationData_UpdatePlanOptions v) { setField(19, v); } + set updatePlanOptions(ApplicationData_UpdatePlanOptions v) { setField(19, v); } @$pb.TagNumber(19) - $core.bool hasUpdateplanoptions() => $_has(15); + $core.bool hasUpdatePlanOptions() => $_has(15); @$pb.TagNumber(19) - void clearUpdateplanoptions() => clearField(19); + void clearUpdatePlanOptions() => clearField(19); @$pb.TagNumber(19) - ApplicationData_UpdatePlanOptions ensureUpdateplanoptions() => $_ensure(15); + ApplicationData_UpdatePlanOptions ensureUpdatePlanOptions() => $_ensure(15); @$pb.TagNumber(20) - ApplicationData_DownloadDone get downloaddone => $_getN(16); + ApplicationData_DownloadDone get downloadDone => $_getN(16); @$pb.TagNumber(20) - set downloaddone(ApplicationData_DownloadDone v) { setField(20, v); } + set downloadDone(ApplicationData_DownloadDone v) { setField(20, v); } @$pb.TagNumber(20) - $core.bool hasDownloaddone() => $_has(16); + $core.bool hasDownloadDone() => $_has(16); @$pb.TagNumber(20) - void clearDownloaddone() => clearField(20); + void clearDownloadDone() => clearField(20); @$pb.TagNumber(20) - ApplicationData_DownloadDone ensureDownloaddone() => $_ensure(16); + ApplicationData_DownloadDone ensureDownloadDone() => $_ensure(16); @$pb.TagNumber(22) - ApplicationData_GetSignedPreKeyByUserId get getsignedprekeybyuserid => $_getN(17); + ApplicationData_GetSignedPreKeyByUserId get getSignedPrekeyByUserid => $_getN(17); @$pb.TagNumber(22) - set getsignedprekeybyuserid(ApplicationData_GetSignedPreKeyByUserId v) { setField(22, v); } + set getSignedPrekeyByUserid(ApplicationData_GetSignedPreKeyByUserId v) { setField(22, v); } @$pb.TagNumber(22) - $core.bool hasGetsignedprekeybyuserid() => $_has(17); + $core.bool hasGetSignedPrekeyByUserid() => $_has(17); @$pb.TagNumber(22) - void clearGetsignedprekeybyuserid() => clearField(22); + void clearGetSignedPrekeyByUserid() => clearField(22); @$pb.TagNumber(22) - ApplicationData_GetSignedPreKeyByUserId ensureGetsignedprekeybyuserid() => $_ensure(17); + ApplicationData_GetSignedPreKeyByUserId ensureGetSignedPrekeyByUserid() => $_ensure(17); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey get updatesignedprekey => $_getN(18); + ApplicationData_UpdateSignedPreKey get updateSignedPrekey => $_getN(18); @$pb.TagNumber(23) - set updatesignedprekey(ApplicationData_UpdateSignedPreKey v) { setField(23, v); } + set updateSignedPrekey(ApplicationData_UpdateSignedPreKey v) { setField(23, v); } @$pb.TagNumber(23) - $core.bool hasUpdatesignedprekey() => $_has(18); + $core.bool hasUpdateSignedPrekey() => $_has(18); @$pb.TagNumber(23) - void clearUpdatesignedprekey() => clearField(23); + void clearUpdateSignedPrekey() => clearField(23); @$pb.TagNumber(23) - ApplicationData_UpdateSignedPreKey ensureUpdatesignedprekey() => $_ensure(18); + ApplicationData_UpdateSignedPreKey ensureUpdateSignedPrekey() => $_ensure(18); @$pb.TagNumber(24) - ApplicationData_DeleteAccount get deleteaccount => $_getN(19); + ApplicationData_DeleteAccount get deleteAccount => $_getN(19); @$pb.TagNumber(24) - set deleteaccount(ApplicationData_DeleteAccount v) { setField(24, v); } + set deleteAccount(ApplicationData_DeleteAccount v) { setField(24, v); } @$pb.TagNumber(24) - $core.bool hasDeleteaccount() => $_has(19); + $core.bool hasDeleteAccount() => $_has(19); @$pb.TagNumber(24) - void clearDeleteaccount() => clearField(24); + void clearDeleteAccount() => clearField(24); @$pb.TagNumber(24) - ApplicationData_DeleteAccount ensureDeleteaccount() => $_ensure(19); + ApplicationData_DeleteAccount ensureDeleteAccount() => $_ensure(19); @$pb.TagNumber(25) - ApplicationData_ReportUser get reportuser => $_getN(20); + ApplicationData_ReportUser get reportUser => $_getN(20); @$pb.TagNumber(25) - set reportuser(ApplicationData_ReportUser v) { setField(25, v); } + set reportUser(ApplicationData_ReportUser v) { setField(25, v); } @$pb.TagNumber(25) - $core.bool hasReportuser() => $_has(20); + $core.bool hasReportUser() => $_has(20); @$pb.TagNumber(25) - void clearReportuser() => clearField(25); + void clearReportUser() => clearField(25); @$pb.TagNumber(25) - ApplicationData_ReportUser ensureReportuser() => $_ensure(20); + ApplicationData_ReportUser ensureReportUser() => $_ensure(20); + + @$pb.TagNumber(26) + ApplicationData_ChangeUsername get changeUsername => $_getN(21); + @$pb.TagNumber(26) + set changeUsername(ApplicationData_ChangeUsername v) { setField(26, v); } + @$pb.TagNumber(26) + $core.bool hasChangeUsername() => $_has(21); + @$pb.TagNumber(26) + void clearChangeUsername() => clearField(26); + @$pb.TagNumber(26) + ApplicationData_ChangeUsername ensureChangeUsername() => $_ensure(21); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index 7c05b1b..b6cfd08 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -56,16 +56,22 @@ const Handshake$json = { '1': 'Handshake', '2': [ {'1': 'register', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.Handshake.Register', '9': 0, '10': 'register'}, - {'1': 'getauthchallenge', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthChallenge', '9': 0, '10': 'getauthchallenge'}, - {'1': 'getauthtoken', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthToken', '9': 0, '10': 'getauthtoken'}, + {'1': 'getAuthChallenge', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthChallenge', '9': 0, '10': 'getAuthChallenge'}, + {'1': 'getAuthToken', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.Handshake.GetAuthToken', '9': 0, '10': 'getAuthToken'}, {'1': 'authenticate', '3': 4, '4': 1, '5': 11, '6': '.client_to_server.Handshake.Authenticate', '9': 0, '10': 'authenticate'}, + {'1': 'requestPOW', '3': 5, '4': 1, '5': 11, '6': '.client_to_server.Handshake.RequestPOW', '9': 0, '10': 'requestPOW'}, ], - '3': [Handshake_Register$json, Handshake_GetAuthChallenge$json, Handshake_GetAuthToken$json, Handshake_Authenticate$json], + '3': [Handshake_RequestPOW$json, Handshake_Register$json, Handshake_GetAuthChallenge$json, Handshake_GetAuthToken$json, Handshake_Authenticate$json], '8': [ {'1': 'Handshake'}, ], }; +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_RequestPOW$json = { + '1': 'RequestPOW', +}; + @$core.Deprecated('Use handshakeDescriptor instead') const Handshake_Register$json = { '1': 'Register', @@ -77,11 +83,12 @@ const Handshake_Register$json = { {'1': 'signed_prekey_signature', '3': 5, '4': 1, '5': 12, '10': 'signedPrekeySignature'}, {'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '10': 'signedPrekeyId'}, {'1': 'registration_id', '3': 7, '4': 1, '5': 3, '10': 'registrationId'}, - {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '9': 1, '10': 'isIos', '17': true}, + {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, + {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, + {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, ], '8': [ {'1': '_invite_code'}, - {'1': '_is_ios'}, ], }; @@ -117,51 +124,54 @@ const Handshake_Authenticate$json = { /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' - 'FrZS5SZWdpc3RlckgAUghyZWdpc3RlchJaChBnZXRhdXRoY2hhbGxlbmdlGAIgASgLMiwuY2xp' - 'ZW50X3RvX3NlcnZlci5IYW5kc2hha2UuR2V0QXV0aENoYWxsZW5nZUgAUhBnZXRhdXRoY2hhbG' - 'xlbmdlEk4KDGdldGF1dGh0b2tlbhgDIAEoCzIoLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtl' - 'LkdldEF1dGhUb2tlbkgAUgxnZXRhdXRodG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' - 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRrj' - 'AgoIUmVnaXN0ZXISGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGA' - 'IgASgJSABSCmludml0ZUNvZGWIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVi' - 'bGljSWRlbnRpdHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3' - 'NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQ' - 'c2lnbmVkX3ByZWtleV9pZBgGIAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2' - 'lkGAcgASgDUg5yZWdpc3RyYXRpb25JZBIaCgZpc19pb3MYCCABKAhIAVIFaXNJb3OIAQFCDgoM' - 'X2ludml0ZV9jb2RlQgkKB19pc19pb3MaEgoQR2V0QXV0aENoYWxsZW5nZRpDCgxHZXRBdXRoVG' - '9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25z' - 'ZRqsAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2' - 'VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3ZlcnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogB' - 'ARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aWNlSWSIAQFCDgoMX2FwcF92ZXJzaW9uQgwKCl' - '9kZXZpY2VfaWRCCwoJSGFuZHNoYWtl'); + 'FrZS5SZWdpc3RlckgAUghyZWdpc3RlchJaChBnZXRBdXRoQ2hhbGxlbmdlGAIgASgLMiwuY2xp' + 'ZW50X3RvX3NlcnZlci5IYW5kc2hha2UuR2V0QXV0aENoYWxsZW5nZUgAUhBnZXRBdXRoQ2hhbG' + 'xlbmdlEk4KDGdldEF1dGhUb2tlbhgDIAEoCzIoLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtl' + 'LkdldEF1dGhUb2tlbkgAUgxnZXRBdXRoVG9rZW4STgoMYXV0aGVudGljYXRlGAQgASgLMiguY2' + 'xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlSABSDGF1dGhlbnRpY2F0ZRJI' + 'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF' + 'BPV0gAUgpyZXF1ZXN0UE9XGgwKClJlcXVlc3RQT1calAMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1l' + 'GAEgASgJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi' + '4KE3B1YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25l' + 'ZF9wcmVrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZR' + 'gFIAEoDFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANS' + 'DnNpZ25lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSW' + 'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK' + 'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX' + 'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS' + 'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGqwBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB' + 'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy' + 'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2' + 'VJZIgBAUIOCgxfYXBwX3ZlcnNpb25CDAoKX2RldmljZV9pZEILCglIYW5kc2hha2U='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { '1': 'ApplicationData', '2': [ - {'1': 'textmessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textmessage'}, - {'1': 'getuserbyusername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getuserbyusername'}, - {'1': 'getprekeysbyuserid', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getprekeysbyuserid'}, - {'1': 'getuserbyid', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getuserbyid'}, - {'1': 'updategooglefcmtoken', '3': 8, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateGoogleFcmToken', '9': 0, '10': 'updategooglefcmtoken'}, - {'1': 'getlocation', '3': 9, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetLocation', '9': 0, '10': 'getlocation'}, - {'1': 'getcurrentplaninfos', '3': 10, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetCurrentPlanInfos', '9': 0, '10': 'getcurrentplaninfos'}, - {'1': 'redeemvoucher', '3': 11, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemVoucher', '9': 0, '10': 'redeemvoucher'}, - {'1': 'getavailableplans', '3': 12, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAvailablePlans', '9': 0, '10': 'getavailableplans'}, - {'1': 'createvoucher', '3': 13, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.CreateVoucher', '9': 0, '10': 'createvoucher'}, - {'1': 'getvouchers', '3': 14, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetVouchers', '9': 0, '10': 'getvouchers'}, - {'1': 'Switchtopayedplan', '3': 15, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.SwitchToPayedPlan', '9': 0, '10': 'Switchtopayedplan'}, - {'1': 'getaddaccountsinvites', '3': 16, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAddAccountsInvites', '9': 0, '10': 'getaddaccountsinvites'}, - {'1': 'redeemadditionalcode', '3': 17, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemAdditionalCode', '9': 0, '10': 'redeemadditionalcode'}, - {'1': 'removeadditionaluser', '3': 18, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RemoveAdditionalUser', '9': 0, '10': 'removeadditionaluser'}, - {'1': 'updateplanoptions', '3': 19, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdatePlanOptions', '9': 0, '10': 'updateplanoptions'}, - {'1': 'downloaddone', '3': 20, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadDone', '9': 0, '10': 'downloaddone'}, - {'1': 'getsignedprekeybyuserid', '3': 22, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetSignedPreKeyByUserId', '9': 0, '10': 'getsignedprekeybyuserid'}, - {'1': 'updatesignedprekey', '3': 23, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateSignedPreKey', '9': 0, '10': 'updatesignedprekey'}, - {'1': 'deleteaccount', '3': 24, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DeleteAccount', '9': 0, '10': 'deleteaccount'}, - {'1': 'reportuser', '3': 25, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ReportUser', '9': 0, '10': 'reportuser'}, + {'1': 'textMessage', '3': 1, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.TextMessage', '9': 0, '10': 'textMessage'}, + {'1': 'getUserByUsername', '3': 2, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserByUsername', '9': 0, '10': 'getUserByUsername'}, + {'1': 'getPrekeysByUserId', '3': 3, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetPrekeysByUserId', '9': 0, '10': 'getPrekeysByUserId'}, + {'1': 'getUserById', '3': 6, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetUserById', '9': 0, '10': 'getUserById'}, + {'1': 'updateGoogleFcmToken', '3': 8, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateGoogleFcmToken', '9': 0, '10': 'updateGoogleFcmToken'}, + {'1': 'getLocation', '3': 9, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetLocation', '9': 0, '10': 'getLocation'}, + {'1': 'getCurrentPlanInfos', '3': 10, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetCurrentPlanInfos', '9': 0, '10': 'getCurrentPlanInfos'}, + {'1': 'redeemVoucher', '3': 11, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemVoucher', '9': 0, '10': 'redeemVoucher'}, + {'1': 'getAvailablePlans', '3': 12, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAvailablePlans', '9': 0, '10': 'getAvailablePlans'}, + {'1': 'createVoucher', '3': 13, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.CreateVoucher', '9': 0, '10': 'createVoucher'}, + {'1': 'getVouchers', '3': 14, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetVouchers', '9': 0, '10': 'getVouchers'}, + {'1': 'switchtoPayedPlan', '3': 15, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.SwitchToPayedPlan', '9': 0, '10': 'switchtoPayedPlan'}, + {'1': 'getAddaccountsInvites', '3': 16, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetAddAccountsInvites', '9': 0, '10': 'getAddaccountsInvites'}, + {'1': 'redeemAdditionalCode', '3': 17, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RedeemAdditionalCode', '9': 0, '10': 'redeemAdditionalCode'}, + {'1': 'removeAdditionalUser', '3': 18, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.RemoveAdditionalUser', '9': 0, '10': 'removeAdditionalUser'}, + {'1': 'updatePlanOptions', '3': 19, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdatePlanOptions', '9': 0, '10': 'updatePlanOptions'}, + {'1': 'downloadDone', '3': 20, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DownloadDone', '9': 0, '10': 'downloadDone'}, + {'1': 'getSignedPrekeyByUserid', '3': 22, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.GetSignedPreKeyByUserId', '9': 0, '10': 'getSignedPrekeyByUserid'}, + {'1': 'updateSignedPrekey', '3': 23, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.UpdateSignedPreKey', '9': 0, '10': 'updateSignedPrekey'}, + {'1': 'deleteAccount', '3': 24, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.DeleteAccount', '9': 0, '10': 'deleteAccount'}, + {'1': 'reportUser', '3': 25, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ReportUser', '9': 0, '10': 'reportUser'}, + {'1': 'changeUsername', '3': 26, '4': 1, '5': 11, '6': '.client_to_server.ApplicationData.ChangeUsername', '9': 0, '10': 'changeUsername'}, ], - '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_RedeemVoucher$json, ApplicationData_SwitchToPayedPlan$json, ApplicationData_UpdatePlanOptions$json, ApplicationData_CreateVoucher$json, ApplicationData_GetLocation$json, ApplicationData_GetVouchers$json, ApplicationData_GetAvailablePlans$json, ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, ApplicationData_RedeemAdditionalCode$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json, ApplicationData_UpdateSignedPreKey$json, ApplicationData_DownloadDone$json, ApplicationData_ReportUser$json, ApplicationData_DeleteAccount$json], + '3': [ApplicationData_TextMessage$json, ApplicationData_GetUserByUsername$json, ApplicationData_ChangeUsername$json, ApplicationData_UpdateGoogleFcmToken$json, ApplicationData_GetUserById$json, ApplicationData_RedeemVoucher$json, ApplicationData_SwitchToPayedPlan$json, ApplicationData_UpdatePlanOptions$json, ApplicationData_CreateVoucher$json, ApplicationData_GetLocation$json, ApplicationData_GetVouchers$json, ApplicationData_GetAvailablePlans$json, ApplicationData_GetAddAccountsInvites$json, ApplicationData_GetCurrentPlanInfos$json, ApplicationData_RedeemAdditionalCode$json, ApplicationData_RemoveAdditionalUser$json, ApplicationData_GetPrekeysByUserId$json, ApplicationData_GetSignedPreKeyByUserId$json, ApplicationData_UpdateSignedPreKey$json, ApplicationData_DownloadDone$json, ApplicationData_ReportUser$json, ApplicationData_DeleteAccount$json], '8': [ {'1': 'ApplicationData'}, ], @@ -188,6 +198,14 @@ const ApplicationData_GetUserByUsername$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_ChangeUsername$json = { + '1': 'ChangeUsername', + '2': [ + {'1': 'username', '3': 1, '4': 1, '5': 9, '10': 'username'}, + ], +}; + @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData_UpdateGoogleFcmToken$json = { '1': 'UpdateGoogleFcmToken', @@ -329,64 +347,67 @@ const ApplicationData_DeleteAccount$json = { /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( - 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dG1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' - 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0bWVzc2FnZRJjChFnZXR1c2Vy' - 'Ynl1c2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' - 'VzZXJCeVVzZXJuYW1lSABSEWdldHVzZXJieXVzZXJuYW1lEmYKEmdldHByZWtleXNieXVzZXJp' + 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' + 'VyLkFwcGxpY2F0aW9uRGF0YS5UZXh0TWVzc2FnZUgAUgt0ZXh0TWVzc2FnZRJjChFnZXRVc2Vy' + 'QnlVc2VybmFtZRgCIAEoCzIzLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldF' + 'VzZXJCeVVzZXJuYW1lSABSEWdldFVzZXJCeVVzZXJuYW1lEmYKEmdldFByZWtleXNCeVVzZXJJ' 'ZBgDIAEoCzI0LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFByZWtleXNCeV' - 'VzZXJJZEgAUhJnZXRwcmVrZXlzYnl1c2VyaWQSUQoLZ2V0dXNlcmJ5aWQYBiABKAsyLS5jbGll' - 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXR1c2VyYnlpZB' - 'JsChR1cGRhdGVnb29nbGVmY210b2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' - 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZWdvb2dsZWZjbXRva2VuEl' - 'EKC2dldGxvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' - 'R2V0TG9jYXRpb25IAFILZ2V0bG9jYXRpb24SaQoTZ2V0Y3VycmVudHBsYW5pbmZvcxgKIAEoCz' + 'VzZXJJZEgAUhJnZXRQcmVrZXlzQnlVc2VySWQSUQoLZ2V0VXNlckJ5SWQYBiABKAsyLS5jbGll' + 'bnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRVc2VyQnlJZEgAUgtnZXRVc2VyQnlJZB' + 'JsChR1cGRhdGVHb29nbGVGY21Ub2tlbhgIIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGlj' + 'YXRpb25EYXRhLlVwZGF0ZUdvb2dsZUZjbVRva2VuSABSFHVwZGF0ZUdvb2dsZUZjbVRva2VuEl' + 'EKC2dldExvY2F0aW9uGAkgASgLMi0uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEu' + 'R2V0TG9jYXRpb25IAFILZ2V0TG9jYXRpb24SaQoTZ2V0Q3VycmVudFBsYW5JbmZvcxgKIAEoCz' 'I1LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldEN1cnJlbnRQbGFuSW5mb3NI' - 'AFITZ2V0Y3VycmVudHBsYW5pbmZvcxJXCg1yZWRlZW12b3VjaGVyGAsgASgLMi8uY2xpZW50X3' - 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW12b3VjaGVy' - 'EmMKEWdldGF2YWlsYWJsZXBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' - 'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0YXZhaWxhYmxlcGxhbnMSVwoNY3JlYXRl' - 'dm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' - 'ZvdWNoZXJIAFINY3JlYXRldm91Y2hlchJRCgtnZXR2b3VjaGVycxgOIAEoCzItLmNsaWVudF90' - 'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldHZvdWNoZXJzEmMKEV' - 'N3aXRjaHRvcGF5ZWRwbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' - 'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRU3dpdGNodG9wYXllZHBsYW4SbwoVZ2V0YWRkYWNjb3' - 'VudHNpbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' - 'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldGFkZGFjY291bnRzaW52aXRlcxJsChRyZWRlZW1hZG' - 'RpdGlvbmFsY29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' - 'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbWFkZGl0aW9uYWxjb2RlEmwKFHJlbW92ZWFkZG' - 'l0aW9uYWx1c2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVt' - 'b3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlYWRkaXRpb25hbHVzZXISYwoRdXBkYXRlcGxhbm' + 'AFITZ2V0Q3VycmVudFBsYW5JbmZvcxJXCg1yZWRlZW1Wb3VjaGVyGAsgASgLMi8uY2xpZW50X3' + 'RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVkZWVtVm91Y2hlckgAUg1yZWRlZW1Wb3VjaGVy' + 'EmMKEWdldEF2YWlsYWJsZVBsYW5zGAwgASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' + 'lvbkRhdGEuR2V0QXZhaWxhYmxlUGxhbnNIAFIRZ2V0QXZhaWxhYmxlUGxhbnMSVwoNY3JlYXRl' + 'Vm91Y2hlchgNIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkNyZWF0ZV' + 'ZvdWNoZXJIAFINY3JlYXRlVm91Y2hlchJRCgtnZXRWb3VjaGVycxgOIAEoCzItLmNsaWVudF90' + 'b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkdldFZvdWNoZXJzSABSC2dldFZvdWNoZXJzEmMKEX' + 'N3aXRjaHRvUGF5ZWRQbGFuGA8gASgLMjMuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRh' + 'dGEuU3dpdGNoVG9QYXllZFBsYW5IAFIRc3dpdGNodG9QYXllZFBsYW4SbwoVZ2V0QWRkYWNjb3' + 'VudHNJbnZpdGVzGBAgASgLMjcuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuR2V0' + 'QWRkQWNjb3VudHNJbnZpdGVzSABSFWdldEFkZGFjY291bnRzSW52aXRlcxJsChRyZWRlZW1BZG' + 'RpdGlvbmFsQ29kZRgRIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlJl' + 'ZGVlbUFkZGl0aW9uYWxDb2RlSABSFHJlZGVlbUFkZGl0aW9uYWxDb2RlEmwKFHJlbW92ZUFkZG' + 'l0aW9uYWxVc2VyGBIgASgLMjYuY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdGlvbkRhdGEuUmVt' + 'b3ZlQWRkaXRpb25hbFVzZXJIAFIUcmVtb3ZlQWRkaXRpb25hbFVzZXISYwoRdXBkYXRlUGxhbk' '9wdGlvbnMYEyABKAsyMy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5VcGRhdGVQ' - 'bGFuT3B0aW9uc0gAUhF1cGRhdGVwbGFub3B0aW9ucxJUCgxkb3dubG9hZGRvbmUYFCABKAsyLi' + 'bGFuT3B0aW9uc0gAUhF1cGRhdGVQbGFuT3B0aW9ucxJUCgxkb3dubG9hZERvbmUYFCABKAsyLi' '5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5Eb3dubG9hZERvbmVIAFIMZG93bmxv' - 'YWRkb25lEnUKF2dldHNpZ25lZHByZWtleWJ5dXNlcmlkGBYgASgLMjkuY2xpZW50X3RvX3Nlcn' - 'Zlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIXZ2V0c2lnbmVk' - 'cHJla2V5Ynl1c2VyaWQSZgoSdXBkYXRlc2lnbmVkcHJla2V5GBcgASgLMjQuY2xpZW50X3RvX3' - 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVwZGF0ZXNpZ25l' - 'ZHByZWtleRJXCg1kZWxldGVhY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3NlcnZlci5BcHBsaW' - 'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVhY2NvdW50Ek4KCnJlcG9ydHVzZXIY' + 'YWREb25lEnUKF2dldFNpZ25lZFByZWtleUJ5VXNlcmlkGBYgASgLMjkuY2xpZW50X3RvX3Nlcn' + 'Zlci5BcHBsaWNhdGlvbkRhdGEuR2V0U2lnbmVkUHJlS2V5QnlVc2VySWRIAFIXZ2V0U2lnbmVk' + 'UHJla2V5QnlVc2VyaWQSZgoSdXBkYXRlU2lnbmVkUHJla2V5GBcgASgLMjQuY2xpZW50X3RvX3' + 'NlcnZlci5BcHBsaWNhdGlvbkRhdGEuVXBkYXRlU2lnbmVkUHJlS2V5SABSEnVwZGF0ZVNpZ25l' + 'ZFByZWtleRJXCg1kZWxldGVBY2NvdW50GBggASgLMi8uY2xpZW50X3RvX3NlcnZlci5BcHBsaW' + 'NhdGlvbkRhdGEuRGVsZXRlQWNjb3VudEgAUg1kZWxldGVBY2NvdW50Ek4KCnJlcG9ydFVzZXIY' 'GSABKAsyLC5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5SZXBvcnRVc2VySABSCn' - 'JlcG9ydHVzZXIaagoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJv' - 'ZHkYAyABKAxSBGJvZHkSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdX' - 'NoX2RhdGEaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1l' - 'GjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbR' - 'omCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hl' - 'chIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGnAKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW' - '5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9udGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxh' - 'dXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDG' - 'F1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1' - 'ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCgtHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycx' - 'oTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3Vy' - 'cmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdGlvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIA' - 'EoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgD' - 'UgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZB' - 'oyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEK' - 'ElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcm' - 'VrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3By' - 'ZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZE' - 'RvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNl' - 'chIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAi' - 'ABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50QhEKD0FwcGxpY2F0aW9uRGF0YQ=='); + 'JlcG9ydFVzZXISWgoOY2hhbmdlVXNlcm5hbWUYGiABKAsyMC5jbGllbnRfdG9fc2VydmVyLkFw' + 'cGxpY2F0aW9uRGF0YS5DaGFuZ2VVc2VybmFtZUgAUg5jaGFuZ2VVc2VybmFtZRpqCgtUZXh0TW' + 'Vzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglw' + 'dXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQn' + 'lVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUS' + 'GgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCm' + 'dvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEg' + 'ASgDUgZ1c2VySWQaKQoNUmVkZWVtVm91Y2hlchIYCgd2b3VjaGVyGAEgASgJUgd2b3VjaGVyGn' + 'AKEVN3aXRjaFRvUGF5ZWRQbGFuEhcKB3BsYW5faWQYASABKAlSBnBsYW5JZBIfCgtwYXlfbW9u' + 'dGhseRgCIAEoCFIKcGF5TW9udGhseRIhCgxhdXRvX3JlbmV3YWwYAyABKAhSC2F1dG9SZW5ld2' + 'FsGjYKEVVwZGF0ZVBsYW5PcHRpb25zEiEKDGF1dG9fcmVuZXdhbBgBIAEoCFILYXV0b1JlbmV3' + 'YWwaMAoNQ3JlYXRlVm91Y2hlchIfCgt2YWx1ZV9jZW50cxgBIAEoDVIKdmFsdWVDZW50cxoNCg' + 'tHZXRMb2NhdGlvbhoNCgtHZXRWb3VjaGVycxoTChFHZXRBdmFpbGFibGVQbGFucxoXChVHZXRB' + 'ZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxo3ChRSZWRlZW1BZGRpdG' + 'lvbmFsQ29kZRIfCgtpbnZpdGVfY29kZRgCIAEoCVIKaW52aXRlQ29kZRovChRSZW1vdmVBZGRp' + 'dGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlck' + 'lkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIX' + 'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZW' + 'RfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GAIgASgM' + 'UgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZF' + 'ByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YASABKAxS' + 'DWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEgASgDUg' + '5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhoPCg1EZWxldGVBY2NvdW50' + 'QhEKD0FwcGxpY2F0aW9uRGF0YQ=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/api/websocket/error.pbenum.dart b/lib/src/model/protobuf/api/websocket/error.pbenum.dart index 3e9c5e0..1c5331e 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbenum.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbenum.dart @@ -48,6 +48,8 @@ class ErrorCode extends $pb.ProtobufEnum { static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken'); static const ErrorCode AppVersionOutdated = ErrorCode._(1030, _omitEnumNames ? '' : 'AppVersionOutdated'); static const ErrorCode NewDeviceRegistered = ErrorCode._(1031, _omitEnumNames ? '' : 'NewDeviceRegistered'); + static const ErrorCode InvalidProofOfWork = ErrorCode._(1032, _omitEnumNames ? '' : 'InvalidProofOfWork'); + static const ErrorCode RegistrationDisabled = ErrorCode._(1033, _omitEnumNames ? '' : 'RegistrationDisabled'); static const $core.List values = [ Unknown, @@ -84,6 +86,8 @@ class ErrorCode extends $pb.ProtobufEnum { UserIdAlreadyTaken, AppVersionOutdated, NewDeviceRegistered, + InvalidProofOfWork, + RegistrationDisabled, ]; static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/api/websocket/error.pbjson.dart b/lib/src/model/protobuf/api/websocket/error.pbjson.dart index fad129d..2e937dd 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbjson.dart @@ -51,6 +51,8 @@ const ErrorCode$json = { {'1': 'UserIdAlreadyTaken', '2': 1029}, {'1': 'AppVersionOutdated', '2': 1030}, {'1': 'NewDeviceRegistered', '2': 1031}, + {'1': 'InvalidProofOfWork', '2': 1032}, + {'1': 'RegistrationDisabled', '2': 1033}, ], }; @@ -70,5 +72,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode( 'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' - 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcI'); + 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh' + 'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCA=='); diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 8d67382..381c100 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -1536,6 +1536,70 @@ class Response_DownloadTokens extends $pb.GeneratedMessage { $core.List<$core.List<$core.int>> get downloadTokens => $_getList(0); } +class Response_ProofOfWork extends $pb.GeneratedMessage { + factory Response_ProofOfWork({ + $core.String? prefix, + $fixnum.Int64? difficulty, + }) { + final $result = create(); + if (prefix != null) { + $result.prefix = prefix; + } + if (difficulty != null) { + $result.difficulty = difficulty; + } + return $result; + } + Response_ProofOfWork._() : super(); + factory Response_ProofOfWork.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Response_ProofOfWork.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Response.ProofOfWork', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'prefix') + ..aInt64(2, _omitFieldNames ? '' : 'difficulty') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Response_ProofOfWork clone() => Response_ProofOfWork()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Response_ProofOfWork copyWith(void Function(Response_ProofOfWork) updates) => super.copyWith((message) => updates(message as Response_ProofOfWork)) as Response_ProofOfWork; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_ProofOfWork create() => Response_ProofOfWork._(); + Response_ProofOfWork createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Response_ProofOfWork getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Response_ProofOfWork? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get prefix => $_getSZ(0); + @$pb.TagNumber(1) + set prefix($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasPrefix() => $_has(0); + @$pb.TagNumber(1) + void clearPrefix() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get difficulty => $_getI64(1); + @$pb.TagNumber(2) + set difficulty($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasDifficulty() => $_has(1); + @$pb.TagNumber(2) + void clearDifficulty() => clearField(2); +} + enum Response_Ok_Ok { none, userid, @@ -1551,6 +1615,7 @@ enum Response_Ok_Ok { addaccountsinvites, downloadtokens, signedprekey, + proofOfWork, notSet } @@ -1570,6 +1635,7 @@ class Response_Ok extends $pb.GeneratedMessage { Response_AddAccountsInvites? addaccountsinvites, Response_DownloadTokens? downloadtokens, Response_SignedPreKey? signedprekey, + Response_ProofOfWork? proofOfWork, }) { final $result = create(); if (none != null) { @@ -1614,6 +1680,9 @@ class Response_Ok extends $pb.GeneratedMessage { if (signedprekey != null) { $result.signedprekey = signedprekey; } + if (proofOfWork != null) { + $result.proofOfWork = proofOfWork; + } return $result; } Response_Ok._() : super(); @@ -1635,10 +1704,11 @@ class Response_Ok extends $pb.GeneratedMessage { 12 : Response_Ok_Ok.addaccountsinvites, 13 : Response_Ok_Ok.downloadtokens, 14 : Response_Ok_Ok.signedprekey, + 15 : Response_Ok_Ok.proofOfWork, 0 : Response_Ok_Ok.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Response.Ok', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aInt64(2, _omitFieldNames ? '' : 'userid') ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'authchallenge', $pb.PbFieldType.OY) @@ -1653,6 +1723,7 @@ class Response_Ok extends $pb.GeneratedMessage { ..aOM(12, _omitFieldNames ? '' : 'addaccountsinvites', subBuilder: Response_AddAccountsInvites.create) ..aOM(13, _omitFieldNames ? '' : 'downloadtokens', subBuilder: Response_DownloadTokens.create) ..aOM(14, _omitFieldNames ? '' : 'signedprekey', subBuilder: Response_SignedPreKey.create) + ..aOM(15, _omitFieldNames ? '' : 'proofOfWork', protoName: 'proofOfWork', subBuilder: Response_ProofOfWork.create) ..hasRequiredFields = false ; @@ -1825,6 +1896,17 @@ class Response_Ok extends $pb.GeneratedMessage { void clearSignedprekey() => clearField(14); @$pb.TagNumber(14) Response_SignedPreKey ensureSignedprekey() => $_ensure(13); + + @$pb.TagNumber(15) + Response_ProofOfWork get proofOfWork => $_getN(14); + @$pb.TagNumber(15) + set proofOfWork(Response_ProofOfWork v) { setField(15, v); } + @$pb.TagNumber(15) + $core.bool hasProofOfWork() => $_has(14); + @$pb.TagNumber(15) + void clearProofOfWork() => clearField(15); + @$pb.TagNumber(15) + Response_ProofOfWork ensureProofOfWork() => $_ensure(14); } enum Response_Response { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index eabade8..441503e 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -73,7 +73,7 @@ const Response$json = { {'1': 'ok', '3': 1, '4': 1, '5': 11, '6': '.server_to_client.Response.Ok', '9': 0, '10': 'ok'}, {'1': 'error', '3': 2, '4': 1, '5': 14, '6': '.error.ErrorCode', '9': 0, '10': 'error'}, ], - '3': [Response_Authenticated$json, Response_Plan$json, Response_Plans$json, Response_AddAccountsInvite$json, Response_AddAccountsInvites$json, Response_Transaction$json, Response_AdditionalAccount$json, Response_Voucher$json, Response_Vouchers$json, Response_PlanBallance$json, Response_Location$json, Response_PreKey$json, Response_SignedPreKey$json, Response_UserData$json, Response_UploadToken$json, Response_DownloadTokens$json, Response_Ok$json], + '3': [Response_Authenticated$json, Response_Plan$json, Response_Plans$json, Response_AddAccountsInvite$json, Response_AddAccountsInvites$json, Response_Transaction$json, Response_AdditionalAccount$json, Response_Voucher$json, Response_Vouchers$json, Response_PlanBallance$json, Response_Location$json, Response_PreKey$json, Response_SignedPreKey$json, Response_UserData$json, Response_UploadToken$json, Response_DownloadTokens$json, Response_ProofOfWork$json, Response_Ok$json], '4': [Response_TransactionTypes$json], '8': [ {'1': 'Response'}, @@ -257,6 +257,15 @@ const Response_DownloadTokens$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_ProofOfWork$json = { + '1': 'ProofOfWork', + '2': [ + {'1': 'prefix', '3': 1, '4': 1, '5': 9, '10': 'prefix'}, + {'1': 'difficulty', '3': 2, '4': 1, '5': 3, '10': 'difficulty'}, + ], +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_Ok$json = { '1': 'Ok', @@ -275,6 +284,7 @@ const Response_Ok$json = { {'1': 'addaccountsinvites', '3': 12, '4': 1, '5': 11, '6': '.server_to_client.Response.AddAccountsInvites', '9': 0, '10': 'addaccountsinvites'}, {'1': 'downloadtokens', '3': 13, '4': 1, '5': 11, '6': '.server_to_client.Response.DownloadTokens', '9': 0, '10': 'downloadtokens'}, {'1': 'signedprekey', '3': 14, '4': 1, '5': 11, '6': '.server_to_client.Response.SignedPreKey', '9': 0, '10': 'signedprekey'}, + {'1': 'proofOfWork', '3': 15, '4': 1, '5': 11, '6': '.server_to_client.Response.ProofOfWork', '9': 0, '10': 'proofOfWork'}, ], '8': [ {'1': 'Ok'}, @@ -352,24 +362,26 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( '9zaWduZWRfcHJla2V5X2lkGlkKC1VwbG9hZFRva2VuEiEKDHVwbG9hZF90b2tlbhgBIAEoDFIL' 'dXBsb2FkVG9rZW4SJwoPZG93bmxvYWRfdG9rZW5zGAIgAygMUg5kb3dubG9hZFRva2Vucxo5Cg' '5Eb3dubG9hZFRva2VucxInCg9kb3dubG9hZF90b2tlbnMYASADKAxSDmRvd25sb2FkVG9rZW5z' - 'GvcGCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZXJpZB' - 'ImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9rZW4Y' - 'BCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG9hZH' - 'Rva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Vc2Vy' - 'RGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkEKCGxvY2' - 'F0aW9uGAcgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Mb2NhdGlvbkgAUghsb2Nh' - 'dGlvbhJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS' - '5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJf' - 'dG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCz' - 'InLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5j' - 'ZRJBCgh2b3VjaGVycxgLIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVm91Y2hlcn' - 'NIAFIIdm91Y2hlcnMSXwoSYWRkYWNjb3VudHNpbnZpdGVzGAwgASgLMi0uc2VydmVyX3RvX2Ns' - 'aWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAFISYWRkYWNjb3VudHNpbnZpdGVzEl' - 'MKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5Eb3du' - 'bG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaWduZWRwcmVrZXkYDiABKAsyJy5zZX' - 'J2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtleUgAUgxzaWduZWRwcmVrZXlCBAoC' - 'T2silgEKEFRyYW5zYWN0aW9uVHlwZXMSCgoGUmVmdW5kEAASEwoPVm91Y2hlclJlZGVlbWVkEA' - 'ESEgoOVm91Y2hlckNyZWF0ZWQQAhIICgRDYXNoEAMSDwoLUGxhblVwZ3JhZGUQBBILCgdVbmtu' - 'b3duEAUSFAoQVGhhbmtzRm9yVGVzdGluZxAGEg8KC0F1dG9SZW5ld2FsEAdCCgoIUmVzcG9uc2' - 'U='); + 'GkUKC1Byb29mT2ZXb3JrEhYKBnByZWZpeBgBIAEoCVIGcHJlZml4Eh4KCmRpZmZpY3VsdHkYAi' + 'ABKANSCmRpZmZpY3VsdHkawwcKAk9rEhQKBE5vbmUYASABKAhIAFIETm9uZRIYCgZ1c2VyaWQY' + 'AiABKANIAFIGdXNlcmlkEiYKDWF1dGhjaGFsbGVuZ2UYAyABKAxIAFINYXV0aGNoYWxsZW5nZR' + 'JKCgt1cGxvYWR0b2tlbhgEIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXBsb2Fk' + 'VG9rZW5IAFILdXBsb2FkdG9rZW4SQQoIdXNlcmRhdGEYBSABKAsyIy5zZXJ2ZXJfdG9fY2xpZW' + '50LlJlc3BvbnNlLlVzZXJEYXRhSABSCHVzZXJkYXRhEh4KCWF1dGh0b2tlbhgGIAEoDEgAUglh' + 'dXRodG9rZW4SQQoIbG9jYXRpb24YByABKAsyIy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk' + 'xvY2F0aW9uSABSCGxvY2F0aW9uElAKDWF1dGhlbnRpY2F0ZWQYCCABKAsyKC5zZXJ2ZXJfdG9f' + 'Y2xpZW50LlJlc3BvbnNlLkF1dGhlbnRpY2F0ZWRIAFINYXV0aGVudGljYXRlZBI4CgVwbGFucx' + 'gJIAEoCzIgLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbnNIAFIFcGxhbnMSTQoMcGxh' + 'bmJhbGxhbmNlGAogASgLMicuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuQmFsbGFuY2' + 'VIAFIMcGxhbmJhbGxhbmNlEkEKCHZvdWNoZXJzGAsgASgLMiMuc2VydmVyX3RvX2NsaWVudC5S' + 'ZXNwb25zZS5Wb3VjaGVyc0gAUgh2b3VjaGVycxJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKA' + 'syLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRh' + 'Y2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW' + '50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHBy' + 'ZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDH' + 'NpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVz' + 'cG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2silgEKEFRyYW5zYWN0aW9uVH' + 'lwZXMSCgoGUmVmdW5kEAASEwoPVm91Y2hlclJlZGVlbWVkEAESEgoOVm91Y2hlckNyZWF0ZWQQ' + 'AhIICgRDYXNoEAMSDwoLUGxhblVwZ3JhZGUQBBILCgdVbmtub3duEAUSFAoQVGhhbmtzRm9yVG' + 'VzdGluZxAGEg8KC0F1dG9SZW5ld2FsEAdCCgoIUmVzcG9uc2U='); diff --git a/lib/src/model/protobuf/backup/backup.proto b/lib/src/model/protobuf/client/backup.proto similarity index 100% rename from lib/src/model/protobuf/backup/backup.proto rename to lib/src/model/protobuf/client/backup.proto diff --git a/lib/src/model/protobuf/backup/backup.pb.dart b/lib/src/model/protobuf/client/generated/backup.pb.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pb.dart rename to lib/src/model/protobuf/client/generated/backup.pb.dart diff --git a/lib/src/model/protobuf/backup/backup.pbenum.dart b/lib/src/model/protobuf/client/generated/backup.pbenum.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbenum.dart rename to lib/src/model/protobuf/client/generated/backup.pbenum.dart diff --git a/lib/src/model/protobuf/backup/backup.pbjson.dart b/lib/src/model/protobuf/client/generated/backup.pbjson.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbjson.dart rename to lib/src/model/protobuf/client/generated/backup.pbjson.dart diff --git a/lib/src/model/protobuf/backup/backup.pbserver.dart b/lib/src/model/protobuf/client/generated/backup.pbserver.dart similarity index 100% rename from lib/src/model/protobuf/backup/backup.pbserver.dart rename to lib/src/model/protobuf/client/generated/backup.pbserver.dart diff --git a/lib/src/model/protobuf/client/generated/groups.pb.dart b/lib/src/model/protobuf/client/generated/groups.pb.dart new file mode 100644 index 0000000..1be9b98 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pb.dart @@ -0,0 +1,246 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'groups.pbenum.dart'; + +export 'groups.pbenum.dart'; + +/// Stored encrypted on the server in the members columns. +class EncryptedGroupState extends $pb.GeneratedMessage { + factory EncryptedGroupState({ + $core.Iterable<$fixnum.Int64>? memberIds, + $core.Iterable<$fixnum.Int64>? adminIds, + $core.String? groupName, + $fixnum.Int64? deleteMessagesAfterMilliseconds, + $core.List<$core.int>? padding, + }) { + final $result = create(); + if (memberIds != null) { + $result.memberIds.addAll(memberIds); + } + if (adminIds != null) { + $result.adminIds.addAll(adminIds); + } + if (groupName != null) { + $result.groupName = groupName; + } + if (deleteMessagesAfterMilliseconds != null) { + $result.deleteMessagesAfterMilliseconds = deleteMessagesAfterMilliseconds; + } + if (padding != null) { + $result.padding = padding; + } + return $result; + } + EncryptedGroupState._() : super(); + factory EncryptedGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedGroupState', createEmptyInstance: create) + ..p<$fixnum.Int64>(1, _omitFieldNames ? '' : 'memberIds', $pb.PbFieldType.K6, protoName: 'memberIds') + ..p<$fixnum.Int64>(2, _omitFieldNames ? '' : 'adminIds', $pb.PbFieldType.K6, protoName: 'adminIds') + ..aOS(3, _omitFieldNames ? '' : 'groupName', protoName: 'groupName') + ..aInt64(4, _omitFieldNames ? '' : 'deleteMessagesAfterMilliseconds', protoName: 'deleteMessagesAfterMilliseconds') + ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'padding', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedGroupState clone() => EncryptedGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedGroupState copyWith(void Function(EncryptedGroupState) updates) => super.copyWith((message) => updates(message as EncryptedGroupState)) as EncryptedGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedGroupState create() => EncryptedGroupState._(); + EncryptedGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedGroupState? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$fixnum.Int64> get memberIds => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$fixnum.Int64> get adminIds => $_getList(1); + + @$pb.TagNumber(3) + $core.String get groupName => $_getSZ(2); + @$pb.TagNumber(3) + set groupName($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasGroupName() => $_has(2); + @$pb.TagNumber(3) + void clearGroupName() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get deleteMessagesAfterMilliseconds => $_getI64(3); + @$pb.TagNumber(4) + set deleteMessagesAfterMilliseconds($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasDeleteMessagesAfterMilliseconds() => $_has(3); + @$pb.TagNumber(4) + void clearDeleteMessagesAfterMilliseconds() => clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get padding => $_getN(4); + @$pb.TagNumber(5) + set padding($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(5) + $core.bool hasPadding() => $_has(4); + @$pb.TagNumber(5) + void clearPadding() => clearField(5); +} + +class EncryptedAppendedGroupState extends $pb.GeneratedMessage { + factory EncryptedAppendedGroupState({ + EncryptedAppendedGroupState_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + EncryptedAppendedGroupState._() : super(); + factory EncryptedAppendedGroupState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedAppendedGroupState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedAppendedGroupState', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedAppendedGroupState_Type.LEFT_GROUP, valueOf: EncryptedAppendedGroupState_Type.valueOf, enumValues: EncryptedAppendedGroupState_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedAppendedGroupState clone() => EncryptedAppendedGroupState()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedAppendedGroupState copyWith(void Function(EncryptedAppendedGroupState) updates) => super.copyWith((message) => updates(message as EncryptedAppendedGroupState)) as EncryptedAppendedGroupState; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedAppendedGroupState create() => EncryptedAppendedGroupState._(); + EncryptedAppendedGroupState createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedAppendedGroupState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedAppendedGroupState? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedAppendedGroupState_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedAppendedGroupState_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + +class EncryptedGroupStateEnvelop extends $pb.GeneratedMessage { + factory EncryptedGroupStateEnvelop({ + $core.List<$core.int>? nonce, + $core.List<$core.int>? encryptedGroupState, + $core.List<$core.int>? mac, + }) { + final $result = create(); + if (nonce != null) { + $result.nonce = nonce; + } + if (encryptedGroupState != null) { + $result.encryptedGroupState = encryptedGroupState; + } + if (mac != null) { + $result.mac = mac; + } + return $result; + } + EncryptedGroupStateEnvelop._() : super(); + factory EncryptedGroupStateEnvelop.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedGroupStateEnvelop.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedGroupStateEnvelop', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'encryptedGroupState', $pb.PbFieldType.OY, protoName: 'encryptedGroupState') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedGroupStateEnvelop clone() => EncryptedGroupStateEnvelop()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedGroupStateEnvelop copyWith(void Function(EncryptedGroupStateEnvelop) updates) => super.copyWith((message) => updates(message as EncryptedGroupStateEnvelop)) as EncryptedGroupStateEnvelop; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedGroupStateEnvelop create() => EncryptedGroupStateEnvelop._(); + EncryptedGroupStateEnvelop createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedGroupStateEnvelop getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedGroupStateEnvelop? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get nonce => $_getN(0); + @$pb.TagNumber(1) + set nonce($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasNonce() => $_has(0); + @$pb.TagNumber(1) + void clearNonce() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedGroupState => $_getN(1); + @$pb.TagNumber(2) + set encryptedGroupState($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasEncryptedGroupState() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedGroupState() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get mac => $_getN(2); + @$pb.TagNumber(3) + set mac($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasMac() => $_has(2); + @$pb.TagNumber(3) + void clearMac() => clearField(3); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/client/generated/groups.pbenum.dart b/lib/src/model/protobuf/client/generated/groups.pbenum.dart new file mode 100644 index 0000000..69a0e68 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbenum.dart @@ -0,0 +1,30 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class EncryptedAppendedGroupState_Type extends $pb.ProtobufEnum { + static const EncryptedAppendedGroupState_Type LEFT_GROUP = EncryptedAppendedGroupState_Type._(0, _omitEnumNames ? '' : 'LEFT_GROUP'); + + static const $core.List values = [ + LEFT_GROUP, + ]; + + static final $core.Map<$core.int, EncryptedAppendedGroupState_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedAppendedGroupState_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedAppendedGroupState_Type._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/groups.pbjson.dart b/lib/src/model/protobuf/client/generated/groups.pbjson.dart new file mode 100644 index 0000000..1d81d71 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbjson.dart @@ -0,0 +1,76 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use encryptedGroupStateDescriptor instead') +const EncryptedGroupState$json = { + '1': 'EncryptedGroupState', + '2': [ + {'1': 'memberIds', '3': 1, '4': 3, '5': 3, '10': 'memberIds'}, + {'1': 'adminIds', '3': 2, '4': 3, '5': 3, '10': 'adminIds'}, + {'1': 'groupName', '3': 3, '4': 1, '5': 9, '10': 'groupName'}, + {'1': 'deleteMessagesAfterMilliseconds', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'deleteMessagesAfterMilliseconds', '17': true}, + {'1': 'padding', '3': 5, '4': 1, '5': 12, '10': 'padding'}, + ], + '8': [ + {'1': '_deleteMessagesAfterMilliseconds'}, + ], +}; + +/// Descriptor for `EncryptedGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedGroupStateDescriptor = $convert.base64Decode( + 'ChNFbmNyeXB0ZWRHcm91cFN0YXRlEhwKCW1lbWJlcklkcxgBIAMoA1IJbWVtYmVySWRzEhoKCG' + 'FkbWluSWRzGAIgAygDUghhZG1pbklkcxIcCglncm91cE5hbWUYAyABKAlSCWdyb3VwTmFtZRJN' + 'Ch9kZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgASgDSABSH2RlbGV0ZU1lc3NhZ2' + 'VzQWZ0ZXJNaWxsaXNlY29uZHOIAQESGAoHcGFkZGluZxgFIAEoDFIHcGFkZGluZ0IiCiBfZGVs' + 'ZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcw=='); + +@$core.Deprecated('Use encryptedAppendedGroupStateDescriptor instead') +const EncryptedAppendedGroupState$json = { + '1': 'EncryptedAppendedGroupState', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedAppendedGroupState.Type', '10': 'type'}, + ], + '4': [EncryptedAppendedGroupState_Type$json], +}; + +@$core.Deprecated('Use encryptedAppendedGroupStateDescriptor instead') +const EncryptedAppendedGroupState_Type$json = { + '1': 'Type', + '2': [ + {'1': 'LEFT_GROUP', '2': 0}, + ], +}; + +/// Descriptor for `EncryptedAppendedGroupState`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedAppendedGroupStateDescriptor = $convert.base64Decode( + 'ChtFbmNyeXB0ZWRBcHBlbmRlZEdyb3VwU3RhdGUSNQoEdHlwZRgBIAEoDjIhLkVuY3J5cHRlZE' + 'FwcGVuZGVkR3JvdXBTdGF0ZS5UeXBlUgR0eXBlIhYKBFR5cGUSDgoKTEVGVF9HUk9VUBAA'); + +@$core.Deprecated('Use encryptedGroupStateEnvelopDescriptor instead') +const EncryptedGroupStateEnvelop$json = { + '1': 'EncryptedGroupStateEnvelop', + '2': [ + {'1': 'nonce', '3': 1, '4': 1, '5': 12, '10': 'nonce'}, + {'1': 'encryptedGroupState', '3': 2, '4': 1, '5': 12, '10': 'encryptedGroupState'}, + {'1': 'mac', '3': 3, '4': 1, '5': 12, '10': 'mac'}, + ], +}; + +/// Descriptor for `EncryptedGroupStateEnvelop`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedGroupStateEnvelopDescriptor = $convert.base64Decode( + 'ChpFbmNyeXB0ZWRHcm91cFN0YXRlRW52ZWxvcBIUCgVub25jZRgBIAEoDFIFbm9uY2USMAoTZW' + '5jcnlwdGVkR3JvdXBTdGF0ZRgCIAEoDFITZW5jcnlwdGVkR3JvdXBTdGF0ZRIQCgNtYWMYAyAB' + 'KAxSA21hYw=='); + diff --git a/lib/src/model/protobuf/client/generated/groups.pbserver.dart b/lib/src/model/protobuf/client/generated/groups.pbserver.dart new file mode 100644 index 0000000..087f85c --- /dev/null +++ b/lib/src/model/protobuf/client/generated/groups.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: groups.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'groups.pb.dart'; + diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart new file mode 100644 index 0000000..3babd94 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -0,0 +1,1630 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'messages.pbenum.dart'; + +export 'messages.pbenum.dart'; + +class Message extends $pb.GeneratedMessage { + factory Message({ + Message_Type? type, + $core.String? receiptId, + $core.List<$core.int>? encryptedContent, + PlaintextContent? plaintextContent, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (receiptId != null) { + $result.receiptId = receiptId; + } + if (encryptedContent != null) { + $result.encryptedContent = encryptedContent; + } + if (plaintextContent != null) { + $result.plaintextContent = plaintextContent; + } + return $result; + } + Message._() : super(); + factory Message.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Message.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Message', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: Message_Type.SENDER_DELIVERY_RECEIPT, valueOf: Message_Type.valueOf, enumValues: Message_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'receiptId', protoName: 'receiptId') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'encryptedContent', $pb.PbFieldType.OY, protoName: 'encryptedContent') + ..aOM(4, _omitFieldNames ? '' : 'plaintextContent', protoName: 'plaintextContent', subBuilder: PlaintextContent.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Message clone() => Message()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Message copyWith(void Function(Message) updates) => super.copyWith((message) => updates(message as Message)) as Message; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Message create() => Message._(); + Message createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Message getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Message? _defaultInstance; + + @$pb.TagNumber(1) + Message_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(Message_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get receiptId => $_getSZ(1); + @$pb.TagNumber(2) + set receiptId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasReceiptId() => $_has(1); + @$pb.TagNumber(2) + void clearReceiptId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get encryptedContent => $_getN(2); + @$pb.TagNumber(3) + set encryptedContent($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasEncryptedContent() => $_has(2); + @$pb.TagNumber(3) + void clearEncryptedContent() => clearField(3); + + @$pb.TagNumber(4) + PlaintextContent get plaintextContent => $_getN(3); + @$pb.TagNumber(4) + set plaintextContent(PlaintextContent v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasPlaintextContent() => $_has(3); + @$pb.TagNumber(4) + void clearPlaintextContent() => clearField(4); + @$pb.TagNumber(4) + PlaintextContent ensurePlaintextContent() => $_ensure(3); +} + +class PlaintextContent_RetryErrorMessage extends $pb.GeneratedMessage { + factory PlaintextContent_RetryErrorMessage() => create(); + PlaintextContent_RetryErrorMessage._() : super(); + factory PlaintextContent_RetryErrorMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent_RetryErrorMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent.RetryErrorMessage', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent_RetryErrorMessage clone() => PlaintextContent_RetryErrorMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent_RetryErrorMessage copyWith(void Function(PlaintextContent_RetryErrorMessage) updates) => super.copyWith((message) => updates(message as PlaintextContent_RetryErrorMessage)) as PlaintextContent_RetryErrorMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent_RetryErrorMessage create() => PlaintextContent_RetryErrorMessage._(); + PlaintextContent_RetryErrorMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent_RetryErrorMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent_RetryErrorMessage? _defaultInstance; +} + +class PlaintextContent_DecryptionErrorMessage extends $pb.GeneratedMessage { + factory PlaintextContent_DecryptionErrorMessage({ + PlaintextContent_DecryptionErrorMessage_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + PlaintextContent_DecryptionErrorMessage._() : super(); + factory PlaintextContent_DecryptionErrorMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent_DecryptionErrorMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent.DecryptionErrorMessage', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, valueOf: PlaintextContent_DecryptionErrorMessage_Type.valueOf, enumValues: PlaintextContent_DecryptionErrorMessage_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent_DecryptionErrorMessage clone() => PlaintextContent_DecryptionErrorMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent_DecryptionErrorMessage copyWith(void Function(PlaintextContent_DecryptionErrorMessage) updates) => super.copyWith((message) => updates(message as PlaintextContent_DecryptionErrorMessage)) as PlaintextContent_DecryptionErrorMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent_DecryptionErrorMessage create() => PlaintextContent_DecryptionErrorMessage._(); + PlaintextContent_DecryptionErrorMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent_DecryptionErrorMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent_DecryptionErrorMessage? _defaultInstance; + + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(PlaintextContent_DecryptionErrorMessage_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + +class PlaintextContent extends $pb.GeneratedMessage { + factory PlaintextContent({ + PlaintextContent_DecryptionErrorMessage? decryptionErrorMessage, + PlaintextContent_RetryErrorMessage? retryControlError, + }) { + final $result = create(); + if (decryptionErrorMessage != null) { + $result.decryptionErrorMessage = decryptionErrorMessage; + } + if (retryControlError != null) { + $result.retryControlError = retryControlError; + } + return $result; + } + PlaintextContent._() : super(); + factory PlaintextContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PlaintextContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PlaintextContent', createEmptyInstance: create) + ..aOM(1, _omitFieldNames ? '' : 'decryptionErrorMessage', protoName: 'decryptionErrorMessage', subBuilder: PlaintextContent_DecryptionErrorMessage.create) + ..aOM(2, _omitFieldNames ? '' : 'retryControlError', protoName: 'retryControlError', subBuilder: PlaintextContent_RetryErrorMessage.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PlaintextContent clone() => PlaintextContent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PlaintextContent copyWith(void Function(PlaintextContent) updates) => super.copyWith((message) => updates(message as PlaintextContent)) as PlaintextContent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PlaintextContent create() => PlaintextContent._(); + PlaintextContent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PlaintextContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PlaintextContent? _defaultInstance; + + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage get decryptionErrorMessage => $_getN(0); + @$pb.TagNumber(1) + set decryptionErrorMessage(PlaintextContent_DecryptionErrorMessage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDecryptionErrorMessage() => $_has(0); + @$pb.TagNumber(1) + void clearDecryptionErrorMessage() => clearField(1); + @$pb.TagNumber(1) + PlaintextContent_DecryptionErrorMessage ensureDecryptionErrorMessage() => $_ensure(0); + + @$pb.TagNumber(2) + PlaintextContent_RetryErrorMessage get retryControlError => $_getN(1); + @$pb.TagNumber(2) + set retryControlError(PlaintextContent_RetryErrorMessage v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRetryControlError() => $_has(1); + @$pb.TagNumber(2) + void clearRetryControlError() => clearField(2); + @$pb.TagNumber(2) + PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1); +} + +class EncryptedContent_GroupCreate extends $pb.GeneratedMessage { + factory EncryptedContent_GroupCreate({ + $core.List<$core.int>? stateKey, + $core.List<$core.int>? groupPublicKey, + }) { + final $result = create(); + if (stateKey != null) { + $result.stateKey = stateKey; + } + if (groupPublicKey != null) { + $result.groupPublicKey = groupPublicKey; + } + return $result; + } + EncryptedContent_GroupCreate._() : super(); + factory EncryptedContent_GroupCreate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupCreate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupCreate', createEmptyInstance: create) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY, protoName: 'stateKey') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupCreate clone() => EncryptedContent_GroupCreate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupCreate copyWith(void Function(EncryptedContent_GroupCreate) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupCreate)) as EncryptedContent_GroupCreate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupCreate create() => EncryptedContent_GroupCreate._(); + EncryptedContent_GroupCreate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupCreate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupCreate? _defaultInstance; + + /// key for the state stored on the server + @$pb.TagNumber(3) + $core.List<$core.int> get stateKey => $_getN(0); + @$pb.TagNumber(3) + set stateKey($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(3) + $core.bool hasStateKey() => $_has(0); + @$pb.TagNumber(3) + void clearStateKey() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get groupPublicKey => $_getN(1); + @$pb.TagNumber(4) + set groupPublicKey($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(4) + $core.bool hasGroupPublicKey() => $_has(1); + @$pb.TagNumber(4) + void clearGroupPublicKey() => clearField(4); +} + +class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { + factory EncryptedContent_GroupJoin({ + $core.List<$core.int>? groupPublicKey, + }) { + final $result = create(); + if (groupPublicKey != null) { + $result.groupPublicKey = groupPublicKey; + } + return $result; + } + EncryptedContent_GroupJoin._() : super(); + factory EncryptedContent_GroupJoin.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupJoin.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupJoin', createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY, protoName: 'groupPublicKey') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupJoin clone() => EncryptedContent_GroupJoin()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupJoin copyWith(void Function(EncryptedContent_GroupJoin) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupJoin)) as EncryptedContent_GroupJoin; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupJoin create() => EncryptedContent_GroupJoin._(); + EncryptedContent_GroupJoin createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupJoin getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupJoin? _defaultInstance; + + /// key for the state stored on the server + @$pb.TagNumber(1) + $core.List<$core.int> get groupPublicKey => $_getN(0); + @$pb.TagNumber(1) + set groupPublicKey($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasGroupPublicKey() => $_has(0); + @$pb.TagNumber(1) + void clearGroupPublicKey() => clearField(1); +} + +class EncryptedContent_ResendGroupPublicKey extends $pb.GeneratedMessage { + factory EncryptedContent_ResendGroupPublicKey() => create(); + EncryptedContent_ResendGroupPublicKey._() : super(); + factory EncryptedContent_ResendGroupPublicKey.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ResendGroupPublicKey.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ResendGroupPublicKey', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ResendGroupPublicKey clone() => EncryptedContent_ResendGroupPublicKey()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ResendGroupPublicKey copyWith(void Function(EncryptedContent_ResendGroupPublicKey) updates) => super.copyWith((message) => updates(message as EncryptedContent_ResendGroupPublicKey)) as EncryptedContent_ResendGroupPublicKey; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ResendGroupPublicKey create() => EncryptedContent_ResendGroupPublicKey._(); + EncryptedContent_ResendGroupPublicKey createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ResendGroupPublicKey getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ResendGroupPublicKey? _defaultInstance; +} + +class EncryptedContent_GroupUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_GroupUpdate({ + $core.String? groupActionType, + $fixnum.Int64? affectedContactId, + $core.String? newGroupName, + $fixnum.Int64? newDeleteMessagesAfterMilliseconds, + }) { + final $result = create(); + if (groupActionType != null) { + $result.groupActionType = groupActionType; + } + if (affectedContactId != null) { + $result.affectedContactId = affectedContactId; + } + if (newGroupName != null) { + $result.newGroupName = newGroupName; + } + if (newDeleteMessagesAfterMilliseconds != null) { + $result.newDeleteMessagesAfterMilliseconds = newDeleteMessagesAfterMilliseconds; + } + return $result; + } + EncryptedContent_GroupUpdate._() : super(); + factory EncryptedContent_GroupUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_GroupUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.GroupUpdate', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'groupActionType', protoName: 'groupActionType') + ..aInt64(2, _omitFieldNames ? '' : 'affectedContactId', protoName: 'affectedContactId') + ..aOS(3, _omitFieldNames ? '' : 'newGroupName', protoName: 'newGroupName') + ..aInt64(4, _omitFieldNames ? '' : 'newDeleteMessagesAfterMilliseconds', protoName: 'newDeleteMessagesAfterMilliseconds') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupUpdate clone() => EncryptedContent_GroupUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_GroupUpdate copyWith(void Function(EncryptedContent_GroupUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_GroupUpdate)) as EncryptedContent_GroupUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupUpdate create() => EncryptedContent_GroupUpdate._(); + EncryptedContent_GroupUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_GroupUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_GroupUpdate? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get groupActionType => $_getSZ(0); + @$pb.TagNumber(1) + set groupActionType($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasGroupActionType() => $_has(0); + @$pb.TagNumber(1) + void clearGroupActionType() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get affectedContactId => $_getI64(1); + @$pb.TagNumber(2) + set affectedContactId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasAffectedContactId() => $_has(1); + @$pb.TagNumber(2) + void clearAffectedContactId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get newGroupName => $_getSZ(2); + @$pb.TagNumber(3) + set newGroupName($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasNewGroupName() => $_has(2); + @$pb.TagNumber(3) + void clearNewGroupName() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get newDeleteMessagesAfterMilliseconds => $_getI64(3); + @$pb.TagNumber(4) + set newDeleteMessagesAfterMilliseconds($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasNewDeleteMessagesAfterMilliseconds() => $_has(3); + @$pb.TagNumber(4) + void clearNewDeleteMessagesAfterMilliseconds() => clearField(4); +} + +class EncryptedContent_TextMessage extends $pb.GeneratedMessage { + factory EncryptedContent_TextMessage({ + $core.String? senderMessageId, + $core.String? text, + $fixnum.Int64? timestamp, + $core.String? quoteMessageId, + }) { + final $result = create(); + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (text != null) { + $result.text = text; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + if (quoteMessageId != null) { + $result.quoteMessageId = quoteMessageId; + } + return $result; + } + EncryptedContent_TextMessage._() : super(); + factory EncryptedContent_TextMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_TextMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.TextMessage', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..aOS(2, _omitFieldNames ? '' : 'text') + ..aInt64(3, _omitFieldNames ? '' : 'timestamp') + ..aOS(4, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_TextMessage clone() => EncryptedContent_TextMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_TextMessage copyWith(void Function(EncryptedContent_TextMessage) updates) => super.copyWith((message) => updates(message as EncryptedContent_TextMessage)) as EncryptedContent_TextMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_TextMessage create() => EncryptedContent_TextMessage._(); + EncryptedContent_TextMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_TextMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_TextMessage? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get senderMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set senderMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasSenderMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearSenderMessageId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get text => $_getSZ(1); + @$pb.TagNumber(2) + set text($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasText() => $_has(1); + @$pb.TagNumber(2) + void clearText() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get timestamp => $_getI64(2); + @$pb.TagNumber(3) + set timestamp($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasTimestamp() => $_has(2); + @$pb.TagNumber(3) + void clearTimestamp() => clearField(3); + + @$pb.TagNumber(4) + $core.String get quoteMessageId => $_getSZ(3); + @$pb.TagNumber(4) + set quoteMessageId($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasQuoteMessageId() => $_has(3); + @$pb.TagNumber(4) + void clearQuoteMessageId() => clearField(4); +} + +class EncryptedContent_Reaction extends $pb.GeneratedMessage { + factory EncryptedContent_Reaction({ + $core.String? targetMessageId, + $core.String? emoji, + $core.bool? remove, + }) { + final $result = create(); + if (targetMessageId != null) { + $result.targetMessageId = targetMessageId; + } + if (emoji != null) { + $result.emoji = emoji; + } + if (remove != null) { + $result.remove = remove; + } + return $result; + } + EncryptedContent_Reaction._() : super(); + factory EncryptedContent_Reaction.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_Reaction.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Reaction', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') + ..aOS(2, _omitFieldNames ? '' : 'emoji') + ..aOB(3, _omitFieldNames ? '' : 'remove') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_Reaction clone() => EncryptedContent_Reaction()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_Reaction copyWith(void Function(EncryptedContent_Reaction) updates) => super.copyWith((message) => updates(message as EncryptedContent_Reaction)) as EncryptedContent_Reaction; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_Reaction create() => EncryptedContent_Reaction._(); + EncryptedContent_Reaction createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_Reaction getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_Reaction? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get targetMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set targetMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTargetMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearTargetMessageId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get emoji => $_getSZ(1); + @$pb.TagNumber(2) + set emoji($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasEmoji() => $_has(1); + @$pb.TagNumber(2) + void clearEmoji() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get remove => $_getBF(2); + @$pb.TagNumber(3) + set remove($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasRemove() => $_has(2); + @$pb.TagNumber(3) + void clearRemove() => clearField(3); +} + +class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_MessageUpdate({ + EncryptedContent_MessageUpdate_Type? type, + $core.String? senderMessageId, + $core.Iterable<$core.String>? multipleTargetMessageIds, + $core.String? text, + $fixnum.Int64? timestamp, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (multipleTargetMessageIds != null) { + $result.multipleTargetMessageIds.addAll(multipleTargetMessageIds); + } + if (text != null) { + $result.text = text; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + return $result; + } + EncryptedContent_MessageUpdate._() : super(); + factory EncryptedContent_MessageUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_MessageUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..pPS(3, _omitFieldNames ? '' : 'multipleTargetMessageIds', protoName: 'multipleTargetMessageIds') + ..aOS(4, _omitFieldNames ? '' : 'text') + ..aInt64(5, _omitFieldNames ? '' : 'timestamp') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_MessageUpdate clone() => EncryptedContent_MessageUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_MessageUpdate copyWith(void Function(EncryptedContent_MessageUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_MessageUpdate)) as EncryptedContent_MessageUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_MessageUpdate create() => EncryptedContent_MessageUpdate._(); + EncryptedContent_MessageUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_MessageUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_MessageUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_MessageUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_MessageUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get senderMessageId => $_getSZ(1); + @$pb.TagNumber(2) + set senderMessageId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasSenderMessageId() => $_has(1); + @$pb.TagNumber(2) + void clearSenderMessageId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.String> get multipleTargetMessageIds => $_getList(2); + + @$pb.TagNumber(4) + $core.String get text => $_getSZ(3); + @$pb.TagNumber(4) + set text($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasText() => $_has(3); + @$pb.TagNumber(4) + void clearText() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get timestamp => $_getI64(4); + @$pb.TagNumber(5) + set timestamp($fixnum.Int64 v) { $_setInt64(4, v); } + @$pb.TagNumber(5) + $core.bool hasTimestamp() => $_has(4); + @$pb.TagNumber(5) + void clearTimestamp() => clearField(5); +} + +class EncryptedContent_Media extends $pb.GeneratedMessage { + factory EncryptedContent_Media({ + $core.String? senderMessageId, + EncryptedContent_Media_Type? type, + $fixnum.Int64? displayLimitInMilliseconds, + $core.bool? requiresAuthentication, + $fixnum.Int64? timestamp, + $core.String? quoteMessageId, + $core.List<$core.int>? downloadToken, + $core.List<$core.int>? encryptionKey, + $core.List<$core.int>? encryptionMac, + $core.List<$core.int>? encryptionNonce, + }) { + final $result = create(); + if (senderMessageId != null) { + $result.senderMessageId = senderMessageId; + } + if (type != null) { + $result.type = type; + } + if (displayLimitInMilliseconds != null) { + $result.displayLimitInMilliseconds = displayLimitInMilliseconds; + } + if (requiresAuthentication != null) { + $result.requiresAuthentication = requiresAuthentication; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + if (quoteMessageId != null) { + $result.quoteMessageId = quoteMessageId; + } + if (downloadToken != null) { + $result.downloadToken = downloadToken; + } + if (encryptionKey != null) { + $result.encryptionKey = encryptionKey; + } + if (encryptionMac != null) { + $result.encryptionMac = encryptionMac; + } + if (encryptionNonce != null) { + $result.encryptionNonce = encryptionNonce; + } + return $result; + } + EncryptedContent_Media._() : super(); + factory EncryptedContent_Media.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_Media.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Media', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') + ..e(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.REUPLOAD, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values) + ..aInt64(3, _omitFieldNames ? '' : 'displayLimitInMilliseconds', protoName: 'displayLimitInMilliseconds') + ..aOB(4, _omitFieldNames ? '' : 'requiresAuthentication', protoName: 'requiresAuthentication') + ..aInt64(5, _omitFieldNames ? '' : 'timestamp') + ..aOS(6, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId') + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken') + ..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey') + ..a<$core.List<$core.int>>(9, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac') + ..a<$core.List<$core.int>>(10, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_Media clone() => EncryptedContent_Media()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_Media copyWith(void Function(EncryptedContent_Media) updates) => super.copyWith((message) => updates(message as EncryptedContent_Media)) as EncryptedContent_Media; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_Media create() => EncryptedContent_Media._(); + EncryptedContent_Media createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_Media getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_Media? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get senderMessageId => $_getSZ(0); + @$pb.TagNumber(1) + set senderMessageId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasSenderMessageId() => $_has(0); + @$pb.TagNumber(1) + void clearSenderMessageId() => clearField(1); + + @$pb.TagNumber(2) + EncryptedContent_Media_Type get type => $_getN(1); + @$pb.TagNumber(2) + set type(EncryptedContent_Media_Type v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasType() => $_has(1); + @$pb.TagNumber(2) + void clearType() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get displayLimitInMilliseconds => $_getI64(2); + @$pb.TagNumber(3) + set displayLimitInMilliseconds($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasDisplayLimitInMilliseconds() => $_has(2); + @$pb.TagNumber(3) + void clearDisplayLimitInMilliseconds() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get requiresAuthentication => $_getBF(3); + @$pb.TagNumber(4) + set requiresAuthentication($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasRequiresAuthentication() => $_has(3); + @$pb.TagNumber(4) + void clearRequiresAuthentication() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get timestamp => $_getI64(4); + @$pb.TagNumber(5) + set timestamp($fixnum.Int64 v) { $_setInt64(4, v); } + @$pb.TagNumber(5) + $core.bool hasTimestamp() => $_has(4); + @$pb.TagNumber(5) + void clearTimestamp() => clearField(5); + + @$pb.TagNumber(6) + $core.String get quoteMessageId => $_getSZ(5); + @$pb.TagNumber(6) + set quoteMessageId($core.String v) { $_setString(5, v); } + @$pb.TagNumber(6) + $core.bool hasQuoteMessageId() => $_has(5); + @$pb.TagNumber(6) + void clearQuoteMessageId() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get downloadToken => $_getN(6); + @$pb.TagNumber(7) + set downloadToken($core.List<$core.int> v) { $_setBytes(6, v); } + @$pb.TagNumber(7) + $core.bool hasDownloadToken() => $_has(6); + @$pb.TagNumber(7) + void clearDownloadToken() => clearField(7); + + @$pb.TagNumber(8) + $core.List<$core.int> get encryptionKey => $_getN(7); + @$pb.TagNumber(8) + set encryptionKey($core.List<$core.int> v) { $_setBytes(7, v); } + @$pb.TagNumber(8) + $core.bool hasEncryptionKey() => $_has(7); + @$pb.TagNumber(8) + void clearEncryptionKey() => clearField(8); + + @$pb.TagNumber(9) + $core.List<$core.int> get encryptionMac => $_getN(8); + @$pb.TagNumber(9) + set encryptionMac($core.List<$core.int> v) { $_setBytes(8, v); } + @$pb.TagNumber(9) + $core.bool hasEncryptionMac() => $_has(8); + @$pb.TagNumber(9) + void clearEncryptionMac() => clearField(9); + + @$pb.TagNumber(10) + $core.List<$core.int> get encryptionNonce => $_getN(9); + @$pb.TagNumber(10) + set encryptionNonce($core.List<$core.int> v) { $_setBytes(9, v); } + @$pb.TagNumber(10) + $core.bool hasEncryptionNonce() => $_has(9); + @$pb.TagNumber(10) + void clearEncryptionNonce() => clearField(10); +} + +class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_MediaUpdate({ + EncryptedContent_MediaUpdate_Type? type, + $core.String? targetMessageId, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (targetMessageId != null) { + $result.targetMessageId = targetMessageId; + } + return $result; + } + EncryptedContent_MediaUpdate._() : super(); + factory EncryptedContent_MediaUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_MediaUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values) + ..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_MediaUpdate clone() => EncryptedContent_MediaUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_MediaUpdate copyWith(void Function(EncryptedContent_MediaUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_MediaUpdate)) as EncryptedContent_MediaUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_MediaUpdate create() => EncryptedContent_MediaUpdate._(); + EncryptedContent_MediaUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_MediaUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_MediaUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_MediaUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_MediaUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.String get targetMessageId => $_getSZ(1); + @$pb.TagNumber(2) + set targetMessageId($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasTargetMessageId() => $_has(1); + @$pb.TagNumber(2) + void clearTargetMessageId() => clearField(2); +} + +class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { + factory EncryptedContent_ContactRequest({ + EncryptedContent_ContactRequest_Type? type, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + return $result; + } + EncryptedContent_ContactRequest._() : super(); + factory EncryptedContent_ContactRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ContactRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactRequest', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactRequest_Type.REQUEST, valueOf: EncryptedContent_ContactRequest_Type.valueOf, enumValues: EncryptedContent_ContactRequest_Type.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactRequest clone() => EncryptedContent_ContactRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactRequest copyWith(void Function(EncryptedContent_ContactRequest) updates) => super.copyWith((message) => updates(message as EncryptedContent_ContactRequest)) as EncryptedContent_ContactRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactRequest create() => EncryptedContent_ContactRequest._(); + EncryptedContent_ContactRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ContactRequest? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_ContactRequest_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_ContactRequest_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); +} + +class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { + factory EncryptedContent_ContactUpdate({ + EncryptedContent_ContactUpdate_Type? type, + $core.List<$core.int>? avatarSvgCompressed, + $core.String? username, + $core.String? displayName, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (avatarSvgCompressed != null) { + $result.avatarSvgCompressed = avatarSvgCompressed; + } + if (username != null) { + $result.username = username; + } + if (displayName != null) { + $result.displayName = displayName; + } + return $result; + } + EncryptedContent_ContactUpdate._() : super(); + factory EncryptedContent_ContactUpdate.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_ContactUpdate.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.ContactUpdate', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_ContactUpdate_Type.REQUEST, valueOf: EncryptedContent_ContactUpdate_Type.valueOf, enumValues: EncryptedContent_ContactUpdate_Type.values) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY, protoName: 'avatarSvgCompressed') + ..aOS(3, _omitFieldNames ? '' : 'username') + ..aOS(4, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactUpdate clone() => EncryptedContent_ContactUpdate()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_ContactUpdate copyWith(void Function(EncryptedContent_ContactUpdate) updates) => super.copyWith((message) => updates(message as EncryptedContent_ContactUpdate)) as EncryptedContent_ContactUpdate; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactUpdate create() => EncryptedContent_ContactUpdate._(); + EncryptedContent_ContactUpdate createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_ContactUpdate getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_ContactUpdate? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_ContactUpdate_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_ContactUpdate_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get avatarSvgCompressed => $_getN(1); + @$pb.TagNumber(2) + set avatarSvgCompressed($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasAvatarSvgCompressed() => $_has(1); + @$pb.TagNumber(2) + void clearAvatarSvgCompressed() => clearField(2); + + @$pb.TagNumber(3) + $core.String get username => $_getSZ(2); + @$pb.TagNumber(3) + set username($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasUsername() => $_has(2); + @$pb.TagNumber(3) + void clearUsername() => clearField(3); + + @$pb.TagNumber(4) + $core.String get displayName => $_getSZ(3); + @$pb.TagNumber(4) + set displayName($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasDisplayName() => $_has(3); + @$pb.TagNumber(4) + void clearDisplayName() => clearField(4); +} + +class EncryptedContent_PushKeys extends $pb.GeneratedMessage { + factory EncryptedContent_PushKeys({ + EncryptedContent_PushKeys_Type? type, + $fixnum.Int64? keyId, + $core.List<$core.int>? key, + $fixnum.Int64? createdAt, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (keyId != null) { + $result.keyId = keyId; + } + if (key != null) { + $result.key = key; + } + if (createdAt != null) { + $result.createdAt = createdAt; + } + return $result; + } + EncryptedContent_PushKeys._() : super(); + factory EncryptedContent_PushKeys.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_PushKeys.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.PushKeys', createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_PushKeys_Type.REQUEST, valueOf: EncryptedContent_PushKeys_Type.valueOf, enumValues: EncryptedContent_PushKeys_Type.values) + ..aInt64(2, _omitFieldNames ? '' : 'keyId', protoName: 'keyId') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY) + ..aInt64(4, _omitFieldNames ? '' : 'createdAt', protoName: 'createdAt') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_PushKeys clone() => EncryptedContent_PushKeys()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_PushKeys copyWith(void Function(EncryptedContent_PushKeys) updates) => super.copyWith((message) => updates(message as EncryptedContent_PushKeys)) as EncryptedContent_PushKeys; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_PushKeys create() => EncryptedContent_PushKeys._(); + EncryptedContent_PushKeys createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_PushKeys getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_PushKeys? _defaultInstance; + + @$pb.TagNumber(1) + EncryptedContent_PushKeys_Type get type => $_getN(0); + @$pb.TagNumber(1) + set type(EncryptedContent_PushKeys_Type v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get keyId => $_getI64(1); + @$pb.TagNumber(2) + set keyId($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasKeyId() => $_has(1); + @$pb.TagNumber(2) + void clearKeyId() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get key => $_getN(2); + @$pb.TagNumber(3) + set key($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasKey() => $_has(2); + @$pb.TagNumber(3) + void clearKey() => clearField(3); + + @$pb.TagNumber(4) + $fixnum.Int64 get createdAt => $_getI64(3); + @$pb.TagNumber(4) + set createdAt($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasCreatedAt() => $_has(3); + @$pb.TagNumber(4) + void clearCreatedAt() => clearField(4); +} + +class EncryptedContent_FlameSync extends $pb.GeneratedMessage { + factory EncryptedContent_FlameSync({ + $fixnum.Int64? flameCounter, + $fixnum.Int64? lastFlameCounterChange, + $core.bool? bestFriend, + $core.bool? forceUpdate, + }) { + final $result = create(); + if (flameCounter != null) { + $result.flameCounter = flameCounter; + } + if (lastFlameCounterChange != null) { + $result.lastFlameCounterChange = lastFlameCounterChange; + } + if (bestFriend != null) { + $result.bestFriend = bestFriend; + } + if (forceUpdate != null) { + $result.forceUpdate = forceUpdate; + } + return $result; + } + EncryptedContent_FlameSync._() : super(); + factory EncryptedContent_FlameSync.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent_FlameSync.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.FlameSync', createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter') + ..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange') + ..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend') + ..aOB(4, _omitFieldNames ? '' : 'forceUpdate', protoName: 'forceUpdate') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent_FlameSync clone() => EncryptedContent_FlameSync()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent_FlameSync copyWith(void Function(EncryptedContent_FlameSync) updates) => super.copyWith((message) => updates(message as EncryptedContent_FlameSync)) as EncryptedContent_FlameSync; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent_FlameSync create() => EncryptedContent_FlameSync._(); + EncryptedContent_FlameSync createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent_FlameSync getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent_FlameSync? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get flameCounter => $_getI64(0); + @$pb.TagNumber(1) + set flameCounter($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasFlameCounter() => $_has(0); + @$pb.TagNumber(1) + void clearFlameCounter() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get lastFlameCounterChange => $_getI64(1); + @$pb.TagNumber(2) + set lastFlameCounterChange($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasLastFlameCounterChange() => $_has(1); + @$pb.TagNumber(2) + void clearLastFlameCounterChange() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get bestFriend => $_getBF(2); + @$pb.TagNumber(3) + set bestFriend($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasBestFriend() => $_has(2); + @$pb.TagNumber(3) + void clearBestFriend() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get forceUpdate => $_getBF(3); + @$pb.TagNumber(4) + set forceUpdate($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasForceUpdate() => $_has(3); + @$pb.TagNumber(4) + void clearForceUpdate() => clearField(4); +} + +class EncryptedContent extends $pb.GeneratedMessage { + factory EncryptedContent({ + $core.String? groupId, + $core.bool? isDirectChat, + $fixnum.Int64? senderProfileCounter, + EncryptedContent_MessageUpdate? messageUpdate, + EncryptedContent_Media? media, + EncryptedContent_MediaUpdate? mediaUpdate, + EncryptedContent_ContactUpdate? contactUpdate, + EncryptedContent_ContactRequest? contactRequest, + EncryptedContent_FlameSync? flameSync, + EncryptedContent_PushKeys? pushKeys, + EncryptedContent_Reaction? reaction, + EncryptedContent_TextMessage? textMessage, + EncryptedContent_GroupCreate? groupCreate, + EncryptedContent_GroupJoin? groupJoin, + EncryptedContent_GroupUpdate? groupUpdate, + EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, + }) { + final $result = create(); + if (groupId != null) { + $result.groupId = groupId; + } + if (isDirectChat != null) { + $result.isDirectChat = isDirectChat; + } + if (senderProfileCounter != null) { + $result.senderProfileCounter = senderProfileCounter; + } + if (messageUpdate != null) { + $result.messageUpdate = messageUpdate; + } + if (media != null) { + $result.media = media; + } + if (mediaUpdate != null) { + $result.mediaUpdate = mediaUpdate; + } + if (contactUpdate != null) { + $result.contactUpdate = contactUpdate; + } + if (contactRequest != null) { + $result.contactRequest = contactRequest; + } + if (flameSync != null) { + $result.flameSync = flameSync; + } + if (pushKeys != null) { + $result.pushKeys = pushKeys; + } + if (reaction != null) { + $result.reaction = reaction; + } + if (textMessage != null) { + $result.textMessage = textMessage; + } + if (groupCreate != null) { + $result.groupCreate = groupCreate; + } + if (groupJoin != null) { + $result.groupJoin = groupJoin; + } + if (groupUpdate != null) { + $result.groupUpdate = groupUpdate; + } + if (resendGroupPublicKey != null) { + $result.resendGroupPublicKey = resendGroupPublicKey; + } + return $result; + } + EncryptedContent._() : super(); + factory EncryptedContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EncryptedContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent', createEmptyInstance: create) + ..aOS(2, _omitFieldNames ? '' : 'groupId', protoName: 'groupId') + ..aOB(3, _omitFieldNames ? '' : 'isDirectChat', protoName: 'isDirectChat') + ..aInt64(4, _omitFieldNames ? '' : 'senderProfileCounter', protoName: 'senderProfileCounter') + ..aOM(5, _omitFieldNames ? '' : 'messageUpdate', protoName: 'messageUpdate', subBuilder: EncryptedContent_MessageUpdate.create) + ..aOM(6, _omitFieldNames ? '' : 'media', subBuilder: EncryptedContent_Media.create) + ..aOM(7, _omitFieldNames ? '' : 'mediaUpdate', protoName: 'mediaUpdate', subBuilder: EncryptedContent_MediaUpdate.create) + ..aOM(8, _omitFieldNames ? '' : 'contactUpdate', protoName: 'contactUpdate', subBuilder: EncryptedContent_ContactUpdate.create) + ..aOM(9, _omitFieldNames ? '' : 'contactRequest', protoName: 'contactRequest', subBuilder: EncryptedContent_ContactRequest.create) + ..aOM(10, _omitFieldNames ? '' : 'flameSync', protoName: 'flameSync', subBuilder: EncryptedContent_FlameSync.create) + ..aOM(11, _omitFieldNames ? '' : 'pushKeys', protoName: 'pushKeys', subBuilder: EncryptedContent_PushKeys.create) + ..aOM(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create) + ..aOM(13, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', subBuilder: EncryptedContent_TextMessage.create) + ..aOM(14, _omitFieldNames ? '' : 'groupCreate', protoName: 'groupCreate', subBuilder: EncryptedContent_GroupCreate.create) + ..aOM(15, _omitFieldNames ? '' : 'groupJoin', protoName: 'groupJoin', subBuilder: EncryptedContent_GroupJoin.create) + ..aOM(16, _omitFieldNames ? '' : 'groupUpdate', protoName: 'groupUpdate', subBuilder: EncryptedContent_GroupUpdate.create) + ..aOM(17, _omitFieldNames ? '' : 'resendGroupPublicKey', protoName: 'resendGroupPublicKey', subBuilder: EncryptedContent_ResendGroupPublicKey.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EncryptedContent clone() => EncryptedContent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EncryptedContent copyWith(void Function(EncryptedContent) updates) => super.copyWith((message) => updates(message as EncryptedContent)) as EncryptedContent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EncryptedContent create() => EncryptedContent._(); + EncryptedContent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EncryptedContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EncryptedContent? _defaultInstance; + + @$pb.TagNumber(2) + $core.String get groupId => $_getSZ(0); + @$pb.TagNumber(2) + set groupId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(2) + $core.bool hasGroupId() => $_has(0); + @$pb.TagNumber(2) + void clearGroupId() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get isDirectChat => $_getBF(1); + @$pb.TagNumber(3) + set isDirectChat($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(3) + $core.bool hasIsDirectChat() => $_has(1); + @$pb.TagNumber(3) + void clearIsDirectChat() => clearField(3); + + /// / This can be added, so the receiver can check weather he is up to date with the current profile + @$pb.TagNumber(4) + $fixnum.Int64 get senderProfileCounter => $_getI64(2); + @$pb.TagNumber(4) + set senderProfileCounter($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(4) + $core.bool hasSenderProfileCounter() => $_has(2); + @$pb.TagNumber(4) + void clearSenderProfileCounter() => clearField(4); + + @$pb.TagNumber(5) + EncryptedContent_MessageUpdate get messageUpdate => $_getN(3); + @$pb.TagNumber(5) + set messageUpdate(EncryptedContent_MessageUpdate v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasMessageUpdate() => $_has(3); + @$pb.TagNumber(5) + void clearMessageUpdate() => clearField(5); + @$pb.TagNumber(5) + EncryptedContent_MessageUpdate ensureMessageUpdate() => $_ensure(3); + + @$pb.TagNumber(6) + EncryptedContent_Media get media => $_getN(4); + @$pb.TagNumber(6) + set media(EncryptedContent_Media v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasMedia() => $_has(4); + @$pb.TagNumber(6) + void clearMedia() => clearField(6); + @$pb.TagNumber(6) + EncryptedContent_Media ensureMedia() => $_ensure(4); + + @$pb.TagNumber(7) + EncryptedContent_MediaUpdate get mediaUpdate => $_getN(5); + @$pb.TagNumber(7) + set mediaUpdate(EncryptedContent_MediaUpdate v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasMediaUpdate() => $_has(5); + @$pb.TagNumber(7) + void clearMediaUpdate() => clearField(7); + @$pb.TagNumber(7) + EncryptedContent_MediaUpdate ensureMediaUpdate() => $_ensure(5); + + @$pb.TagNumber(8) + EncryptedContent_ContactUpdate get contactUpdate => $_getN(6); + @$pb.TagNumber(8) + set contactUpdate(EncryptedContent_ContactUpdate v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasContactUpdate() => $_has(6); + @$pb.TagNumber(8) + void clearContactUpdate() => clearField(8); + @$pb.TagNumber(8) + EncryptedContent_ContactUpdate ensureContactUpdate() => $_ensure(6); + + @$pb.TagNumber(9) + EncryptedContent_ContactRequest get contactRequest => $_getN(7); + @$pb.TagNumber(9) + set contactRequest(EncryptedContent_ContactRequest v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasContactRequest() => $_has(7); + @$pb.TagNumber(9) + void clearContactRequest() => clearField(9); + @$pb.TagNumber(9) + EncryptedContent_ContactRequest ensureContactRequest() => $_ensure(7); + + @$pb.TagNumber(10) + EncryptedContent_FlameSync get flameSync => $_getN(8); + @$pb.TagNumber(10) + set flameSync(EncryptedContent_FlameSync v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasFlameSync() => $_has(8); + @$pb.TagNumber(10) + void clearFlameSync() => clearField(10); + @$pb.TagNumber(10) + EncryptedContent_FlameSync ensureFlameSync() => $_ensure(8); + + @$pb.TagNumber(11) + EncryptedContent_PushKeys get pushKeys => $_getN(9); + @$pb.TagNumber(11) + set pushKeys(EncryptedContent_PushKeys v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasPushKeys() => $_has(9); + @$pb.TagNumber(11) + void clearPushKeys() => clearField(11); + @$pb.TagNumber(11) + EncryptedContent_PushKeys ensurePushKeys() => $_ensure(9); + + @$pb.TagNumber(12) + EncryptedContent_Reaction get reaction => $_getN(10); + @$pb.TagNumber(12) + set reaction(EncryptedContent_Reaction v) { setField(12, v); } + @$pb.TagNumber(12) + $core.bool hasReaction() => $_has(10); + @$pb.TagNumber(12) + void clearReaction() => clearField(12); + @$pb.TagNumber(12) + EncryptedContent_Reaction ensureReaction() => $_ensure(10); + + @$pb.TagNumber(13) + EncryptedContent_TextMessage get textMessage => $_getN(11); + @$pb.TagNumber(13) + set textMessage(EncryptedContent_TextMessage v) { setField(13, v); } + @$pb.TagNumber(13) + $core.bool hasTextMessage() => $_has(11); + @$pb.TagNumber(13) + void clearTextMessage() => clearField(13); + @$pb.TagNumber(13) + EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11); + + @$pb.TagNumber(14) + EncryptedContent_GroupCreate get groupCreate => $_getN(12); + @$pb.TagNumber(14) + set groupCreate(EncryptedContent_GroupCreate v) { setField(14, v); } + @$pb.TagNumber(14) + $core.bool hasGroupCreate() => $_has(12); + @$pb.TagNumber(14) + void clearGroupCreate() => clearField(14); + @$pb.TagNumber(14) + EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(12); + + @$pb.TagNumber(15) + EncryptedContent_GroupJoin get groupJoin => $_getN(13); + @$pb.TagNumber(15) + set groupJoin(EncryptedContent_GroupJoin v) { setField(15, v); } + @$pb.TagNumber(15) + $core.bool hasGroupJoin() => $_has(13); + @$pb.TagNumber(15) + void clearGroupJoin() => clearField(15); + @$pb.TagNumber(15) + EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(13); + + @$pb.TagNumber(16) + EncryptedContent_GroupUpdate get groupUpdate => $_getN(14); + @$pb.TagNumber(16) + set groupUpdate(EncryptedContent_GroupUpdate v) { setField(16, v); } + @$pb.TagNumber(16) + $core.bool hasGroupUpdate() => $_has(14); + @$pb.TagNumber(16) + void clearGroupUpdate() => clearField(16); + @$pb.TagNumber(16) + EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14); + + @$pb.TagNumber(17) + EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(15); + @$pb.TagNumber(17) + set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey v) { setField(17, v); } + @$pb.TagNumber(17) + $core.bool hasResendGroupPublicKey() => $_has(15); + @$pb.TagNumber(17) + void clearResendGroupPublicKey() => clearField(17); + @$pb.TagNumber(17) + EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() => $_ensure(15); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart new file mode 100644 index 0000000..11a6701 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -0,0 +1,155 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class Message_Type extends $pb.ProtobufEnum { + static const Message_Type SENDER_DELIVERY_RECEIPT = Message_Type._(0, _omitEnumNames ? '' : 'SENDER_DELIVERY_RECEIPT'); + static const Message_Type PLAINTEXT_CONTENT = Message_Type._(1, _omitEnumNames ? '' : 'PLAINTEXT_CONTENT'); + static const Message_Type CIPHERTEXT = Message_Type._(2, _omitEnumNames ? '' : 'CIPHERTEXT'); + static const Message_Type PREKEY_BUNDLE = Message_Type._(3, _omitEnumNames ? '' : 'PREKEY_BUNDLE'); + static const Message_Type TEST_NOTIFICATION = Message_Type._(4, _omitEnumNames ? '' : 'TEST_NOTIFICATION'); + + static const $core.List values = [ + SENDER_DELIVERY_RECEIPT, + PLAINTEXT_CONTENT, + CIPHERTEXT, + PREKEY_BUNDLE, + TEST_NOTIFICATION, + ]; + + static final $core.Map<$core.int, Message_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static Message_Type? valueOf($core.int value) => _byValue[value]; + + const Message_Type._($core.int v, $core.String n) : super(v, n); +} + +class PlaintextContent_DecryptionErrorMessage_Type extends $pb.ProtobufEnum { + static const PlaintextContent_DecryptionErrorMessage_Type UNKNOWN = PlaintextContent_DecryptionErrorMessage_Type._(0, _omitEnumNames ? '' : 'UNKNOWN'); + static const PlaintextContent_DecryptionErrorMessage_Type PREKEY_UNKNOWN = PlaintextContent_DecryptionErrorMessage_Type._(1, _omitEnumNames ? '' : 'PREKEY_UNKNOWN'); + + static const $core.List values = [ + UNKNOWN, + PREKEY_UNKNOWN, + ]; + + static final $core.Map<$core.int, PlaintextContent_DecryptionErrorMessage_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static PlaintextContent_DecryptionErrorMessage_Type? valueOf($core.int value) => _byValue[value]; + + const PlaintextContent_DecryptionErrorMessage_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_MessageUpdate_Type DELETE = EncryptedContent_MessageUpdate_Type._(0, _omitEnumNames ? '' : 'DELETE'); + static const EncryptedContent_MessageUpdate_Type EDIT_TEXT = EncryptedContent_MessageUpdate_Type._(1, _omitEnumNames ? '' : 'EDIT_TEXT'); + static const EncryptedContent_MessageUpdate_Type OPENED = EncryptedContent_MessageUpdate_Type._(2, _omitEnumNames ? '' : 'OPENED'); + + static const $core.List values = [ + DELETE, + EDIT_TEXT, + OPENED, + ]; + + static final $core.Map<$core.int, EncryptedContent_MessageUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_MessageUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_MessageUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_Media_Type extends $pb.ProtobufEnum { + static const EncryptedContent_Media_Type REUPLOAD = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'REUPLOAD'); + static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE'); + static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO'); + static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(3, _omitEnumNames ? '' : 'GIF'); + static const EncryptedContent_Media_Type AUDIO = EncryptedContent_Media_Type._(4, _omitEnumNames ? '' : 'AUDIO'); + + static const $core.List values = [ + REUPLOAD, + IMAGE, + VIDEO, + GIF, + AUDIO, + ]; + + static final $core.Map<$core.int, EncryptedContent_Media_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_Media_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_Media_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_MediaUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_MediaUpdate_Type REOPENED = EncryptedContent_MediaUpdate_Type._(0, _omitEnumNames ? '' : 'REOPENED'); + static const EncryptedContent_MediaUpdate_Type STORED = EncryptedContent_MediaUpdate_Type._(1, _omitEnumNames ? '' : 'STORED'); + static const EncryptedContent_MediaUpdate_Type DECRYPTION_ERROR = EncryptedContent_MediaUpdate_Type._(2, _omitEnumNames ? '' : 'DECRYPTION_ERROR'); + + static const $core.List values = [ + REOPENED, + STORED, + DECRYPTION_ERROR, + ]; + + static final $core.Map<$core.int, EncryptedContent_MediaUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_MediaUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_MediaUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_ContactRequest_Type extends $pb.ProtobufEnum { + static const EncryptedContent_ContactRequest_Type REQUEST = EncryptedContent_ContactRequest_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_ContactRequest_Type REJECT = EncryptedContent_ContactRequest_Type._(1, _omitEnumNames ? '' : 'REJECT'); + static const EncryptedContent_ContactRequest_Type ACCEPT = EncryptedContent_ContactRequest_Type._(2, _omitEnumNames ? '' : 'ACCEPT'); + + static const $core.List values = [ + REQUEST, + REJECT, + ACCEPT, + ]; + + static final $core.Map<$core.int, EncryptedContent_ContactRequest_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_ContactRequest_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_ContactRequest_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum { + static const EncryptedContent_ContactUpdate_Type REQUEST = EncryptedContent_ContactUpdate_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_ContactUpdate_Type UPDATE = EncryptedContent_ContactUpdate_Type._(1, _omitEnumNames ? '' : 'UPDATE'); + + static const $core.List values = [ + REQUEST, + UPDATE, + ]; + + static final $core.Map<$core.int, EncryptedContent_ContactUpdate_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_ContactUpdate_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_ContactUpdate_Type._($core.int v, $core.String n) : super(v, n); +} + +class EncryptedContent_PushKeys_Type extends $pb.ProtobufEnum { + static const EncryptedContent_PushKeys_Type REQUEST = EncryptedContent_PushKeys_Type._(0, _omitEnumNames ? '' : 'REQUEST'); + static const EncryptedContent_PushKeys_Type UPDATE = EncryptedContent_PushKeys_Type._(1, _omitEnumNames ? '' : 'UPDATE'); + + static const $core.List values = [ + REQUEST, + UPDATE, + ]; + + static final $core.Map<$core.int, EncryptedContent_PushKeys_Type> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptedContent_PushKeys_Type? valueOf($core.int value) => _byValue[value]; + + const EncryptedContent_PushKeys_Type._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart new file mode 100644 index 0000000..08c49e4 --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -0,0 +1,448 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use messageDescriptor instead') +const Message$json = { + '1': 'Message', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.Message.Type', '10': 'type'}, + {'1': 'receiptId', '3': 2, '4': 1, '5': 9, '10': 'receiptId'}, + {'1': 'encryptedContent', '3': 3, '4': 1, '5': 12, '9': 0, '10': 'encryptedContent', '17': true}, + {'1': 'plaintextContent', '3': 4, '4': 1, '5': 11, '6': '.PlaintextContent', '9': 1, '10': 'plaintextContent', '17': true}, + ], + '4': [Message_Type$json], + '8': [ + {'1': '_encryptedContent'}, + {'1': '_plaintextContent'}, + ], +}; + +@$core.Deprecated('Use messageDescriptor instead') +const Message_Type$json = { + '1': 'Type', + '2': [ + {'1': 'SENDER_DELIVERY_RECEIPT', '2': 0}, + {'1': 'PLAINTEXT_CONTENT', '2': 1}, + {'1': 'CIPHERTEXT', '2': 2}, + {'1': 'PREKEY_BUNDLE', '2': 3}, + {'1': 'TEST_NOTIFICATION', '2': 4}, + ], +}; + +/// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( + 'CgdNZXNzYWdlEiEKBHR5cGUYASABKA4yDS5NZXNzYWdlLlR5cGVSBHR5cGUSHAoJcmVjZWlwdE' + 'lkGAIgASgJUglyZWNlaXB0SWQSLwoQZW5jcnlwdGVkQ29udGVudBgDIAEoDEgAUhBlbmNyeXB0' + 'ZWRDb250ZW50iAEBEkIKEHBsYWludGV4dENvbnRlbnQYBCABKAsyES5QbGFpbnRleHRDb250ZW' + '50SAFSEHBsYWludGV4dENvbnRlbnSIAQEidAoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD' + 'RUlQVBAAEhUKEVBMQUlOVEVYVF9DT05URU5UEAESDgoKQ0lQSEVSVEVYVBACEhEKDVBSRUtFWV' + '9CVU5ETEUQAxIVChFURVNUX05PVElGSUNBVElPThAEQhMKEV9lbmNyeXB0ZWRDb250ZW50QhMK' + 'EV9wbGFpbnRleHRDb250ZW50'); + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent$json = { + '1': 'PlaintextContent', + '2': [ + {'1': 'decryptionErrorMessage', '3': 1, '4': 1, '5': 11, '6': '.PlaintextContent.DecryptionErrorMessage', '9': 0, '10': 'decryptionErrorMessage', '17': true}, + {'1': 'retryControlError', '3': 2, '4': 1, '5': 11, '6': '.PlaintextContent.RetryErrorMessage', '9': 1, '10': 'retryControlError', '17': true}, + ], + '3': [PlaintextContent_RetryErrorMessage$json, PlaintextContent_DecryptionErrorMessage$json], + '8': [ + {'1': '_decryptionErrorMessage'}, + {'1': '_retryControlError'}, + ], +}; + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_RetryErrorMessage$json = { + '1': 'RetryErrorMessage', +}; + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_DecryptionErrorMessage$json = { + '1': 'DecryptionErrorMessage', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.PlaintextContent.DecryptionErrorMessage.Type', '10': 'type'}, + ], + '4': [PlaintextContent_DecryptionErrorMessage_Type$json], +}; + +@$core.Deprecated('Use plaintextContentDescriptor instead') +const PlaintextContent_DecryptionErrorMessage_Type$json = { + '1': 'Type', + '2': [ + {'1': 'UNKNOWN', '2': 0}, + {'1': 'PREKEY_UNKNOWN', '2': 1}, + ], +}; + +/// Descriptor for `PlaintextContent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List plaintextContentDescriptor = $convert.base64Decode( + 'ChBQbGFpbnRleHRDb250ZW50EmUKFmRlY3J5cHRpb25FcnJvck1lc3NhZ2UYASABKAsyKC5QbG' + 'FpbnRleHRDb250ZW50LkRlY3J5cHRpb25FcnJvck1lc3NhZ2VIAFIWZGVjcnlwdGlvbkVycm9y' + 'TWVzc2FnZYgBARJWChFyZXRyeUNvbnRyb2xFcnJvchgCIAEoCzIjLlBsYWludGV4dENvbnRlbn' + 'QuUmV0cnlFcnJvck1lc3NhZ2VIAVIRcmV0cnlDb250cm9sRXJyb3KIAQEaEwoRUmV0cnlFcnJv' + 'ck1lc3NhZ2UahAEKFkRlY3J5cHRpb25FcnJvck1lc3NhZ2USQQoEdHlwZRgBIAEoDjItLlBsYW' + 'ludGV4dENvbnRlbnQuRGVjcnlwdGlvbkVycm9yTWVzc2FnZS5UeXBlUgR0eXBlIicKBFR5cGUS' + 'CwoHVU5LTk9XThAAEhIKDlBSRUtFWV9VTktOT1dOEAFCGQoXX2RlY3J5cHRpb25FcnJvck1lc3' + 'NhZ2VCFAoSX3JldHJ5Q29udHJvbEVycm9y'); + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent$json = { + '1': 'EncryptedContent', + '2': [ + {'1': 'groupId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'groupId', '17': true}, + {'1': 'isDirectChat', '3': 3, '4': 1, '5': 8, '9': 1, '10': 'isDirectChat', '17': true}, + {'1': 'senderProfileCounter', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'senderProfileCounter', '17': true}, + {'1': 'messageUpdate', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', '9': 3, '10': 'messageUpdate', '17': true}, + {'1': 'media', '3': 6, '4': 1, '5': 11, '6': '.EncryptedContent.Media', '9': 4, '10': 'media', '17': true}, + {'1': 'mediaUpdate', '3': 7, '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', '9': 5, '10': 'mediaUpdate', '17': true}, + {'1': 'contactUpdate', '3': 8, '4': 1, '5': 11, '6': '.EncryptedContent.ContactUpdate', '9': 6, '10': 'contactUpdate', '17': true}, + {'1': 'contactRequest', '3': 9, '4': 1, '5': 11, '6': '.EncryptedContent.ContactRequest', '9': 7, '10': 'contactRequest', '17': true}, + {'1': 'flameSync', '3': 10, '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', '9': 8, '10': 'flameSync', '17': true}, + {'1': 'pushKeys', '3': 11, '4': 1, '5': 11, '6': '.EncryptedContent.PushKeys', '9': 9, '10': 'pushKeys', '17': true}, + {'1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', '9': 10, '10': 'reaction', '17': true}, + {'1': 'textMessage', '3': 13, '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', '9': 11, '10': 'textMessage', '17': true}, + {'1': 'groupCreate', '3': 14, '4': 1, '5': 11, '6': '.EncryptedContent.GroupCreate', '9': 12, '10': 'groupCreate', '17': true}, + {'1': 'groupJoin', '3': 15, '4': 1, '5': 11, '6': '.EncryptedContent.GroupJoin', '9': 13, '10': 'groupJoin', '17': true}, + {'1': 'groupUpdate', '3': 16, '4': 1, '5': 11, '6': '.EncryptedContent.GroupUpdate', '9': 14, '10': 'groupUpdate', '17': true}, + {'1': 'resendGroupPublicKey', '3': 17, '4': 1, '5': 11, '6': '.EncryptedContent.ResendGroupPublicKey', '9': 15, '10': 'resendGroupPublicKey', '17': true}, + ], + '3': [EncryptedContent_GroupCreate$json, EncryptedContent_GroupJoin$json, EncryptedContent_ResendGroupPublicKey$json, EncryptedContent_GroupUpdate$json, EncryptedContent_TextMessage$json, EncryptedContent_Reaction$json, EncryptedContent_MessageUpdate$json, EncryptedContent_Media$json, EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json], + '8': [ + {'1': '_groupId'}, + {'1': '_isDirectChat'}, + {'1': '_senderProfileCounter'}, + {'1': '_messageUpdate'}, + {'1': '_media'}, + {'1': '_mediaUpdate'}, + {'1': '_contactUpdate'}, + {'1': '_contactRequest'}, + {'1': '_flameSync'}, + {'1': '_pushKeys'}, + {'1': '_reaction'}, + {'1': '_textMessage'}, + {'1': '_groupCreate'}, + {'1': '_groupJoin'}, + {'1': '_groupUpdate'}, + {'1': '_resendGroupPublicKey'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupCreate$json = { + '1': 'GroupCreate', + '2': [ + {'1': 'stateKey', '3': 3, '4': 1, '5': 12, '10': 'stateKey'}, + {'1': 'groupPublicKey', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupJoin$json = { + '1': 'GroupJoin', + '2': [ + {'1': 'groupPublicKey', '3': 1, '4': 1, '5': 12, '10': 'groupPublicKey'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ResendGroupPublicKey$json = { + '1': 'ResendGroupPublicKey', +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_GroupUpdate$json = { + '1': 'GroupUpdate', + '2': [ + {'1': 'groupActionType', '3': 1, '4': 1, '5': 9, '10': 'groupActionType'}, + {'1': 'affectedContactId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'affectedContactId', '17': true}, + {'1': 'newGroupName', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'newGroupName', '17': true}, + {'1': 'newDeleteMessagesAfterMilliseconds', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'newDeleteMessagesAfterMilliseconds', '17': true}, + ], + '8': [ + {'1': '_affectedContactId'}, + {'1': '_newGroupName'}, + {'1': '_newDeleteMessagesAfterMilliseconds'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_TextMessage$json = { + '1': 'TextMessage', + '2': [ + {'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'}, + {'1': 'timestamp', '3': 3, '4': 1, '5': 3, '10': 'timestamp'}, + {'1': 'quoteMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true}, + ], + '8': [ + {'1': '_quoteMessageId'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Reaction$json = { + '1': 'Reaction', + '2': [ + {'1': 'targetMessageId', '3': 1, '4': 1, '5': 9, '10': 'targetMessageId'}, + {'1': 'emoji', '3': 2, '4': 1, '5': 9, '10': 'emoji'}, + {'1': 'remove', '3': 3, '4': 1, '5': 8, '10': 'remove'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MessageUpdate$json = { + '1': 'MessageUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'}, + {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true}, + {'1': 'multipleTargetMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleTargetMessageIds'}, + {'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true}, + {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, + ], + '4': [EncryptedContent_MessageUpdate_Type$json], + '8': [ + {'1': '_senderMessageId'}, + {'1': '_text'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MessageUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'DELETE', '2': 0}, + {'1': 'EDIT_TEXT', '2': 1}, + {'1': 'OPENED', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Media$json = { + '1': 'Media', + '2': [ + {'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'}, + {'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.EncryptedContent.Media.Type', '10': 'type'}, + {'1': 'displayLimitInMilliseconds', '3': 3, '4': 1, '5': 3, '9': 0, '10': 'displayLimitInMilliseconds', '17': true}, + {'1': 'requiresAuthentication', '3': 4, '4': 1, '5': 8, '10': 'requiresAuthentication'}, + {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, + {'1': 'quoteMessageId', '3': 6, '4': 1, '5': 9, '9': 1, '10': 'quoteMessageId', '17': true}, + {'1': 'downloadToken', '3': 7, '4': 1, '5': 12, '9': 2, '10': 'downloadToken', '17': true}, + {'1': 'encryptionKey', '3': 8, '4': 1, '5': 12, '9': 3, '10': 'encryptionKey', '17': true}, + {'1': 'encryptionMac', '3': 9, '4': 1, '5': 12, '9': 4, '10': 'encryptionMac', '17': true}, + {'1': 'encryptionNonce', '3': 10, '4': 1, '5': 12, '9': 5, '10': 'encryptionNonce', '17': true}, + ], + '4': [EncryptedContent_Media_Type$json], + '8': [ + {'1': '_displayLimitInMilliseconds'}, + {'1': '_quoteMessageId'}, + {'1': '_downloadToken'}, + {'1': '_encryptionKey'}, + {'1': '_encryptionMac'}, + {'1': '_encryptionNonce'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_Media_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REUPLOAD', '2': 0}, + {'1': 'IMAGE', '2': 1}, + {'1': 'VIDEO', '2': 2}, + {'1': 'GIF', '2': 3}, + {'1': 'AUDIO', '2': 4}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MediaUpdate$json = { + '1': 'MediaUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, + {'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'}, + ], + '4': [EncryptedContent_MediaUpdate_Type$json], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_MediaUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REOPENED', '2': 0}, + {'1': 'STORED', '2': 1}, + {'1': 'DECRYPTION_ERROR', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactRequest$json = { + '1': 'ContactRequest', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactRequest.Type', '10': 'type'}, + ], + '4': [EncryptedContent_ContactRequest_Type$json], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactRequest_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'REJECT', '2': 1}, + {'1': 'ACCEPT', '2': 2}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactUpdate$json = { + '1': 'ContactUpdate', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.ContactUpdate.Type', '10': 'type'}, + {'1': 'avatarSvgCompressed', '3': 2, '4': 1, '5': 12, '9': 0, '10': 'avatarSvgCompressed', '17': true}, + {'1': 'username', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'username', '17': true}, + {'1': 'displayName', '3': 4, '4': 1, '5': 9, '9': 2, '10': 'displayName', '17': true}, + ], + '4': [EncryptedContent_ContactUpdate_Type$json], + '8': [ + {'1': '_avatarSvgCompressed'}, + {'1': '_username'}, + {'1': '_displayName'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_ContactUpdate_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'UPDATE', '2': 1}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_PushKeys$json = { + '1': 'PushKeys', + '2': [ + {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.PushKeys.Type', '10': 'type'}, + {'1': 'keyId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true}, + {'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true}, + {'1': 'createdAt', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'createdAt', '17': true}, + ], + '4': [EncryptedContent_PushKeys_Type$json], + '8': [ + {'1': '_keyId'}, + {'1': '_key'}, + {'1': '_createdAt'}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_PushKeys_Type$json = { + '1': 'Type', + '2': [ + {'1': 'REQUEST', '2': 0}, + {'1': 'UPDATE', '2': 1}, + ], +}; + +@$core.Deprecated('Use encryptedContentDescriptor instead') +const EncryptedContent_FlameSync$json = { + '1': 'FlameSync', + '2': [ + {'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'}, + {'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'}, + {'1': 'bestFriend', '3': 3, '4': 1, '5': 8, '10': 'bestFriend'}, + {'1': 'forceUpdate', '3': 4, '4': 1, '5': 8, '10': 'forceUpdate'}, + ], +}; + +/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( + 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0' + 'RpcmVjdENoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjcKFHNlbmRlclByb2ZpbGVDb3Vu' + 'dGVyGAQgASgDSAJSFHNlbmRlclByb2ZpbGVDb3VudGVyiAEBEkoKDW1lc3NhZ2VVcGRhdGUYBS' + 'ABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGVIA1INbWVzc2FnZVVwZGF0ZYgB' + 'ARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFIBFIFbWVkaWGIAQESRA' + 'oLbWVkaWFVcGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAVSC21l' + 'ZGlhVXBkYXRliAEBEkoKDWNvbnRhY3RVcGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk' + 'NvbnRhY3RVcGRhdGVIBlINY29udGFjdFVwZGF0ZYgBARJNCg5jb250YWN0UmVxdWVzdBgJIAEo' + 'CzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3RIB1IOY29udGFjdFJlcXVlc3SIAQ' + 'ESPgoJZmxhbWVTeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNICFIJZmxh' + 'bWVTeW5jiAEBEjsKCHB1c2hLZXlzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0' + 'gJUghwdXNoS2V5c4gBARI7CghyZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVh' + 'Y3Rpb25IClIIcmVhY3Rpb26IAQESRAoLdGV4dE1lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb2' + '50ZW50LlRleHRNZXNzYWdlSAtSC3RleHRNZXNzYWdliAEBEkQKC2dyb3VwQ3JlYXRlGA4gASgL' + 'Mh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cENyZWF0ZUgMUgtncm91cENyZWF0ZYgBARI+Cglncm' + '91cEpvaW4YDyABKAsyGy5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgNUglncm91cEpvaW6I' + 'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA' + '5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw' + 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' + 'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ' + 'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym' + 'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkatgIK' + 'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj' + 'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l' + 'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESUwoibmV3RGVsZXRlTWVzc2FnZX' + 'NBZnRlck1pbGxpc2Vjb25kcxgEIAEoA0gCUiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlz' + 'ZWNvbmRziAEBQhQKEl9hZmZlY3RlZENvbnRhY3RJZEIPCg1fbmV3R3JvdXBOYW1lQiUKI19uZX' + 'dEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGqkBCgtUZXh0TWVzc2FnZRIoCg9zZW5k' + 'ZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh' + 'wKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJSABS' + 'DnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg90YX' + 'JnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1v' + 'amkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIA' + 'EoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRl' + 'ck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZX' + 'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo' + 'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE' + 'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH' + 'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' + 'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD' + 'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG' + 'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1' + 'dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2' + 'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI' + 'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2' + '5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu' + 'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV' + 'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k' + 'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2' + 'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp' + 'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn' + 'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl' + 'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE' + 'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD' + 'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg' + 'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk' + 'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0' + 'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD' + 'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY' + 'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl' + 'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA' + 'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg' + 'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS' + 'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf' + 'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG' + 'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW' + 'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi' + 'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl' + 'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW' + 'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz' + 'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW' + 'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz' + 'ZW5kR3JvdXBQdWJsaWNLZXk='); + diff --git a/lib/src/model/protobuf/client/generated/messages.pbserver.dart b/lib/src/model/protobuf/client/generated/messages.pbserver.dart new file mode 100644 index 0000000..956b01d --- /dev/null +++ b/lib/src/model/protobuf/client/generated/messages.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: messages.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'messages.pb.dart'; + diff --git a/lib/src/model/protobuf/push_notification/push_notification.pb.dart b/lib/src/model/protobuf/client/generated/push_notification.pb.dart similarity index 94% rename from lib/src/model/protobuf/push_notification/push_notification.pb.dart rename to lib/src/model/protobuf/client/generated/push_notification.pb.dart index c0e8a0d..c4ad7fe 100644 --- a/lib/src/model/protobuf/push_notification/push_notification.pb.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pb.dart @@ -113,8 +113,8 @@ class EncryptedPushNotification extends $pb.GeneratedMessage { class PushNotification extends $pb.GeneratedMessage { factory PushNotification({ PushKind? kind, - $fixnum.Int64? messageId, - $core.String? reactionContent, + $core.String? messageId, + $core.String? additionalContent, }) { final $result = create(); if (kind != null) { @@ -123,8 +123,8 @@ class PushNotification extends $pb.GeneratedMessage { if (messageId != null) { $result.messageId = messageId; } - if (reactionContent != null) { - $result.reactionContent = reactionContent; + if (additionalContent != null) { + $result.additionalContent = additionalContent; } return $result; } @@ -134,8 +134,8 @@ class PushNotification extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushNotification', createEmptyInstance: create) ..e(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: PushKind.reaction, valueOf: PushKind.valueOf, enumValues: PushKind.values) - ..aInt64(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') - ..aOS(3, _omitFieldNames ? '' : 'reactionContent', protoName: 'reactionContent') + ..aOS(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId') + ..aOS(3, _omitFieldNames ? '' : 'additionalContent', protoName: 'additionalContent') ..hasRequiredFields = false ; @@ -170,22 +170,22 @@ class PushNotification extends $pb.GeneratedMessage { void clearKind() => clearField(1); @$pb.TagNumber(2) - $fixnum.Int64 get messageId => $_getI64(1); + $core.String get messageId => $_getSZ(1); @$pb.TagNumber(2) - set messageId($fixnum.Int64 v) { $_setInt64(1, v); } + set messageId($core.String v) { $_setString(1, v); } @$pb.TagNumber(2) $core.bool hasMessageId() => $_has(1); @$pb.TagNumber(2) void clearMessageId() => clearField(2); @$pb.TagNumber(3) - $core.String get reactionContent => $_getSZ(2); + $core.String get additionalContent => $_getSZ(2); @$pb.TagNumber(3) - set reactionContent($core.String v) { $_setString(2, v); } + set additionalContent($core.String v) { $_setString(2, v); } @$pb.TagNumber(3) - $core.bool hasReactionContent() => $_has(2); + $core.bool hasAdditionalContent() => $_has(2); @$pb.TagNumber(3) - void clearReactionContent() => clearField(3); + void clearAdditionalContent() => clearField(3); } class PushUsers extends $pb.GeneratedMessage { @@ -237,7 +237,7 @@ class PushUser extends $pb.GeneratedMessage { $fixnum.Int64? userId, $core.String? displayName, $core.bool? blocked, - $fixnum.Int64? lastMessageId, + $core.String? lastMessageId, $core.Iterable? pushKeys, }) { final $result = create(); @@ -266,7 +266,7 @@ class PushUser extends $pb.GeneratedMessage { ..aInt64(1, _omitFieldNames ? '' : 'userId', protoName: 'userId') ..aOS(2, _omitFieldNames ? '' : 'displayName', protoName: 'displayName') ..aOB(3, _omitFieldNames ? '' : 'blocked') - ..aInt64(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId') + ..aOS(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId') ..pc(5, _omitFieldNames ? '' : 'pushKeys', $pb.PbFieldType.PM, protoName: 'pushKeys', subBuilder: PushKey.create) ..hasRequiredFields = false ; @@ -320,9 +320,9 @@ class PushUser extends $pb.GeneratedMessage { void clearBlocked() => clearField(3); @$pb.TagNumber(4) - $fixnum.Int64 get lastMessageId => $_getI64(3); + $core.String get lastMessageId => $_getSZ(3); @$pb.TagNumber(4) - set lastMessageId($fixnum.Int64 v) { $_setInt64(3, v); } + set lastMessageId($core.String v) { $_setString(3, v); } @$pb.TagNumber(4) $core.bool hasLastMessageId() => $_has(3); @$pb.TagNumber(4) diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart similarity index 88% rename from lib/src/model/protobuf/push_notification/push_notification.pbenum.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbenum.dart index 8bf3ded..2a2fbf7 100644 --- a/lib/src/model/protobuf/push_notification/push_notification.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbenum.dart @@ -28,6 +28,9 @@ class PushKind extends $pb.ProtobufEnum { static const PushKind reactionToVideo = PushKind._(11, _omitEnumNames ? '' : 'reactionToVideo'); static const PushKind reactionToText = PushKind._(12, _omitEnumNames ? '' : 'reactionToText'); static const PushKind reactionToImage = PushKind._(13, _omitEnumNames ? '' : 'reactionToImage'); + static const PushKind reactionToAudio = PushKind._(14, _omitEnumNames ? '' : 'reactionToAudio'); + static const PushKind addedToGroup = PushKind._(15, _omitEnumNames ? '' : 'addedToGroup'); + static const PushKind audio = PushKind._(16, _omitEnumNames ? '' : 'audio'); static const $core.List values = [ reaction, @@ -44,6 +47,9 @@ class PushKind extends $pb.ProtobufEnum { reactionToVideo, reactionToText, reactionToImage, + reactionToAudio, + addedToGroup, + audio, ]; static final $core.Map<$core.int, PushKind> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart similarity index 86% rename from lib/src/model/protobuf/push_notification/push_notification.pbjson.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbjson.dart index d66d5aa..4d576a1 100644 --- a/lib/src/model/protobuf/push_notification/push_notification.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/push_notification.pbjson.dart @@ -31,6 +31,9 @@ const PushKind$json = { {'1': 'reactionToVideo', '2': 11}, {'1': 'reactionToText', '2': 12}, {'1': 'reactionToImage', '2': 13}, + {'1': 'reactionToAudio', '2': 14}, + {'1': 'addedToGroup', '2': 15}, + {'1': 'audio', '2': 16}, ], }; @@ -40,7 +43,8 @@ final $typed_data.Uint8List pushKindDescriptor = $convert.base64Decode( 'VvEAMSCgoGdHdvbmx5EAQSCQoFaW1hZ2UQBRISCg5jb250YWN0UmVxdWVzdBAGEhEKDWFjY2Vw' 'dFJlcXVlc3QQBxITCg9zdG9yZWRNZWRpYUZpbGUQCBIUChB0ZXN0Tm90aWZpY2F0aW9uEAkSEQ' 'oNcmVvcGVuZWRNZWRpYRAKEhMKD3JlYWN0aW9uVG9WaWRlbxALEhIKDnJlYWN0aW9uVG9UZXh0' - 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0='); + 'EAwSEwoPcmVhY3Rpb25Ub0ltYWdlEA0SEwoPcmVhY3Rpb25Ub0F1ZGlvEA4SEAoMYWRkZWRUb0' + 'dyb3VwEA8SCQoFYXVkaW8QEA=='); @$core.Deprecated('Use encryptedPushNotificationDescriptor instead') const EncryptedPushNotification$json = { @@ -64,20 +68,21 @@ const PushNotification$json = { '1': 'PushNotification', '2': [ {'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.PushKind', '10': 'kind'}, - {'1': 'messageId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'messageId', '17': true}, - {'1': 'reactionContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'reactionContent', '17': true}, + {'1': 'messageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'messageId', '17': true}, + {'1': 'additionalContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'additionalContent', '17': true}, ], '8': [ {'1': '_messageId'}, - {'1': '_reactionContent'}, + {'1': '_additionalContent'}, ], }; /// Descriptor for `PushNotification`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List pushNotificationDescriptor = $convert.base64Decode( 'ChBQdXNoTm90aWZpY2F0aW9uEh0KBGtpbmQYASABKA4yCS5QdXNoS2luZFIEa2luZBIhCgltZX' - 'NzYWdlSWQYAiABKANIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB' - 'Ug9yZWFjdGlvbkNvbnRlbnSIAQFCDAoKX21lc3NhZ2VJZEISChBfcmVhY3Rpb25Db250ZW50'); + 'NzYWdlSWQYAiABKAlIAFIJbWVzc2FnZUlkiAEBEjEKEWFkZGl0aW9uYWxDb250ZW50GAMgASgJ' + 'SAFSEWFkZGl0aW9uYWxDb250ZW50iAEBQgwKCl9tZXNzYWdlSWRCFAoSX2FkZGl0aW9uYWxDb2' + '50ZW50'); @$core.Deprecated('Use pushUsersDescriptor instead') const PushUsers$json = { @@ -98,7 +103,7 @@ const PushUser$json = { {'1': 'userId', '3': 1, '4': 1, '5': 3, '10': 'userId'}, {'1': 'displayName', '3': 2, '4': 1, '5': 9, '10': 'displayName'}, {'1': 'blocked', '3': 3, '4': 1, '5': 8, '10': 'blocked'}, - {'1': 'lastMessageId', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'lastMessageId', '17': true}, + {'1': 'lastMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'lastMessageId', '17': true}, {'1': 'pushKeys', '3': 5, '4': 3, '5': 11, '6': '.PushKey', '10': 'pushKeys'}, ], '8': [ @@ -110,7 +115,7 @@ const PushUser$json = { final $typed_data.Uint8List pushUserDescriptor = $convert.base64Decode( 'CghQdXNoVXNlchIWCgZ1c2VySWQYASABKANSBnVzZXJJZBIgCgtkaXNwbGF5TmFtZRgCIAEoCV' 'ILZGlzcGxheU5hbWUSGAoHYmxvY2tlZBgDIAEoCFIHYmxvY2tlZBIpCg1sYXN0TWVzc2FnZUlk' - 'GAQgASgDSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug' + 'GAQgASgJSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug' 'hwdXNoS2V5c0IQCg5fbGFzdE1lc3NhZ2VJZA=='); @$core.Deprecated('Use pushKeyDescriptor instead') diff --git a/lib/src/model/protobuf/push_notification/push_notification.pbserver.dart b/lib/src/model/protobuf/client/generated/push_notification.pbserver.dart similarity index 100% rename from lib/src/model/protobuf/push_notification/push_notification.pbserver.dart rename to lib/src/model/protobuf/client/generated/push_notification.pbserver.dart diff --git a/lib/src/model/protobuf/client/groups.proto b/lib/src/model/protobuf/client/groups.proto new file mode 100644 index 0000000..268ad1f --- /dev/null +++ b/lib/src/model/protobuf/client/groups.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +// Stored encrypted on the server in the members columns. +message EncryptedGroupState { + repeated int64 memberIds = 1; + repeated int64 adminIds = 2; + string groupName = 3; + optional int64 deleteMessagesAfterMilliseconds = 4; + bytes padding = 5; +} + +message EncryptedAppendedGroupState { + enum Type { + LEFT_GROUP = 0; + } + Type type = 1; +} + +message EncryptedGroupStateEnvelop { + bytes nonce = 1; + bytes encryptedGroupState = 2; + bytes mac = 3; +} \ No newline at end of file diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto new file mode 100644 index 0000000..61f6364 --- /dev/null +++ b/lib/src/model/protobuf/client/messages.proto @@ -0,0 +1,176 @@ +syntax = "proto3"; + +message Message { + enum Type { + SENDER_DELIVERY_RECEIPT = 0; + PLAINTEXT_CONTENT = 1; + CIPHERTEXT = 2; + PREKEY_BUNDLE = 3; + TEST_NOTIFICATION = 4; + } + Type type = 1; + string receiptId = 2; + optional bytes encryptedContent = 3; + optional PlaintextContent plaintextContent = 4; +} + +message PlaintextContent { + optional DecryptionErrorMessage decryptionErrorMessage = 1; + optional RetryErrorMessage retryControlError = 2; + + message RetryErrorMessage { } + + message DecryptionErrorMessage { + enum Type { + UNKNOWN = 0; + PREKEY_UNKNOWN = 1; + } + Type type = 1; + } +} + + +message EncryptedContent { + + optional string groupId = 2; + optional bool isDirectChat = 3; + + /// This can be added, so the receiver can check weather he is up to date with the current profile + optional int64 senderProfileCounter = 4; + + optional MessageUpdate messageUpdate = 5; + optional Media media = 6; + optional MediaUpdate mediaUpdate = 7; + optional ContactUpdate contactUpdate = 8; + optional ContactRequest contactRequest = 9; + optional FlameSync flameSync = 10; + optional PushKeys pushKeys = 11; + optional Reaction reaction = 12; + optional TextMessage textMessage = 13; + optional GroupCreate groupCreate = 14; + optional GroupJoin groupJoin = 15; + optional GroupUpdate groupUpdate = 16; + optional ResendGroupPublicKey resendGroupPublicKey = 17; + + + message GroupCreate { + // key for the state stored on the server + bytes stateKey = 3; + bytes groupPublicKey = 4; + } + + message GroupJoin { + // key for the state stored on the server + bytes groupPublicKey = 1; + } + + message ResendGroupPublicKey { + + } + + message GroupUpdate { + string groupActionType = 1; // GroupActionType.name + optional int64 affectedContactId = 2; + optional string newGroupName = 3; + optional int64 newDeleteMessagesAfterMilliseconds = 4; + } + + message TextMessage { + string senderMessageId = 1; + string text = 2; + int64 timestamp = 3; + optional string quoteMessageId = 4; + } + + message Reaction { + string targetMessageId = 1; + string emoji = 2; + bool remove = 3; + } + + message MessageUpdate { + enum Type { + DELETE = 0; + EDIT_TEXT = 1; + OPENED = 2; + } + Type type = 1; + optional string senderMessageId = 2; + repeated string multipleTargetMessageIds = 3; + optional string text = 4; + int64 timestamp = 5; + } + + message Media { + enum Type { + REUPLOAD = 0; + IMAGE = 1; + VIDEO = 2; + GIF = 3; + AUDIO = 4; + } + + string senderMessageId = 1; + Type type = 2; + optional int64 displayLimitInMilliseconds = 3; + bool requiresAuthentication = 4; + int64 timestamp = 5; + optional string quoteMessageId = 6; + + optional bytes downloadToken = 7; + optional bytes encryptionKey = 8; + optional bytes encryptionMac = 9; + optional bytes encryptionNonce = 10; + } + + message MediaUpdate { + enum Type { + REOPENED = 0; + STORED = 1; + DECRYPTION_ERROR = 2; + } + Type type = 1; + string targetMessageId = 2; + } + + message ContactRequest { + enum Type { + REQUEST = 0; + REJECT = 1; + ACCEPT = 2; + } + Type type = 1; + } + + message ContactUpdate { + enum Type { + REQUEST = 0; + UPDATE = 1; + } + + Type type = 1; + optional bytes avatarSvgCompressed = 2; + optional string username = 3; + optional string displayName = 4; + } + + message PushKeys { + enum Type { + REQUEST = 0; + UPDATE = 1; + } + + Type type = 1; + optional int64 keyId = 2; + optional bytes key = 3; + optional int64 createdAt = 4; + } + + message FlameSync { + int64 flameCounter = 1; + int64 lastFlameCounterChange = 2; + bool bestFriend = 3; + bool forceUpdate = 4; + } + +} \ No newline at end of file diff --git a/lib/src/model/protobuf/push_notification/push_notification.proto b/lib/src/model/protobuf/client/push_notification.proto similarity index 81% rename from lib/src/model/protobuf/push_notification/push_notification.proto rename to lib/src/model/protobuf/client/push_notification.proto index 3c19946..5e74e9c 100644 --- a/lib/src/model/protobuf/push_notification/push_notification.proto +++ b/lib/src/model/protobuf/client/push_notification.proto @@ -22,12 +22,15 @@ enum PushKind { reactionToVideo = 11; reactionToText = 12; reactionToImage = 13; + reactionToAudio = 14; + addedToGroup = 15; + audio = 16; }; message PushNotification { PushKind kind = 1; - optional int64 messageId = 2; - optional string reactionContent = 3; + optional string messageId = 2; + optional string additionalContent = 3; } @@ -39,7 +42,7 @@ message PushUser { int64 userId = 1; string displayName = 2; bool blocked = 3; - optional int64 lastMessageId = 4; + optional string lastMessageId = 4; repeated PushKey pushKeys = 5; } diff --git a/lib/src/providers/connection.provider.dart b/lib/src/providers/connection.provider.dart index 5b63969..f09472c 100644 --- a/lib/src/providers/connection.provider.dart +++ b/lib/src/providers/connection.provider.dart @@ -1,15 +1,16 @@ import 'package:flutter/foundation.dart'; +import 'package:twonly/src/services/subscription.service.dart'; class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { bool _isConnected = false; bool get isConnected => _isConnected; - String plan = 'Free'; + SubscriptionPlan plan = SubscriptionPlan.Free; Future updateConnectionState(bool update) async { _isConnected = update; notifyListeners(); } - Future updatePlan(String newPlan) async { + Future updatePlan(SubscriptionPlan newPlan) async { plan = newPlan; notifyListeners(); } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 461d180..57b3ed9 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:ui' as ui; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; @@ -17,23 +18,24 @@ import 'package:mutex/mutex.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -49,8 +51,9 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kDebugMode ? '10.99.0.140:3030' : 'api.twonly.eu'; - final String apiSecure = kDebugMode ? '' : 's'; + // final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; + final String apiSecure = kReleaseMode ? 's' : 's'; bool appIsOutdated = false; bool isAuthenticated = false; @@ -94,13 +97,13 @@ class ApiService { if (!globalIsAppInBackground) { unawaited(retransmitRawBytes()); unawaited(tryTransmitMessages()); - unawaited(retryMediaUpload(false)); unawaited(tryDownloadAllMediaFiles()); - unawaited(notifyContactsAboutProfileChange()); twonlyDB.markUpdated(); unawaited(syncFlameCounters()); unawaited(setupNotificationWithUsers()); unawaited(signalHandleNewServerConnection()); + unawaited(fetchGroupStatesForUnjoinedGroups()); + unawaited(fetchMissingGroupPublicKey()); } } @@ -114,7 +117,7 @@ class ApiService { _channel = null; isAuthenticated = false; globalCallbackConnectionState(isConnected: false); - await twonlyDB.messagesDao.resetPendingDownloadState(); + await twonlyDB.mediaFilesDao.resetPendingDownloadState(); } Future startReconnectionTimer() async { @@ -158,11 +161,6 @@ class ApiService { } reconnectionTimer?.cancel(); reconnectionTimer = null; - final user = await getUser(); - if (user != null && user.isDemoUser) { - globalCallbackConnectionState(isConnected: true); - return false; - } return lockConnecting.protect(() async { if (_channel != null) { return true; @@ -291,6 +289,10 @@ class ApiService { request.v0.seq = seq; final requestBytes = request.writeToBuffer(); + Log.info( + 'Sending ${requestBytes.length} bytes to the server via WebSocket.', + ); + if (ensureRetransmission) { await addToRetransmissionBuffer(seq, requestBytes); } @@ -319,7 +321,9 @@ class ApiService { } if (res.error == ErrorCode.NewDeviceRegistered) { globalCallbackNewDeviceRegistered(); - Log.error('Device is disabled, as a newer device restore twonly Safe.'); + Log.error( + 'Device is disabled, as a newer device restore twonly Backup.', + ); appIsOutdated = true; await close(() {}); return Result.error(ErrorCode.InternalError); @@ -346,7 +350,7 @@ class ApiService { await twonlyDB.contactsDao.updateContact( contactId, ContactsCompanion( - deleted: const Value(true), + accountDeleted: const Value(true), username: Value('${contact.username} (${contact.userId})'), ), ); @@ -363,6 +367,12 @@ class ApiService { final user = await getUser(); if (apiAuthToken != null && user != null) { + if (user.appVersion < 62) { + Log.error( + 'DID NOT authenticate the user, as he still has the old version!', + ); + return false; + } final authenticate = Handshake_Authenticate() ..userId = Int64(userId) ..appVersion = (await PackageInfo.fromPlatform()).version @@ -382,7 +392,7 @@ class ApiService { user.subscriptionPlan = authenticated.plan; return user; }); - globalCallbackUpdatePlan(authenticated.plan); + globalCallbackUpdatePlan(planFromString(authenticated.plan)); } Log.info('websocket is authenticated'); unawaited(onAuthenticated()); @@ -412,7 +422,7 @@ class ApiService { } final handshake = Handshake() - ..getauthchallenge = Handshake_GetAuthChallenge(); + ..getAuthChallenge = Handshake_GetAuthChallenge(); final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); @@ -433,7 +443,7 @@ class ApiService { ..response = signature ..userId = Int64(userData.userId); - final getauthtoken = Handshake()..getauthtoken = getAuthToken; + final getauthtoken = Handshake()..getAuthToken = getAuthToken; final req2 = createClientToServerFromHandshake(getauthtoken); @@ -455,7 +465,11 @@ class ApiService { await tryAuthenticateWithToken(userData.userId); } - Future register(String username, String? inviteCode) async { + Future register( + String username, + String? inviteCode, + int proofOfWorkResult, + ) async { final signalIdentity = await getSignalIdentity(); if (signalIdentity == null) { return Result.error(ErrorCode.InternalError); @@ -473,6 +487,8 @@ class ApiService { ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekeySignature = signedPreKey.signature ..signedPrekeyId = Int64(signedPreKey.id) + ..langCode = ui.PlatformDispatcher.instance.locale.languageCode + ..proofOfWork = Int64(proofOfWorkResult) ..isIos = Platform.isIOS; if (inviteCode != null && inviteCode != '') { @@ -485,30 +501,52 @@ class ApiService { return sendRequestSync(req); } - Future getUsername(int userId) async { + Future getUserById(int userId) async { final get = ApplicationData_GetUserById()..userId = Int64(userId); - final appData = ApplicationData()..getuserbyid = get; + final appData = ApplicationData()..getUserById = get; final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req, contactId: userId); + final res = await sendRequestSync(req); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasUserdata()) { + return ok.userdata; + } + } + return null; + } + + Future<(Response_ProofOfWork?, bool)> getProofOfWork() async { + final handshake = Handshake()..requestPOW = Handshake_RequestPOW(); + final req = createClientToServerFromHandshake(handshake); + final result = await sendRequestSync(req, authenticated: false); + if (result.isError) { + Log.error('could not request proof of work params', result); + if (result.error == ErrorCode.RegistrationDisabled) { + return (null, true); + } + Log.error('could not request proof of work params', result); + return (null, false); + } + return (result.value.proofOfWork as Response_ProofOfWork, false); } Future downloadDone(List token) async { final get = ApplicationData_DownloadDone()..downloadToken = token; - final appData = ApplicationData()..downloaddone = get; + final appData = ApplicationData()..downloadDone = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, ensureRetransmission: true); } Future getCurrentLocation() async { final get = ApplicationData_GetLocation(); - final appData = ApplicationData()..getlocation = get; + final appData = ApplicationData()..getLocation = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future getUserData(String username) async { final get = ApplicationData_GetUserByUsername()..username = username; - final appData = ApplicationData()..getuserbyusername = get; + final appData = ApplicationData()..getUserByUsername = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -522,7 +560,7 @@ class ApiService { Future getPlanBallance() async { final get = ApplicationData_GetCurrentPlanInfos(); - final appData = ApplicationData()..getcurrentplaninfos = get; + final appData = ApplicationData()..getCurrentPlanInfos = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -536,7 +574,7 @@ class ApiService { Future getVoucherList() async { final get = ApplicationData_GetVouchers(); - final appData = ApplicationData()..getvouchers = get; + final appData = ApplicationData()..getVouchers = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -550,7 +588,7 @@ class ApiService { Future?> getAdditionalUserInvites() async { final get = ApplicationData_GetAddAccountsInvites(); - final appData = ApplicationData()..getaddaccountsinvites = get; + final appData = ApplicationData()..getAddaccountsInvites = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req); if (res.isSuccess) { @@ -564,21 +602,21 @@ class ApiService { Future updatePlanOptions(bool autoRenewal) async { final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal; - final appData = ApplicationData()..updateplanoptions = get; + final appData = ApplicationData()..updatePlanOptions = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future removeAdditionalUser(Int64 userId) async { final get = ApplicationData_RemoveAdditionalUser()..userId = userId; - final appData = ApplicationData()..removeadditionaluser = get; + final appData = ApplicationData()..removeAdditionalUser = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, contactId: userId.toInt()); } Future buyVoucher(int valueInCents) async { final get = ApplicationData_CreateVoucher()..valueCents = valueInCents; - final appData = ApplicationData()..createvoucher = get; + final appData = ApplicationData()..createVoucher = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -592,14 +630,14 @@ class ApiService { ..planId = planId ..payMonthly = payMonthly ..autoRenewal = autoRenewal; - final appData = ApplicationData()..switchtopayedplan = get; + final appData = ApplicationData()..switchtoPayedPlan = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future redeemVoucher(String voucher) async { final get = ApplicationData_RedeemVoucher()..voucher = voucher; - final appData = ApplicationData()..redeemvoucher = get; + final appData = ApplicationData()..redeemVoucher = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -608,28 +646,35 @@ class ApiService { final get = ApplicationData_ReportUser() ..reportedUserId = Int64(userId) ..reason = reason; - final appData = ApplicationData()..reportuser = get; + final appData = ApplicationData()..reportUser = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future deleteAccount() async { final get = ApplicationData_DeleteAccount(); - final appData = ApplicationData()..deleteaccount = get; + final appData = ApplicationData()..deleteAccount = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future redeemUserInviteCode(String inviteCode) async { final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode; - final appData = ApplicationData()..redeemadditionalcode = get; + final appData = ApplicationData()..redeemAdditionalCode = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } Future updateFCMToken(String googleFcm) async { final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; - final appData = ApplicationData()..updategooglefcmtoken = get; + final appData = ApplicationData()..updateGoogleFcmToken = get; + final req = createClientToServerFromApplicationData(appData); + return sendRequestSync(req); + } + + Future changeUsername(String username) async { + final get = ApplicationData_ChangeUsername()..username = username; + final appData = ApplicationData()..changeUsername = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -643,7 +688,7 @@ class ApiService { ..signedPrekeyId = Int64(signedPreKeyId) ..signedPrekey = signedPreKey ..signedPrekeySignature = signedPreKeySignature; - final appData = ApplicationData()..updatesignedprekey = get; + final appData = ApplicationData()..updateSignedPrekey = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); } @@ -651,7 +696,7 @@ class ApiService { Future getSignedKeyByUserId(int userId) async { final get = ApplicationData_GetSignedPreKeyByUserId() ..userId = Int64(userId); - final appData = ApplicationData()..getsignedprekeybyuserid = get; + final appData = ApplicationData()..getSignedPrekeyByUserid = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req, contactId: userId); if (res.isSuccess) { @@ -665,7 +710,7 @@ class ApiService { Future getPreKeysByUserId(int userId) async { final get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId); - final appData = ApplicationData()..getprekeysbyuserid = get; + final appData = ApplicationData()..getPrekeysByUserId = get; final req = createClientToServerFromApplicationData(appData); final res = await sendRequestSync(req, contactId: userId); if (res.isSuccess) { @@ -699,8 +744,7 @@ class ApiService { if (pushData != null) { testMessage.pushData = pushData; } - - final appData = ApplicationData()..textmessage = testMessage; + final appData = ApplicationData()..textMessage = testMessage; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, contactId: target); } diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart new file mode 100644 index 0000000..5a63668 --- /dev/null +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart' hide Message; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future handleContactRequest( + int fromUserId, + EncryptedContent_ContactRequest contactRequest, +) async { + switch (contactRequest.type) { + case EncryptedContent_ContactRequest_Type.REQUEST: + Log.info('Got a contact request from $fromUserId'); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.accepted) { + // contact was already accepted, so just accept the request in the background. + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.ACCEPT, + ), + ), + ); + return true; + } + } + // Request the username by the server so an attacker can not + // forge the displayed username in the contact request + final user = await apiService.getUserById(fromUserId); + if (user == null) { + return false; + } + await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(user.username)), + userId: Value(fromUserId), + requested: const Value(true), + deletedByUser: const Value(false), + ), + ); + await setupNotificationWithUsers(); + case EncryptedContent_ContactRequest_Type.ACCEPT: + Log.info('Got a contact accept from $fromUserId'); + await twonlyDB.contactsDao.updateContact( + fromUserId, + const ContactsCompanion( + requested: Value(false), + accepted: Value(true), + deletedByUser: Value(false), + ), + ); + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + await twonlyDB.groupsDao.createNewDirectChat( + fromUserId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact!)), + ), + ); + case EncryptedContent_ContactRequest_Type.REJECT: + Log.info('Got a contact reject from $fromUserId'); + await twonlyDB.contactsDao.updateContact( + fromUserId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); + } + return true; +} + +Future handleContactUpdate( + int fromUserId, + EncryptedContent_ContactUpdate contactUpdate, + int? senderProfileCounter, +) async { + switch (contactUpdate.type) { + case EncryptedContent_ContactUpdate_Type.REQUEST: + Log.info('Got a contact update request from $fromUserId'); + await notifyContactsAboutProfileChange(onlyToContact: fromUserId); + + case EncryptedContent_ContactUpdate_Type.UPDATE: + Log.info('Got a contact update $fromUserId'); + if (contactUpdate.hasAvatarSvgCompressed() && + contactUpdate.hasDisplayName() && + contactUpdate.hasUsername() && + senderProfileCounter != null) { + await twonlyDB.contactsDao.updateContact( + fromUserId, + ContactsCompanion( + avatarSvgCompressed: + Value(Uint8List.fromList(contactUpdate.avatarSvgCompressed)), + displayName: Value(contactUpdate.displayName), + username: Value(contactUpdate.username), + senderProfileCounter: Value(senderProfileCounter), + ), + ); + unawaited(createPushAvatars()); + } + } +} + +Future handleFlameSync( + int contactId, + EncryptedContent_FlameSync flameSync, +) async { + Log.info('Got a flameSync from $contactId'); + + final group = await twonlyDB.groupsDao.getDirectChat(contactId); + if (group == null || group.lastFlameCounterChange == null) return; + + var updates = GroupsCompanion( + alsoBestFriend: Value(flameSync.bestFriend), + ); + if (isToday(group.lastFlameCounterChange!) && + isToday(fromTimestamp(flameSync.lastFlameCounterChange)) || + flameSync.forceUpdate) { + if (flameSync.flameCounter > group.flameCounter) { + updates = updates.copyWith( + flameCounter: Value(flameSync.flameCounter.toInt()), + ); + } + if (flameSync.flameCounter > group.maxFlameCounter) { + updates = updates.copyWith( + maxFlameCounter: Value(flameSync.flameCounter.toInt()), + ); + } + } + await twonlyDB.groupsDao.updateGroup(group.groupId, updates); +} + +Future checkForProfileUpdate( + int fromUserId, + EncryptedContent content, +) async { + int? senderProfileCounter; + + if (content.hasSenderProfileCounter()) { + senderProfileCounter = content.senderProfileCounter.toInt(); + if (!content.hasContactUpdate()) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (contact != null) { + if (contact.senderProfileCounter < senderProfileCounter) { + await sendCipherText( + fromUserId, + EncryptedContent( + contactUpdate: EncryptedContent_ContactUpdate( + type: EncryptedContent_ContactUpdate_Type.REQUEST, + ), + ), + ); + } + } + } + } + + return senderProfileCounter; +} diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart new file mode 100644 index 0000000..a20b27f --- /dev/null +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'package:drift/drift.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleGroupCreate( + int fromUserId, + String groupId, + EncryptedContent_GroupCreate newGroup, +) async { + final user = await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull(); + if (user == null) { + // Only contacts can invite other contacts, so this can (via the UI) not happen. + Log.error( + 'User is not a contact. Aborting.', + ); + return; + } + + // 1. Store the new group -> e.g. store the stateKey and groupPublicKey + // 2. Call function that should fetch all jobs + // 1. This function is also called in the main function, in case the state stored on the server could not be loaded + // 2. This function will also send the GroupJoin to all members -> so they get there public key + // 3. Finished + + final myGroupKey = generateIdentityKeyPair(); + + var group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) { + // Group state is joinedGroup -> As the current state has not yet been downloaded. + group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + ), + ); + } else { + // User was already in the group, so update leftGroup back to false + await twonlyDB.groupsDao.updateGroup( + groupId, + GroupsCompanion( + stateVersionId: const Value(0), + stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), + myGroupPrivateKey: Value(myGroupKey.serialize()), + groupName: const Value(''), + joinedGroup: const Value(false), + leftGroup: const Value(false), + deletedContent: const Value(false), + ), + ); + } + + if (group == null) { + Log.error( + 'Could not create new group. Probably because the group already existed.', + ); + return; + } + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + contactId: Value(fromUserId), + affectedContactId: const Value(null), + type: const Value(GroupActionType.addMember), + ), + ); + + await twonlyDB.groupsDao.insertOrUpdateGroupMember( + GroupMembersCompanion( + groupId: Value(groupId), + contactId: Value(fromUserId), + memberState: const Value( + MemberState.admin, // is the group creator, so must be admin... + ), + groupPublicKey: Value(Uint8List.fromList(newGroup.groupPublicKey)), + ), + ); + + // can be done in the background -> websocket message can be ACK + unawaited(fetchGroupStatesForUnjoinedGroups()); + + await sendCipherTextToGroup( + groupId, + EncryptedContent( + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: myGroupKey.getPublicKey().serialize(), + ), + ), + ); +} + +Future handleGroupUpdate( + int fromUserId, + String groupId, + EncryptedContent_GroupUpdate update, +) async { + Log.info('Got group update for $groupId from $fromUserId'); + + final actionType = groupActionTypeFromString(update.groupActionType); + if (actionType == null) { + Log.error('Group action ${update.groupActionType} is unknown ignoring.'); + return; + } + + final group = (await twonlyDB.groupsDao.getGroup(groupId))!; + + switch (actionType) { + case GroupActionType.updatedGroupName: + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + oldGroupName: Value(group.groupName), + newGroupName: Value(update.newGroupName), + contactId: Value(fromUserId), + ), + ); + case GroupActionType.changeDisplayMaxTime: + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + newDeleteMessagesAfterMilliseconds: + Value(update.newDeleteMessagesAfterMilliseconds.toInt()), + contactId: Value(fromUserId), + ), + ); + if (group.isDirectChat) { + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + deleteMessagesAfterMilliseconds: + Value(update.newDeleteMessagesAfterMilliseconds.toInt()), + ), + ); + } + case GroupActionType.removedMember: + case GroupActionType.addMember: + case GroupActionType.leftGroup: + case GroupActionType.promoteToAdmin: + case GroupActionType.demoteToMember: + int? affectedContactId = update.affectedContactId.toInt(); + + if (affectedContactId == gUser.userId) { + affectedContactId = null; + if (actionType == GroupActionType.removedMember) { + // Oh no, I just got removed from the group... + // This state is handle this case in the fetchGroupState.... + } + } + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: Value(actionType), + affectedContactId: Value(affectedContactId), + contactId: Value(fromUserId), + ), + ); + case GroupActionType.createdGroup: + break; + } + + if (!group.isDirectChat) { + unawaited(fetchGroupState(group)); + } +} + +Future handleGroupJoin( + int fromUserId, + String groupId, + EncryptedContent_GroupJoin join, +) async { + if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { + if (!await addNewHiddenContact(fromUserId)) { + Log.error('Got group join, but could not load contact.'); + // This can happen in case the group join was received before the group create. + // In this case return false, which will cause the receipt to fail and the user + // will resend this message. + return false; + } + } + await twonlyDB.groupsDao.updateMember( + groupId, + fromUserId, + GroupMembersCompanion( + groupPublicKey: Value(Uint8List.fromList(join.groupPublicKey)), + ), + ); + return true; +} + +Future handleResendGroupPublicKey( + int fromUserId, + String groupId, + EncryptedContent_GroupJoin join, +) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null || group.myGroupPrivateKey == null) return; + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + await sendCipherText( + fromUserId, + EncryptedContent( + groupId: groupId, + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); +} diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart new file mode 100644 index 0000000..616086d --- /dev/null +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -0,0 +1,166 @@ +import 'dart:async'; +import 'package:drift/drift.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/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleMedia( + int fromUserId, + String groupId, + EncryptedContent_Media media, +) async { + Log.info( + 'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', + ); + + late MediaType mediaType; + switch (media.type) { + case EncryptedContent_Media_Type.REUPLOAD: + final message = await twonlyDB.messagesDao + .getMessageById(media.senderMessageId) + .getSingleOrNull(); + if (message == null || + message.senderId != fromUserId || + message.mediaId == null) { + return; + } + + // in case there was already a downloaded file delete it... + final mediaService = await MediaFileService.fromMediaId(message.mediaId!); + if (mediaService != null) { + mediaService.tempPath.deleteSync(); + } + + await twonlyDB.mediaFilesDao.updateMedia( + message.mediaId!, + MediaFilesCompanion( + downloadState: const Value(DownloadState.pending), + downloadToken: Value(Uint8List.fromList(media.downloadToken)), + encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), + encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), + encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), + ), + ); + + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + + if (mediaFile != null) { + unawaited(startDownloadMedia(mediaFile, false)); + } + + return; + case EncryptedContent_Media_Type.IMAGE: + mediaType = MediaType.image; + case EncryptedContent_Media_Type.VIDEO: + mediaType = MediaType.video; + case EncryptedContent_Media_Type.GIF: + mediaType = MediaType.gif; + case EncryptedContent_Media_Type.AUDIO: + mediaType = MediaType.audio; + } + + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + downloadState: const Value(DownloadState.pending), + type: Value(mediaType), + requiresAuthentication: Value(media.requiresAuthentication), + displayLimitInMilliseconds: Value( + media.hasDisplayLimitInMilliseconds() + ? media.displayLimitInMilliseconds.toInt() + : null, + ), + downloadToken: Value(Uint8List.fromList(media.downloadToken)), + encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), + encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), + encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), + createdAt: Value(fromTimestamp(media.timestamp)), + ), + ); + + if (mediaFile == null) { + Log.error('Could not insert media file into database'); + return; + } + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(media.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + mediaId: Value(mediaFile.mediaId), + type: const Value(MessageType.media), + quotesMessageId: Value( + media.hasQuoteMessageId() ? media.quoteMessageId : null, + ), + createdAt: Value(fromTimestamp(media.timestamp)), + ), + ); + if (message != null) { + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); + Log.info('Inserted a new media message with ID: ${message.messageId}'); + await twonlyDB.groupsDao.incFlameCounter( + message.groupId, + true, + fromTimestamp(media.timestamp), + ); + + unawaited(startDownloadMedia(mediaFile, false)); + } +} + +Future handleMediaUpdate( + int fromUserId, + EncryptedContent_MediaUpdate mediaUpdate, +) async { + final message = await twonlyDB.messagesDao + .getMessageById(mediaUpdate.targetMessageId) + .getSingleOrNull(); + if (message == null) { + Log.error( + 'Got media update to message ${mediaUpdate.targetMessageId} but message not found.', + ); + } + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!); + if (mediaFile == null) { + Log.info( + 'Got media file update, but media file was not found ${message.mediaId}', + ); + return; + } + + switch (mediaUpdate.type) { + case EncryptedContent_MediaUpdate_Type.REOPENED: + Log.info('Got media file reopened ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + reopenByContact: Value(true), + ), + ); + case EncryptedContent_MediaUpdate_Type.STORED: + Log.info('Got media file stored ${mediaFile.mediaId}'); + final mediaService = await MediaFileService.fromMedia(mediaFile); + await mediaService.storeMediaFile(); + + case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: + Log.info('Got media file decryption error ${mediaFile.mediaId}'); + final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? []; + reuploadRequestedBy.add(fromUserId); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + uploadState: const Value(UploadState.uploading), + reuploadRequestedBy: Value(reuploadRequestedBy), + ), + ); + } +} diff --git a/lib/src/services/api/client2client/messages.c2c.dart b/lib/src/services/api/client2client/messages.c2c.dart new file mode 100644 index 0000000..ff87010 --- /dev/null +++ b/lib/src/services/api/client2client/messages.c2c.dart @@ -0,0 +1,55 @@ +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleMessageUpdate( + int contactId, + EncryptedContent_MessageUpdate messageUpdate, +) async { + switch (messageUpdate.type) { + case EncryptedContent_MessageUpdate_Type.OPENED: + for (final targetMessageId in messageUpdate.multipleTargetMessageIds) { + Log.info( + 'Opened message $targetMessageId', + ); + await twonlyDB.messagesDao.handleMessageOpened( + contactId, + targetMessageId, + fromTimestamp(messageUpdate.timestamp), + ); + } + case EncryptedContent_MessageUpdate_Type.DELETE: + if (!await isSender(contactId, messageUpdate.senderMessageId)) { + return; + } + Log.info('Delete message ${messageUpdate.senderMessageId}'); + await twonlyDB.messagesDao.handleMessageDeletion( + contactId, + messageUpdate.senderMessageId, + fromTimestamp(messageUpdate.timestamp), + ); + case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: + if (!await isSender(contactId, messageUpdate.senderMessageId)) { + return; + } + Log.info('Edit message ${messageUpdate.senderMessageId}'); + await twonlyDB.messagesDao.handleTextEdit( + contactId, + messageUpdate.senderMessageId, + messageUpdate.text, + fromTimestamp(messageUpdate.timestamp), + ); + } +} + +Future isSender(int fromUserId, String messageId) async { + final message = + await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); + if (message == null) return false; + if (message.senderId == fromUserId) { + return true; + } + Log.error('Contact $fromUserId tried to modify the message $messageId'); + return false; +} diff --git a/lib/src/services/api/client2client/prekeys.c2c.dart b/lib/src/services/api/client2client/prekeys.c2c.dart new file mode 100644 index 0000000..4f2ed1c --- /dev/null +++ b/lib/src/services/api/client2client/prekeys.c2c.dart @@ -0,0 +1,20 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' + as client; +import 'package:twonly/src/services/signal/identity.signal.dart'; + +Future handleRequestNewPreKey() async { + final localPreKeys = await signalGetPreKeys(); + + final prekeysList = []; + for (var i = 0; i < localPreKeys.length; i++) { + prekeysList.add( + client.Response_PreKey() + ..id = Int64(localPreKeys[i].id) + ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(), + ); + } + final prekeys = client.Response_Prekeys(prekeys: prekeysList); + final ok = client.Response_Ok()..prekeys = prekeys; + return client.Response()..ok = ok; +} diff --git a/lib/src/services/api/client2client/pushkeys.c2c.dart b/lib/src/services/api/client2client/pushkeys.c2c.dart new file mode 100644 index 0000000..6e3cb4d --- /dev/null +++ b/lib/src/services/api/client2client/pushkeys.c2c.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/utils/log.dart'; + +DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); + +Future handlePushKey( + int contactId, + EncryptedContent_PushKeys pushKeys, +) async { + switch (pushKeys.type) { + case EncryptedContent_PushKeys_Type.REQUEST: + Log.info('Got a pushkey request from $contactId'); + if (lastPushKeyRequest + .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { + lastPushKeyRequest = DateTime.now(); + unawaited(setupNotificationWithUsers(forceContact: contactId)); + } + + case EncryptedContent_PushKeys_Type.UPDATE: + Log.info('Got a pushkey update from $contactId'); + await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); + } +} diff --git a/lib/src/services/api/client2client/reaction.c2c.dart b/lib/src/services/api/client2client/reaction.c2c.dart new file mode 100644 index 0000000..5db2fbd --- /dev/null +++ b/lib/src/services/api/client2client/reaction.c2c.dart @@ -0,0 +1,23 @@ +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleReaction( + int fromUserId, + String groupId, + EncryptedContent_Reaction reaction, +) async { + Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})'); + await twonlyDB.reactionsDao.updateReaction( + fromUserId, + reaction.targetMessageId, + groupId, + reaction.emoji, + reaction.remove, + ); + + if (!reaction.remove) { + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, DateTime.now()); + } +} diff --git a/lib/src/services/api/client2client/text_message.c2c.dart b/lib/src/services/api/client2client/text_message.c2c.dart new file mode 100644 index 0000000..f3444cd --- /dev/null +++ b/lib/src/services/api/client2client/text_message.c2c.dart @@ -0,0 +1,39 @@ +import 'package:drift/drift.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future handleTextMessage( + int fromUserId, + String groupId, + EncryptedContent_TextMessage textMessage, +) async { + Log.info( + 'Got a text message: ${textMessage.senderMessageId} from $groupId', + ); + + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + messageId: Value(textMessage.senderMessageId), + senderId: Value(fromUserId), + groupId: Value(groupId), + content: Value(textMessage.text), + type: const Value(MessageType.text), + quotesMessageId: Value( + textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, + ), + createdAt: Value(fromTimestamp(textMessage.timestamp)), + ackByServer: Value(DateTime.now()), + ), + ); + await twonlyDB.groupsDao.increaseLastMessageExchange( + groupId, + fromTimestamp(textMessage.timestamp), + ); + if (message != null) { + Log.info('Inserted a new text message with ID: ${message.messageId}'); + } +} diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/media_download.dart deleted file mode 100644 index 4148d9d..0000000 --- a/lib/src/services/api/media_download.dart +++ /dev/null @@ -1,482 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:background_downloader/background_downloader.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:http/http.dart' as http; -import 'package:mutex/mutex.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; - -Future tryDownloadAllMediaFiles({bool force = false}) async { - // This is called when WebSocket is newly connected, so allow all downloads to be restarted. - final messages = - await twonlyDB.messagesDao.getAllMessagesPendingDownloading(); - - for (final message in messages) { - await startDownloadMedia(message, force); - } -} - -enum DownloadMediaTypes { - video, - image, -} - -Map> defaultAutoDownloadOptions = { - ConnectivityResult.mobile.name: [], - ConnectivityResult.wifi.name: [ - DownloadMediaTypes.video.name, - DownloadMediaTypes.image.name, - ], -}; - -Future isAllowedToDownload(bool isVideo) async { - final connectivityResult = await Connectivity().checkConnectivity(); - - final user = await getUser(); - final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions; - - if (connectivityResult.contains(ConnectivityResult.mobile)) { - if (isVideo) { - if (options[ConnectivityResult.mobile.name]! - .contains(DownloadMediaTypes.video.name)) { - return true; - } - } else if (options[ConnectivityResult.mobile.name]! - .contains(DownloadMediaTypes.image.name)) { - return true; - } - } - if (connectivityResult.contains(ConnectivityResult.wifi)) { - if (isVideo) { - if (options[ConnectivityResult.wifi.name]! - .contains(DownloadMediaTypes.video.name)) { - return true; - } - } else if (options[ConnectivityResult.wifi.name]! - .contains(DownloadMediaTypes.image.name)) { - return true; - } - } - return false; -} - -Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { - final messageId = int.parse(update.task.taskId.replaceAll('download_', '')); - var failed = false; - - if (update.status == TaskStatus.failed || - update.status == TaskStatus.canceled || - update.status == TaskStatus.notFound) { - failed = true; - } else if (update.status == TaskStatus.complete) { - if (update.responseStatusCode == 200) { - failed = false; - } else { - failed = true; - Log.error( - 'Got invalid response status code: ${update.responseStatusCode}', - ); - } - } else { - Log.info('Got ${update.status} for $messageId'); - return; - } - await handleDownloadStatusUpdateInternal(messageId, failed); -} - -Future handleDownloadStatusUpdateInternal( - int messageId, - bool failed, -) async { - if (failed) { - Log.error('Download failed for $messageId'); - final message = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (message != null && message.downloadState != DownloadState.downloaded) { - await handleMediaError(message); - } - } else { - Log.info('Download was successfully for $messageId'); - await handleEncryptedFile(messageId); - } -} - -Mutex protectDownload = Mutex(); - -Future startDownloadMedia(Message message, bool force) async { - Log.info( - 'Download blocked for ${message.messageId} because of network state.', - ); - if (message.contentJson == null) { - Log.error('Content of ${message.messageId} not found.'); - await handleMediaError(message); - return; - } - - final content = MessageContent.fromJson( - message.kind, - jsonDecode(message.contentJson!) as Map, - ); - - if (content is! MediaMessageContent) { - Log.error('Content of ${message.messageId} is not media file.'); - await handleMediaError(message); - return; - } - - if (content.downloadToken == null) { - Log.error('Download token not defined for ${message.messageId}.'); - await handleMediaError(message); - return; - } - - if (!force && !await isAllowedToDownload(content.isVideo)) { - Log.warn( - 'Download blocked for ${message.messageId} because of network state.', - ); - return; - } - - final isBlocked = await protectDownload.protect(() async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageId) - .getSingleOrNull(); - - if (msg == null) return true; - - if (msg.downloadState != DownloadState.pending) { - Log.error( - '${message.messageId} is already downloaded or is downloading.', - ); - return true; - } - - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - downloadState: Value(DownloadState.downloading), - ), - ); - - return false; - }); - - if (isBlocked) { - Log.info('Download for ${message.messageId} already started.'); - return; - } - - final downloadToken = uint8ListToHex(content.downloadToken!); - - final apiUrl = - 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; - - try { - final task = DownloadTask( - url: apiUrl, - taskId: 'download_${message.messageId}', - directory: 'media/received/', - baseDirectory: BaseDirectory.applicationSupport, - filename: '${message.messageId}.encrypted', - priority: 0, - retries: 10, - ); - - Log.info( - 'Got media file. Starting download: ${downloadToken.substring(0, 10)}', - ); - - try { - await downloadFileFast(message.messageId, apiUrl); - } catch (e) { - Log.error('Fast download failed: $e'); - await FileDownloader().enqueue(task); - } - } catch (e) { - Log.error('Exception during download: $e'); - } -} - -Future downloadFileFast( - int messageId, - String apiUrl, -) async { - final directoryPath = - '${(await getApplicationSupportDirectory()).path}/media/received/'; - final filename = '$messageId.encrypted'; - - final directory = Directory(directoryPath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - final filePath = '${directory.path}/$filename'; - - final response = - await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); - - if (response.statusCode == 200) { - await File(filePath).writeAsBytes(response.bodyBytes); - Log.info('Fast Download successful: $filePath'); - await handleDownloadStatusUpdateInternal(messageId, false); - return; - } else { - if (response.statusCode == 404 || response.statusCode == 403) { - await handleDownloadStatusUpdateInternal(messageId, true); - return; - } - // can be tried again - throw Exception('Fast download failed with status: ${response.statusCode}'); - } -} - -Future handleEncryptedFile(int messageId) async { - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (msg == null) { - Log.error('Not message for downloaded file found: $messageId'); - return; - } - - final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted'); - - if (encryptedBytes == null) { - Log.error('encrypted bytes are not found for ${msg.messageId}'); - return; - } - - final content = - MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map); - - try { - final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); - - final secretBox = SecretBox( - encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), - ); - - // try { - final plaintextBytes = - await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } - - await writeMediaFile(msg.messageId, 'png', imageBytes); - // } catch (e) { - // Log.error( - // "could not decrypt the media file in the second try. reporting error to user: $e"); - // handleMediaError(msg); - // return; - // } - } catch (e) { - Log.error('$e'); - - /// legacy support - final chacha20 = Xchacha20.poly1305Aead(); - final secretKeyData = SecretKeyData(content.encryptionKey!); - - final secretBox = SecretBox( - encryptedBytes, - nonce: content.encryptionNonce!, - mac: Mac(content.encryptionMac!), - ); - - try { - final plaintextBytes = - await chacha20.decrypt(secretBox, secretKey: secretKeyData); - var imageBytes = Uint8List.fromList(plaintextBytes); - - if (content.isVideo) { - final extractedBytes = extractUint8Lists(imageBytes); - imageBytes = extractedBytes[0]; - await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]); - } - - await writeMediaFile(msg.messageId, 'png', imageBytes); - } catch (e) { - Log.error( - 'could not decrypt the media file in the second try. reporting error to user: $e', - ); - await handleMediaError(msg); - return; - } - } - - await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId, - const MessagesCompanion(downloadState: Value(DownloadState.downloaded)), - ); - - Log.info('Download and decryption of ${msg.messageId} was successful'); - - await deleteMediaFile(msg.messageId, 'encrypted'); - - unawaited(apiService.downloadDone(content.downloadToken!)); -} - -Future getImageBytes(int mediaId) async { - return readMediaFile(mediaId, 'png'); -} - -Future getVideoPath(int mediaId) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - return File('$basePath.mp4'); -} - -/// --- helper functions --- - -Future readMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - Log.info('Reading: $file'); - if (!file.existsSync()) { - return null; - } - return file.readAsBytes(); -} - -Future existsMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - return file.existsSync(); -} - -Future writeMediaFile(int mediaId, String type, Uint8List data) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); -} - -Future deleteMediaFile(int mediaId, String type) async { - final basePath = await getMediaFilePath(mediaId, 'received'); - final file = File('$basePath.$type'); - try { - if (file.existsSync()) { - await file.delete(); - } - } catch (e) { - Log.error('Error deleting: $e'); - } -} - -Future purgeReceivedMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'received')); - await purgeMediaFiles(directory); -} - -Future purgeMediaFiles(Directory directory) async { - // Check if the directory exists - if (directory.existsSync()) { - // List all files in the directory - final files = directory.listSync(); - - // Iterate over each file - for (final file in files) { - // Get the filename - final filename = file.uri.pathSegments.last; - - // Use a regular expression to extract the integer part - final match = RegExp(r'(\d+)').firstMatch(filename); - if (match != null) { - // Parse the integer and add it to the list - final fileId = int.parse(match.group(0)!); - - try { - if (directory.path.endsWith('send')) { - final messages = - await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId); - var canBeDeleted = true; - - for (final message in messages) { - try { - final content = MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - - final oneDayAgo = - DateTime.now().subtract(const Duration(days: 1)); - final twoDaysAgo = - DateTime.now().subtract(const Duration(days: 1)); - - if ((message.openedAt == null || - oneDayAgo.isBefore(message.openedAt!)) && - !message.errorWhileSending) { - canBeDeleted = false; - } else if (message.mediaStored) { - if (!file.path.contains('.original.') && - !file.path.contains('.encrypted')) { - canBeDeleted = false; - } - } - - /// In case the image is not yet opened but successfully uploaded - /// to the server preserve the image for two days in case of an receiving error will happen - /// and then delete them as well. - if (message.acknowledgeByServer && - twoDaysAgo.isAfter(message.sendAt)) { - // Preserve images which can be stored by the other person... - if (content.maxShowTime != gMediaShowInfinite) { - canBeDeleted = true; - } - // Encrypted or upload data can be removed when acknowledgeByServer - if (file.path.contains('.upload') || - file.path.contains('.encrypted')) { - canBeDeleted = true; - } - } - } catch (e) { - Log.error(e); - } - } - if (canBeDeleted) { - Log.info('purged media file ${file.path} '); - file.deleteSync(); - } - } else { - final message = await twonlyDB.messagesDao - .getMessageByMessageId(fileId) - .getSingleOrNull(); - if ((message == null) || - (message.openedAt != null && - !message.mediaStored && - message.acknowledgeByServer) || - message.errorWhileSending) { - file.deleteSync(); - } - } - } catch (e) { - Log.error('$e'); - } - } - } - } -} - -// /data/user/0/eu.twonly.testing/files/media/received/27.encrypted -// /data/user/0/eu.twonly.testing/app_flutter/data/user/0/eu.twonly.testing/files/media/received/27.encrypted diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/media_upload.dart deleted file mode 100644 index 96c35a4..0000000 --- a/lib/src/services/api/media_upload.dart +++ /dev/null @@ -1,892 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; - -import 'package:background_downloader/background_downloader.dart'; -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:http/http.dart' as http; -import 'package:mutex/mutex.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:video_compress/video_compress.dart'; - -Future isAllowedToSend() async { - final user = await getUser(); - if (user == null) return null; - if (user.subscriptionPlan == 'Free') { - var todaysImageCounter = user.todaysImageCounter; - if (user.lastImageSend != null && user.todaysImageCounter != null) { - if (isToday(user.lastImageSend!)) { - if (user.todaysImageCounter == 10) { - return ErrorCode.PlanLimitReached; - } - todaysImageCounter = user.todaysImageCounter! + 1; - } else { - todaysImageCounter = 1; - } - } else { - todaysImageCounter = 1; - } - await updateUserdata((user) { - user - ..lastImageSend = DateTime.now() - ..todaysImageCounter = todaysImageCounter; - return user; - }); - } - return null; -} - -Future initFileDownloader() async { - FileDownloader().updates.listen((update) async { - switch (update) { - case TaskStatusUpdate(): - if (update.task.taskId.contains('upload_')) { - await handleUploadStatusUpdate(update); - } - if (update.task.taskId.contains('download_')) { - await handleDownloadStatusUpdate(update); - } - if (update.task.taskId.contains('backup')) { - await handleBackupStatusUpdate(update); - } - case TaskProgressUpdate(): - Log.info( - 'Progress update for ${update.task} with progress ${update.progress}', - ); - } - }); - - await FileDownloader().start(); - - try { - var androidConfig = []; - if (kDebugMode) { - androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; - } - await FileDownloader().configure(androidConfig: androidConfig); - } catch (e) { - Log.error(e); - } - - if (kDebugMode) { - FileDownloader().configureNotification( - running: const TaskNotification( - 'Uploading/Downloading', - '{filename} ({progress}).', - ), - progressBar: true, - ); - } -} - -/// States: -/// when user recorded an video -/// 1. Compress video -/// when user clicked the send button (direct send) or share with -/// 2. Encrypt media files -/// 3. Upload media files -/// click send button -/// 4. Finalize upload by websocket -> get download tokens -/// 5. Send all users the message - -/// Create a new entry in the database - -Future checkForFailedUploads() async { - final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); - final mediaUploadIds = []; - for (final message in messages) { - if (mediaUploadIds.contains(message.mediaUploadId)) { - continue; - } - final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( - message.mediaUploadId!, - const MediaUploadsCompanion( - state: Value(UploadState.pending), - encryptionData: Value( - null, // start from scratch e.q. encrypt the files again if already happen - ), - ), - ); - if (affectedRows == 0) { - Log.error( - 'The media from message ${message.messageId} already deleted.', - ); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } else { - mediaUploadIds.add(message.mediaUploadId!); - } - } - if (messages.isNotEmpty) { - Log.error( - 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', - ); - } - return mediaUploadIds.isNotEmpty; // return true if there are affected -} - -final lockingHandleMediaFile = Mutex(); -Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { - if (maxRetries == 0) { - Log.error('retried media upload 3 times. abort retrying'); - return; - } - final retry = await lockingHandleMediaFile.protect(() async { - final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry(); - if (mediaFiles.isEmpty) { - return checkForFailedUploads(); - } - Log.info('re uploading ${mediaFiles.length} media files.'); - for (final mediaFile in mediaFiles) { - if (mediaFile.messageIds == null || mediaFile.metadata == null) { - if (appRestarted) { - /// When the app got restarted and the messageIds or the metadata is not - /// set then the app was closed before the images was send. - await twonlyDB.mediaUploadsDao - .deleteMediaUpload(mediaFile.mediaUploadId); - Log.info( - 'upload can be removed, the finalized function was never called...', - ); - } - continue; - } - - if (mediaFile.state == UploadState.readyToUpload) { - await handleNextMediaUploadSteps(mediaFile.mediaUploadId); - } else { - await handlePreProcessingState(mediaFile); - } - } - return false; - }); - if (retry) { - await retryMediaUpload(false, maxRetries: maxRetries - 1); - } -} - -Future initMediaUpload() async { - return twonlyDB.mediaUploadsDao - .insertMediaUpload(const MediaUploadsCompanion()); -} - -Future addVideoToUpload(int mediaUploadId, File videoFilePath) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - await videoFilePath.copy('$basePath.original.mp4'); - return compressVideoIfExists(mediaUploadId); -} - -Future addOrModifyImageToUpload( - int mediaUploadId, - Uint8List imageBytes, -) async { - Uint8List imageBytesCompressed; - - final stopwatch = Stopwatch()..start(); - - Log.info('Raw images size in bytes: ${imageBytes.length}'); - - try { - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - // minHeight: 0, - // minWidth: 0, - imageBytes, - quality: 90, - ); - - if (imageBytesCompressed.length >= 1 * 1000 * 1000) { - // if the media file is over 2MB compress it with 60% - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - imageBytes, - quality: 60, - ); - } - await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed); - } catch (e) { - Log.error('$e'); - // as a fall back use the original image - await writeSendMediaFile(mediaUploadId, 'png', imageBytes); - imageBytesCompressed = imageBytes; - } - - stopwatch.stop(); - - Log.info( - 'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds', - ); - Log.info('Raw images size in bytes: ${imageBytesCompressed.length}'); - - // stopwatch.reset(); - // stopwatch.start(); - - // // var helper = MediaUploadHelper(); - // try { - // final webpBytes = - // await convertAndCompressImage(pngRawImageBytes: imageBytes); - // Log.info( - // 'Compression the image in rust took: ${stopwatch.elapsedMilliseconds} milliseconds'); - // Log.info("Raw images size in bytes using webp: ${webpBytes.length}"); - // } catch (e) { - // Log.error("$e"); - // } - - /// in case the media file was already encrypted of even uploaded - /// remove the data so it will be done again. - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - const MediaUploadsCompanion( - encryptionData: Value(null), - ), - ); - return imageBytesCompressed; -} - -Future handlePreProcessingState(MediaUpload media) async { - try { - final imageHandler = readSendMediaFile(media.mediaUploadId, 'png'); - final videoHandler = compressVideoIfExists(media.mediaUploadId); - await encryptMediaFiles( - media.mediaUploadId, - imageHandler, - videoHandler, - ); - } catch (e) { - Log.error('${media.mediaUploadId} got error in pre processing: $e'); - await handleUploadError(media); - } -} - -Future encryptMediaFiles( - int mediaUploadId, - Future imageHandler, - Future? videoHandler, -) async { - Log.info('$mediaUploadId encrypting files'); - var dataToEncrypt = await imageHandler; - - /// if there is a video wait until it is finished with compression - if (videoHandler != null) { - if (await videoHandler) { - final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4'); - dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo); - } - } - - final state = MediaEncryptionData(); - - final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKey = await (await chacha20.newSecretKey()).extract(); - - state - ..encryptionKey = secretKey.bytes - ..encryptionNonce = chacha20.newNonce(); - - final secretBox = await chacha20.encrypt( - dataToEncrypt, - secretKey: secretKey, - nonce: state.encryptionNonce, - ); - - state - ..encryptionMac = secretBox.mac.bytes - ..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes; - - final encryptedBytes = Uint8List.fromList(secretBox.cipherText); - await writeSendMediaFile( - mediaUploadId, - 'encrypted', - encryptedBytes, - ); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - state: const Value(UploadState.readyToUpload), - encryptionData: Value(state), - ), - ); - unawaited(handleNextMediaUploadSteps(mediaUploadId)); -} - -Future finalizeUpload( - int mediaUploadId, - List contactIds, - bool isRealTwonly, - bool isVideo, - bool mirrorVideo, - int maxShowTime, -) async { - final metadata = MediaUploadMetadata() - ..contactIds = contactIds - ..isRealTwonly = isRealTwonly - ..messageSendAt = DateTime.now() - ..isVideo = isVideo - ..maxShowTime = maxShowTime - ..mirrorVideo = mirrorVideo; - - final messageIds = []; - - for (final contactId in contactIds) { - final messageId = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(contactId), - kind: const Value(MessageKind.media), - sendAt: Value(metadata.messageSendAt), - downloadState: const Value(DownloadState.pending), - mediaUploadId: Value(mediaUploadId), - contentJson: Value( - jsonEncode( - MediaMessageContent( - maxShowTime: maxShowTime, - isRealTwonly: isRealTwonly, - isVideo: isVideo, - mirrorVideo: mirrorVideo, - ).toJson(), - ), - ), - ), - ); - // de-archive contact when sending a new message - await twonlyDB.contactsDao.updateContact( - contactId, - const ContactsCompanion( - archived: Value(false), - ), - ); - if (messageId != null) { - messageIds.add(messageId); - } else { - Log.error('Error inserting media upload message in database.'); - } - } - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - messageIds: Value(messageIds), - metadata: Value(metadata), - ), - ); - - unawaited(handleNextMediaUploadSteps(mediaUploadId)); -} - -final lockingHandleNextMediaUploadStep = Mutex(); -Future handleNextMediaUploadSteps(int mediaUploadId) async { - await lockingHandleNextMediaUploadStep.protect(() async { - final mediaUpload = await twonlyDB.mediaUploadsDao - .getMediaUploadById(mediaUploadId) - .getSingleOrNull(); - - if (mediaUpload == null) return false; - if (mediaUpload.state == UploadState.receiverNotified || - mediaUpload.state == UploadState.uploadTaskStarted) { - /// Upload done and all users are notified :) - Log.info('$mediaUploadId is already done'); - return false; - } - try { - /// Stage 1: media files are not yet encrypted... - if (mediaUpload.encryptionData == null) { - // when set this function will be called again by encryptAndPreUploadMediaFiles... - return false; - } - - if (mediaUpload.messageIds == null || mediaUpload.metadata == null) { - /// the finalize function was not called yet... - return false; - } - - await handleMediaUpload(mediaUpload); - } catch (e) { - Log.error('Non recoverable error while sending media file: $e'); - await handleUploadError(mediaUpload); - } - return false; - }); -} - -/// -/// -- private functions -- -/// -/// -/// - -Future handleUploadStatusUpdate(TaskStatusUpdate update) async { - var failed = false; - final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', '')); - - final media = await twonlyDB.mediaUploadsDao - .getMediaUploadById(mediaUploadId) - .getSingleOrNull(); - if (media == null) { - Log.error( - 'Got an upload task but no upload media in the media upload database', - ); - return; - } - if (update.status == TaskStatus.failed || - update.status == TaskStatus.canceled) { - Log.error('Upload failed: ${update.status}'); - failed = true; - } else if (update.status == TaskStatus.complete) { - if (update.responseStatusCode == 200) { - await handleUploadSuccess(media); - return; - } else if (update.responseStatusCode != null) { - if (update.responseStatusCode! >= 400 && - update.responseStatusCode! < 500) { - failed = true; - } - Log.error( - 'Got error while uploading: ${update.responseStatusCode}', - ); - } - } - - if (failed) { - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(true), - ), - ); - } - } - Log.info( - 'Status update for ${update.task.taskId} with status ${update.status}', - ); -} - -Future handleUploadSuccess(MediaUpload media) async { - Log.info('Upload of ${media.mediaUploadId} success!'); - currentUploadTasks.remove(media.mediaUploadId); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - media.mediaUploadId, - const MediaUploadsCompanion( - state: Value(UploadState.receiverNotified), - ), - ); - - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), - ), - ); - } -} - -Future handleUploadError(MediaUpload mediaUpload) async { - // if the messageIds are already there notify the user about this error... - if (mediaUpload.messageIds != null) { - for (final messageId in mediaUpload.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } - } - await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId); -} - -Future handleMediaUpload(MediaUpload media) async { - final bytesToUpload = - await readSendMediaFile(media.mediaUploadId, 'encrypted'); - - if (media.messageIds == null) return; - - final messageIds = media.messageIds!; - - final downloadTokens = []; - - final messagesOnSuccess = []; - - for (var i = 0; i < messageIds.length; i++) { - final message = await twonlyDB.messagesDao - .getMessageByMessageId(messageIds[i]) - .getSingleOrNull(); - if (message == null) continue; - - if (message.downloadState == DownloadState.downloaded) { - // only upload message which are not yet uploaded (or in case of an error re-uploaded) - continue; - } - - final downloadToken = createDownloadToken(); - - final msg = MessageJson( - kind: MessageKind.media, - messageSenderId: messageIds[i], - content: MediaMessageContent( - downloadToken: downloadToken, - maxShowTime: media.metadata!.maxShowTime, - isRealTwonly: media.metadata!.isRealTwonly, - isVideo: media.metadata!.isVideo, - mirrorVideo: media.metadata!.mirrorVideo, - encryptionKey: media.encryptionData!.encryptionKey, - encryptionMac: media.encryptionData!.encryptionMac, - encryptionNonce: media.encryptionData!.encryptionNonce, - ), - timestamp: media.metadata!.messageSendAt, - ); - - final plaintextContent = Uint8List.fromList( - gzip.encode(utf8.encode(jsonEncode(msg.toJson()))), - ); - - final contact = await twonlyDB.contactsDao - .getContactByUserId(message.contactId) - .getSingleOrNull(); - - if (contact == null || contact.deleted) { - Log.warn( - 'Contact deleted ${message.contactId} or not found in database.', - ); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion(errorWhileSending: Value(true)), - ); - continue; - } - - await twonlyDB.contactsDao.incFlameCounter( - message.contactId, - false, - message.sendAt, - ); - - final encryptedBytes = await signalEncryptMessage( - message.contactId, - plaintextContent, - ); - - if (encryptedBytes == null) continue; - - final messageOnSuccess = TextMessage() - ..body = encryptedBytes - ..userId = Int64(message.contactId); - - final pushKind = (media.metadata!.isRealTwonly) - ? PushKind.twonly - : (media.metadata!.isVideo) - ? PushKind.video - : PushKind.image; - - final pushData = await getPushData( - message.contactId, - PushNotification( - messageId: Int64(message.messageId), - kind: pushKind, - ), - ); - if (pushData != null) { - messageOnSuccess.pushData = pushData.toList(); - } - - messagesOnSuccess.add(messageOnSuccess); - downloadTokens.add(downloadToken); - } - - final uploadRequest = UploadRequest( - messagesOnSuccess: messagesOnSuccess, - downloadTokens: downloadTokens, - encryptedData: bytesToUpload, - ); - - final uploadRequestBytes = uploadRequest.writeToBuffer(); - - final apiAuthTokenRaw = await const FlutterSecureStorage() - .read(key: SecureStorageKeys.apiAuthToken); - if (apiAuthTokenRaw == null) { - Log.error('api auth token not defined.'); - return; - } - final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); - - final uploadRequestFile = await writeSendMediaFile( - media.mediaUploadId, - 'upload', - uploadRequestBytes, - ); - - final apiUrl = - 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; - - try { - Log.info('Starting upload from ${media.mediaUploadId}'); - - final task = UploadTask.fromFile( - taskId: 'upload_${media.mediaUploadId}', - displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video', - file: uploadRequestFile, - url: apiUrl, - priority: 0, - retries: 10, - headers: { - 'x-twonly-auth-token': apiAuthToken, - }, - ); - - currentUploadTasks[media.mediaUploadId] = task; - - try { - await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken); - } catch (e) { - Log.error('Fast upload failed: $e. Using slow method directly.'); - await enqueueUploadTask(media.mediaUploadId); - } - } catch (e) { - Log.error('Exception during upload: $e'); - } -} - -Map currentUploadTasks = {}; - -Future enqueueUploadTask(int mediaUploadId) async { - if (currentUploadTasks[mediaUploadId] == null) { - Log.info('could not enqueue upload task: $mediaUploadId'); - return; - } - - Log.info('Enqueue upload task: $mediaUploadId'); - - await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!); - currentUploadTasks.remove(mediaUploadId); - - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - const MediaUploadsCompanion( - state: Value(UploadState.uploadTaskStarted), - ), - ); -} - -Future handleUploadWhenAppGoesBackground() async { - if (currentUploadTasks.keys.isEmpty) { - return; - } - Log.info('App goes into background. Enqueue uploads to the background.'); - final keys = currentUploadTasks.keys.toList(); - for (final key in keys) { - await enqueueUploadTask(key); - } -} - -Future uploadFileFast( - MediaUpload media, - Uint8List uploadRequestFile, - String apiUrl, - String apiAuthToken, -) async { - final requestMultipart = http.MultipartRequest( - 'POST', - Uri.parse(apiUrl), - ); - requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; - - requestMultipart.files.add( - http.MultipartFile.fromBytes( - 'file', - uploadRequestFile, - filename: 'upload', - ), - ); - - final response = await requestMultipart.send(); - if (response.statusCode == 200) { - Log.info('Upload successful!'); - await handleUploadSuccess(media); - return; - } else { - Log.info('Upload failed with status: ${response.statusCode}'); - } -} - -Future compressVideoIfExists(int mediaUploadId) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final videoOriginalFile = File('$basePath.original.mp4'); - final videoCompressedFile = File('$basePath.mp4'); - - if (videoCompressedFile.existsSync()) { - // file is already compressed and exists - return true; - } - - if (!videoOriginalFile.existsSync()) { - // media upload does not have a video - return false; - } - - final stopwatch = Stopwatch()..start(); - - MediaInfo? mediaInfo; - try { - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res1280x720Quality, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 - ); - - Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); - - if (mediaInfo.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res960x540Quality, - includeAudio: true, - ); - } - } catch (e) { - Log.error('during video compression: $e'); - } - stopwatch.stop(); - Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); - - if (mediaInfo == null) { - Log.error('could not compress video.'); - // as a fall back use the non compressed version - await videoOriginalFile.copy(videoCompressedFile.path); - await videoOriginalFile.delete(); - } else { - await mediaInfo.file!.copy(videoCompressedFile.path); - await mediaInfo.file!.delete(); - } - return true; -} - -/// --- helper functions --- - -Future readSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (!file.existsSync()) { - throw Exception('$file not found'); - } - return file.readAsBytes(); -} - -Future writeSendMediaFile( - int mediaUploadId, - String type, - Uint8List data, -) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); - return file; -} - -Future deleteSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (file.existsSync()) { - await file.delete(); - } -} - -Future getMediaFilePath(dynamic mediaId, String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return join(mediaSendDir.path, '$mediaId'); -} - -Future getMediaBaseFilePath(String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return mediaSendDir.path; -} - -/// combines two utf8 list -Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) { - final combinedLength = 4 + list1.length + list2.length; - final combinedList = Uint8List(combinedLength); - ByteData.sublistView(combinedList).setInt32(0, list1.length); - combinedList - ..setRange(4, 4 + list1.length, list1) - ..setRange(4 + list1.length, combinedLength, list2); - return combinedList; -} - -List extractUint8Lists(Uint8List combinedList) { - final byteData = ByteData.sublistView(combinedList); - final sizeOfList1 = byteData.getInt32(0); - final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1); - final list2 = Uint8List.view( - combinedList.buffer, - 4 + sizeOfList1, - combinedList.lengthInBytes - 4 - sizeOfList1, - ); - return [list1, list2]; -} - -Future purgeSendMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'send')); - await purgeMediaFiles(directory); -} - -String uint8ListToHex(List bytes) { - return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); -} - -Uint8List hexToUint8List(String hex) => Uint8List.fromList( - List.generate( - hex.length ~/ 2, - (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), - ), - ); - -Uint8List createDownloadToken() { - final random = Random(); - - final token = Uint8List(32); - for (var j = 0; j < 32; j++) { - token[j] = random.nextInt(256); // Generate a random byte (0-255) - } - return token; -} diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart new file mode 100644 index 0000000..ec8f659 --- /dev/null +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -0,0 +1,307 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart'; +import 'package:http/http.dart' as http; +import 'package:mutex/mutex.dart'; +import 'package:path/path.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future tryDownloadAllMediaFiles({bool force = false}) async { + // This is called when WebSocket is newly connected, so allow all downloads to be restarted. + final mediaFiles = + await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload(); + + for (final mediaFile in mediaFiles) { + await startDownloadMedia(mediaFile, force); + } +} + +enum DownloadMediaTypes { + video, + image, + audio, +} + +Map> defaultAutoDownloadOptions = { + ConnectivityResult.mobile.name: [ + DownloadMediaTypes.audio.name, + ], + ConnectivityResult.wifi.name: [ + DownloadMediaTypes.video.name, + DownloadMediaTypes.image.name, + DownloadMediaTypes.audio.name, + ], +}; + +Future isAllowedToDownload(MediaType type) async { + final connectivityResult = await Connectivity().checkConnectivity(); + + final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; + + if (connectivityResult.contains(ConnectivityResult.mobile)) { + if (type == MediaType.video) { + if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.video.name)) { + return true; + } else if (type == MediaType.audio) { + if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.audio.name)) { + return true; + } + } else if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.image.name)) { + return true; + } + } + } + if (connectivityResult.contains(ConnectivityResult.wifi)) { + if (type == MediaType.video) { + if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.video.name)) { + return true; + } + } else if (type == MediaType.audio) { + if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.audio.name)) { + return true; + } + } else if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.image.name)) { + return true; + } + } + return false; +} + +Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { + final mediaId = update.task.taskId.replaceAll('download_', ''); + var failed = false; + + if (update.status == TaskStatus.failed || + update.status == TaskStatus.canceled || + update.status == TaskStatus.notFound) { + failed = true; + } else if (update.status == TaskStatus.complete) { + if (update.responseStatusCode == 200) { + failed = false; + } else { + failed = true; + Log.error( + 'Got invalid response status code: ${update.responseStatusCode}', + ); + } + } else { + Log.info('Got ${update.status} for $mediaId'); + return; + } + + if (failed) { + Log.error('Background media upload failed: ${update.status}'); + await requestMediaReupload(mediaId); + } else { + await handleEncryptedFile(mediaId); + } +} + +Mutex protectDownload = Mutex(); + +Future startDownloadMedia(MediaFile media, bool force) async { + final mediaService = await MediaFileService.fromMedia(media); + + if (mediaService.encryptedPath.existsSync()) { + await handleEncryptedFile(media.mediaId); + return; + } + + if (!force && !await isAllowedToDownload(media.type)) { + Log.warn( + 'Download blocked for ${media.mediaId} because of network state.', + ); + return; + } + + final isBlocked = await protectDownload.protect(() async { + final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId); + + if (msg == null || msg.downloadState != DownloadState.pending) { + return true; + } + + await twonlyDB.mediaFilesDao.updateMedia( + msg.mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.downloading), + ), + ); + + return false; + }); + + if (isBlocked) { + Log.info('Download for ${media.mediaId} already started.'); + return; + } + + if (media.downloadToken == null) { + Log.info('Download token for ${media.mediaId} not found.'); + return; + } + + final downloadToken = uint8ListToHex(media.downloadToken!); + + final apiUrl = + 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; + + try { + final task = DownloadTask( + url: apiUrl, + taskId: 'download_${media.mediaId}', + directory: mediaService.encryptedPath.parent.path, + baseDirectory: BaseDirectory.root, + filename: basename(mediaService.encryptedPath.path), + priority: 0, + retries: 10, + ); + + Log.info( + 'Downloading ${media.mediaId} to ${mediaService.encryptedPath}', + ); + + try { + await downloadFileFast(media, apiUrl, mediaService.encryptedPath); + } catch (e) { + Log.error('Fast download failed: $e'); + await FileDownloader().enqueue(task); + } + } catch (e) { + Log.error('Exception during download: $e'); + } +} + +Future downloadFileFast( + MediaFile media, + String apiUrl, + File filePath, +) async { + final response = + await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + await filePath.writeAsBytes(response.bodyBytes); + Log.info('Fast Download successful: $filePath'); + await handleEncryptedFile(media.mediaId); + return; + } else { + if (response.statusCode == 404 || + response.statusCode == 403 || + response.statusCode == 400) { + Log.error( + 'Got ${response.statusCode} from server. Requesting upload again', + ); + // Message was deleted from the server. Requesting it again from the sender to upload it again... + await requestMediaReupload(media.mediaId); + return; + } + // Will be tried again using the slow method... + throw Exception('Fast download failed with status: ${response.statusCode}'); + } +} + +Future requestMediaReupload(String mediaId) async { + final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + if (messages.length != 1 || messages.first.senderId == null) { + Log.error( + 'Media file has none or more than one sender. That is not possible', + ); + return; + } + + await sendCipherText( + messages.first.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, + targetMessageId: messages.first.messageId, + ), + ), + ); + + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.reuploadRequested), + ), + ); +} + +Future handleEncryptedFile(String mediaId) async { + final mediaService = await MediaFileService.fromMediaId(mediaId); + if (mediaService == null) { + Log.error('Media file $mediaId not found in database.'); + return; + } + + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.downloaded), + ), + ); + + late Uint8List encryptedBytes; + try { + encryptedBytes = await mediaService.encryptedPath.readAsBytes(); + } catch (e) { + Log.error('Could not read encrypted media file: $mediaId. $e'); + await requestMediaReupload(mediaId); + return; + } + + try { + final chacha20 = FlutterChacha20.poly1305Aead(); + final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!); + + final secretBox = SecretBox( + encryptedBytes, + nonce: mediaService.mediaFile.encryptionNonce!, + mac: Mac(mediaService.mediaFile.encryptionMac!), + ); + + final plaintextBytes = + await chacha20.decrypt(secretBox, secretKey: secretKeyData); + + final rawMediaBytes = Uint8List.fromList(plaintextBytes); + + await mediaService.tempPath.writeAsBytes(rawMediaBytes); + } catch (e) { + Log.error( + 'Could not decrypt the media file. Requesting a new upload.', + ); + await requestMediaReupload(mediaId); + return; + } + + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.ready), + ), + ); + + Log.info('Decryption of $mediaId was successful'); + + mediaService.encryptedPath.deleteSync(); + + unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!)); +} diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart new file mode 100644 index 0000000..6e513d2 --- /dev/null +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -0,0 +1,135 @@ +import 'dart:async'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/foundation.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future initFileDownloader() async { + FileDownloader().updates.listen((update) async { + switch (update) { + case TaskStatusUpdate(): + if (update.task.taskId.contains('upload_')) { + await handleUploadStatusUpdate(update); + } + if (update.task.taskId.contains('download_')) { + await handleDownloadStatusUpdate(update); + } + if (update.task.taskId.contains('backup')) { + await handleBackupStatusUpdate(update); + } + case TaskProgressUpdate(): + Log.info( + 'Progress update for ${update.task} with progress ${update.progress}', + ); + } + }); + + await FileDownloader().start(); + + try { + var androidConfig = []; + if (!kReleaseMode) { + androidConfig = [(Config.bypassTLSCertificateValidation, true)]; + } + await FileDownloader().configure(androidConfig: androidConfig); + } catch (e) { + Log.error(e); + } + + if (!kReleaseMode) { + FileDownloader().configureNotification( + running: const TaskNotification( + 'Uploading/Downloading', + '{filename} ({progress}).', + ), + progressBar: true, + ); + } +} + +Future handleUploadStatusUpdate(TaskStatusUpdate update) async { + final mediaId = update.task.taskId.replaceAll('upload_', ''); + final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + + if (update.status == TaskStatus.running) { + // Ignore these updates + return; + } + + if (media == null) { + Log.error( + 'Got an upload task but no upload media in the media upload database', + ); + return; + } + + if (update.status == TaskStatus.complete) { + if (update.responseStatusCode == 200) { + Log.info('Upload of ${media.mediaId} success!'); + + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploaded), + ), + ); + + /// As the messages where send in a bulk acknowledge all messages. + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); + for (final message in messages) { + final contacts = + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); + for (final contact in contacts) { + await twonlyDB.messagesDao.handleMessageAckByServer( + contact.contactId, + message.messageId, + DateTime.now(), + ); + } + } + + return; + } + Log.error( + 'Got HTTP error ${update.responseStatusCode} for $mediaId', + ); + } + + if (update.status == TaskStatus.notFound) { + await twonlyDB.mediaFilesDao.updateMedia( + mediaId, + const MediaFilesCompanion( + uploadState: Value(UploadState.uploadLimitReached), + ), + ); + Log.info( + 'Background upload failed for $mediaId with status ${update.responseStatusCode}. Not trying again.', + ); + return; + } + + Log.info( + 'Background 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); + + await mediaService.setUploadState(UploadState.uploaded); + // In all other cases just try the upload again... + await startBackgroundMediaUpload(mediaService); + } +} diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart new file mode 100644 index 0000000..7fa39d9 --- /dev/null +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -0,0 +1,326 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mutex/mutex.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/constants/secure_storage_keys.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future finishStartedPreprocessing() async { + final mediaFiles = + await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); + + for (final mediaFile in mediaFiles) { + if (mediaFile.isDraftMedia) { + continue; + } + try { + final service = await MediaFileService.fromMedia(mediaFile); + if (!service.originalPath.existsSync() && + !service.uploadRequestPath.existsSync()) { + if (service.storedPath.existsSync()) { + // media files was just stored.. + continue; + } + Log.info( + 'Deleted media files, as originalPath and uploadRequestPath both do not exists', + ); + // the file does not exists anymore. + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + continue; + } + await startBackgroundMediaUpload(service); + } catch (e) { + Log.error(e); + } + } +} + +Future initializeMediaUpload( + MediaType type, + int? displayLimitInMilliseconds, { + bool isDraftMedia = false, +}) async { + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionKey = await (await chacha20.newSecretKey()).extract(); + final encryptionNonce = chacha20.newNonce(); + + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion(isDraftMedia: Value(false)), + ); + + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + uploadState: const Value(UploadState.initialized), + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)), + encryptionNonce: Value(Uint8List.fromList(encryptionNonce)), + isDraftMedia: Value(isDraftMedia), + type: Value(type), + ), + ); + if (mediaFile == null) return null; + return MediaFileService.fromMedia(mediaFile); +} + +Future insertMediaFileInMessagesTable( + MediaFileService mediaService, + List groupIds, +) async { + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); + for (final groupId in groupIds) { + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + mediaId: Value(mediaService.mediaFile.mediaId), + type: const Value(MessageType.media), + ), + ); + await twonlyDB.groupsDao + .increaseLastMessageExchange(groupId, DateTime.now()); + if (message != null) { + // de-archive contact when sending a new message + await twonlyDB.groupsDao.updateGroup( + message.groupId, + const GroupsCompanion( + archived: Value(false), + deletedContent: Value(false), + ), + ); + } else { + Log.error('Error inserting media upload message in database.'); + } + } + + unawaited(startBackgroundMediaUpload(mediaService)); +} + +Future startBackgroundMediaUpload(MediaFileService mediaService) async { + if (mediaService.mediaFile.uploadState == UploadState.initialized || + mediaService.mediaFile.uploadState == UploadState.preprocessing) { + await mediaService.setUploadState(UploadState.preprocessing); + + if (!mediaService.tempPath.existsSync()) { + await mediaService.compressMedia(); + if (!mediaService.tempPath.existsSync()) { + return; + } + } + + if (!mediaService.encryptedPath.existsSync()) { + await _encryptMediaFiles(mediaService); + if (!mediaService.encryptedPath.existsSync()) { + return; + } + } + + if (!mediaService.uploadRequestPath.existsSync()) { + await _createUploadRequest(mediaService); + } + + if (mediaService.uploadRequestPath.existsSync()) { + await mediaService.setUploadState(UploadState.uploading); + // at this point the original file is not used any more, so it can be deleted + mediaService.originalPath.deleteSync(); + } + } + + if (mediaService.mediaFile.uploadState == UploadState.uploading || + mediaService.mediaFile.uploadState == UploadState.uploadLimitReached) { + await _uploadUploadRequest(mediaService); + } +} + +Future _encryptMediaFiles(MediaFileService mediaService) async { + /// if there is a video wait until it is finished with compression + + if (!mediaService.tempPath.existsSync()) { + Log.error('Could not encrypted image as it does not exists'); + return; + } + + final dataToEncrypt = await mediaService.tempPath.readAsBytes(); + + final chacha20 = FlutterChacha20.poly1305Aead(); + + final secretBox = await chacha20.encrypt( + dataToEncrypt, + secretKey: SecretKey(mediaService.mediaFile.encryptionKey!), + nonce: mediaService.mediaFile.encryptionNonce, + ); + + await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes)); + + mediaService.encryptedPath + .writeAsBytesSync(Uint8List.fromList(secretBox.cipherText)); +} + +Future _createUploadRequest(MediaFileService media) async { + final downloadTokens = []; + + final messagesOnSuccess = []; + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId); + + if (messages.isEmpty) { + // There where no user selected who should receive the image, so waiting with this step... + return; + } + + for (final message in messages) { + final groupMembers = + await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); + + if (media.mediaFile.reuploadRequestedBy == null) { + await twonlyDB.groupsDao.incFlameCounter( + message.groupId, + false, + message.createdAt, + ); + } + + for (final groupMember in groupMembers) { + /// only send the upload to the users + if (media.mediaFile.reuploadRequestedBy != null) { + if (!media.mediaFile.reuploadRequestedBy! + .contains(groupMember.contactId)) { + continue; + } + } + + final downloadToken = getRandomUint8List(32); + + late EncryptedContent_Media_Type type; + switch (media.mediaFile.type) { + case MediaType.audio: + type = EncryptedContent_Media_Type.AUDIO; + case MediaType.image: + type = EncryptedContent_Media_Type.IMAGE; + case MediaType.gif: + type = EncryptedContent_Media_Type.GIF; + case MediaType.video: + type = EncryptedContent_Media_Type.VIDEO; + } + + final notEncryptedContent = EncryptedContent( + groupId: message.groupId, + media: EncryptedContent_Media( + senderMessageId: message.messageId, + type: type, + requiresAuthentication: media.mediaFile.requiresAuthentication, + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), + downloadToken: downloadToken.toList(), + encryptionKey: media.mediaFile.encryptionKey, + encryptionNonce: media.mediaFile.encryptionNonce, + encryptionMac: media.mediaFile.encryptionMac, + ), + ); + + if (media.mediaFile.displayLimitInMilliseconds != null) { + notEncryptedContent.media.displayLimitInMilliseconds = + Int64(media.mediaFile.displayLimitInMilliseconds!); + } + + final cipherText = await sendCipherText( + groupMember.contactId, + notEncryptedContent, + onlyReturnEncryptedData: true, + ); + + if (cipherText == null) { + Log.error( + 'Could not generate ciphertext message for ${groupMember.contactId}', + ); + } + + final messageOnSuccess = TextMessage() + ..body = cipherText!.$1 + ..userId = Int64(groupMember.contactId); + + if (cipherText.$2 != null) { + messageOnSuccess.pushData = cipherText.$2!; + } + + messagesOnSuccess.add(messageOnSuccess); + downloadTokens.add(downloadToken); + } + } + + final bytesToUpload = await media.encryptedPath.readAsBytes(); + + final uploadRequest = UploadRequest( + messagesOnSuccess: messagesOnSuccess, + downloadTokens: downloadTokens, + encryptedData: bytesToUpload, + ); + + final uploadRequestBytes = uploadRequest.writeToBuffer(); + + await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); +} + +Mutex protectUpload = Mutex(); + +Future _uploadUploadRequest(MediaFileService media) async { + await protectUpload.protect(() async { + final currentMedia = + await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId); + + if (currentMedia == null || + currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) { + Log.info('Download for ${media.mediaFile.mediaId} already started.'); + return null; + } + + final apiAuthTokenRaw = await const FlutterSecureStorage() + .read(key: SecureStorageKeys.apiAuthToken); + + if (apiAuthTokenRaw == null) { + Log.error('api auth token not defined.'); + return null; + } + final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); + + final apiUrl = + 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; + + // try { + Log.info('Starting upload from ${media.mediaFile.mediaId}'); + + final task = UploadTask.fromFile( + taskId: 'upload_${media.mediaFile.mediaId}', + displayName: media.mediaFile.type.name, + file: media.uploadRequestPath, + url: apiUrl, + priority: 0, + retries: 10, + headers: { + 'x-twonly-auth-token': apiAuthToken, + }, + ); + + Log.info('Enqueue upload task: ${task.taskId}'); + + await FileDownloader().enqueue(task); + + await media.setUploadState(UploadState.backgroundUploadTaskStarted); + }); +} diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index c939546..b2503fc 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,301 +1,308 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; - -import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; -import 'package:twonly/src/services/api/server_messages.dart' - show messageGetsAck; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/utils/misc.dart'; final lockRetransmission = Mutex(); Future tryTransmitMessages() async { return lockRetransmission.protect(() async { - final retransIds = - await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages(); + final receipts = await twonlyDB.receiptsDao.getReceiptsNotAckByServer(); - Log.info('Retransmitting ${retransIds.length} text messages'); + if (receipts.isEmpty) return; - if (retransIds.isEmpty) return; + Log.info('Reuploading ${receipts.length} messages to the server.'); - for (final retransId in retransIds) { - await sendRetransmitMessage(retransId); + for (final receipt in receipts) { + await tryToSendCompleteMessage(receipt: receipt); } }); } -Future sendRetransmitMessage(int retransId) async { +// When the ackByServerAt is set this value is written in the receipted +Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ + String? receiptId, + Receipt? receipt, + bool onlyReturnEncryptedData = false, +}) async { try { - final retrans = await twonlyDB.messageRetransmissionDao - .getRetransmissionById(retransId) - .getSingleOrNull(); - - if (retrans == null) { - Log.error('$retransId not found in database'); - return; - } - - if (retrans.acknowledgeByServerAt != null) { - Log.error('$retransId message already retransmitted'); - return; - } - - final json = MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(retrans.plaintextContent), - ), - ) as Map, - ); - - Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}'); - - final contact = await twonlyDB.contactsDao - .getContactByUserId(retrans.contactId) - .getSingleOrNull(); - if (contact == null || contact.deleted) { - Log.warn('Contact deleted $retransId or not found in database.'); - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(retransId); - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, - const MessagesCompanion(errorWhileSending: Value(true)), - ); + if (receiptId == null && receipt == null) return null; + if (receipt == null) { + receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); + if (receipt == null) { + Log.error('Receipt $receiptId not found.'); + return null; } - return; + } + receiptId = receipt.receiptId; + + if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { + Log.error('$receiptId message already uploaded!'); + return null; } - final encryptedBytes = await signalEncryptMessage( - retrans.contactId, - retrans.plaintextContent, + Log.info('Uploading $receiptId (Message to ${receipt.contactId})'); + + final message = pb.Message.fromBuffer(receipt.message) + ..receiptId = receiptId; + + final encryptedContent = + pb.EncryptedContent.fromBuffer(message.encryptedContent); + + final pushNotification = await getPushNotificationFromEncryptedContent( + receipt.contactId, + receipt.messageId, + encryptedContent, ); - if (encryptedBytes == null) { - Log.error('Could not encrypt the message. Aborting and trying again.'); - return; + Uint8List? pushData; + if (pushNotification != null && receipt.retryCount <= 3) { + /// In case the message has to be resend more than three times, do not show a notification again... + pushData = + await encryptPushNotification(receipt.contactId, pushNotification); } - final encryptedHash = (await Sha256().hash(encryptedBytes)).bytes; + if (message.type == pb.Message_Type.TEST_NOTIFICATION) { + pushData = (PushNotification()..kind = PushKind.testNotification) + .writeToBuffer(); + } - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - encryptedHash: Value(Uint8List.fromList(encryptedHash)), - ), - ); + if (message.type == pb.Message_Type.CIPHERTEXT) { + final cipherText = await signalEncryptMessage( + receipt.contactId, + Uint8List.fromList(message.encryptedContent), + ); + if (cipherText == null) { + Log.error('Could not encrypt the message. Aborting and trying again.'); + return null; + } + message.encryptedContent = cipherText.serialize(); + switch (cipherText.getType()) { + case CiphertextMessage.prekeyType: + message.type = pb.Message_Type.PREKEY_BUNDLE; + case CiphertextMessage.whisperType: + message.type = pb.Message_Type.CIPHERTEXT; + default: + Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); + return null; + } + } + + if (onlyReturnEncryptedData) { + return (message.writeToBuffer(), pushData); + } final resp = await apiService.sendTextMessage( - retrans.contactId, - encryptedBytes, - retrans.pushData, + receipt.contactId, + message.writeToBuffer(), + pushData, ); - var retry = true; - if (resp.isError) { - Log.error('Could not retransmit message.'); + Log.error('Could not transmit message $receiptId got ${resp.error}.'); if (resp.error == ErrorCode.UserIdNotFound) { - retry = false; - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, - const MessagesCompanion(errorWhileSending: Value(true)), - ); - } + await twonlyDB.receiptsDao.deleteReceipt(receiptId); await twonlyDB.contactsDao.updateContact( - retrans.contactId, - const ContactsCompanion(deleted: Value(true)), + receipt.contactId, + const ContactsCompanion(accountDeleted: Value(true)), ); + return null; } } if (resp.isSuccess) { - retry = false; - if (retrans.messageId != null) { - await twonlyDB.messagesDao.updateMessageByMessageId( - retrans.messageId!, - const MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), - ), + if (receipt.messageId != null) { + await twonlyDB.messagesDao.handleMessageAckByServer( + receipt.contactId, + receipt.messageId!, + DateTime.now(), ); } - } - - if (!retry) { - if (!messageGetsAck(json.kind)) { - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(retransId); + if (!receipt.contactWillSendsReceipt) { + await twonlyDB.receiptsDao.deleteReceipt(receiptId); } else { - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(DateTime.now()), - retryCount: Value(retrans.retryCount + 1), + await twonlyDB.receiptsDao.updateReceipt( + receiptId, + ReceiptsCompanion( + ackByServerAt: Value(DateTime.now()), + retryCount: Value(receipt.retryCount + 1), lastRetry: Value(DateTime.now()), ), ); } } } catch (e) { - Log.error('error resending message: $e'); - await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId); + Log.error('Unknown Error when sending message: $e'); + if (receiptId != null) { + await twonlyDB.receiptsDao.deleteReceipt(receiptId); + } + if (receipt != null) { + await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); + } } + return null; } -// encrypts and stores the message and then sends it in the background -Future encryptAndSendMessageAsync( - int? messageId, - int userId, - MessageJson msg, { - PushNotification? pushNotification, -}) async { - Uint8List? pushData; - if (pushNotification != null) { - pushData = await getPushData(userId, pushNotification); - } - - final retransId = - await twonlyDB.messageRetransmissionDao.insertRetransmission( - MessageRetransmissionsCompanion( - contactId: Value(userId), - messageId: Value(messageId), - plaintextContent: Value(Uint8List(0)), - pushData: Value(pushData), +Future insertAndSendTextMessage( + String groupId, + String textMessage, + String? quotesMessageId, +) async { + final message = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + groupId: Value(groupId), + content: Value(textMessage), + type: const Value(MessageType.text), + quotesMessageId: Value(quotesMessageId), ), ); - - if (retransId == null) { - Log.error('Could not insert the message into the retransmission database'); + if (message == null) { + Log.error('Could not insert message into database'); return; } - msg.retransId = retransId; - - final plaintextContent = - Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); - - await twonlyDB.messageRetransmissionDao.updateRetransmission( - retransId, - MessageRetransmissionsCompanion( - plaintextContent: Value(plaintextContent), + final encryptedContent = pb.EncryptedContent( + textMessage: pb.EncryptedContent_TextMessage( + senderMessageId: message.messageId, + text: textMessage, + timestamp: Int64(message.createdAt.millisecondsSinceEpoch), ), ); - // this can now be done in the background... - unawaited(sendRetransmitMessage(retransId)); + if (quotesMessageId != null) { + encryptedContent.textMessage.quoteMessageId = quotesMessageId; + } + + await sendCipherTextToGroup( + groupId, + encryptedContent, + messageId: message.messageId, + ); } -Future sendTextMessage( - int target, - TextMessageContent content, - PushNotification? pushNotification, -) async { - final messageSendAt = DateTime.now(); - DateTime? openedAt; +Future sendCipherTextToGroup( + String groupId, + pb.EncryptedContent encryptedContent, { + String? messageId, +}) async { + final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); - if (pushNotification != null && pushNotification.hasReactionContent()) { - openedAt = DateTime.now(); - } + await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); - final messageId = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(target), - kind: const Value(MessageKind.textMessage), - sendAt: Value(messageSendAt), - responseToOtherMessageId: Value(content.responseToMessageId), - responseToMessageId: Value(content.responseToOtherMessageId), - downloadState: const Value(DownloadState.downloaded), - openedAt: Value(openedAt), - contentJson: Value( - jsonEncode(content.toJson()), + encryptedContent.groupId = groupId; + + for (final groupMember in groupMembers) { + unawaited( + sendCipherText( + groupMember.contactId, + encryptedContent, + messageId: messageId, ), + ); + } +} + +Future<(Uint8List, Uint8List?)?> sendCipherText( + int contactId, + pb.EncryptedContent encryptedContent, { + bool onlyReturnEncryptedData = false, + String? messageId, +}) async { + encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); + + final response = pb.Message() + ..type = pb.Message_Type.CIPHERTEXT + ..encryptedContent = encryptedContent.writeToBuffer(); + + final receipt = await twonlyDB.receiptsDao.insertReceipt( + ReceiptsCompanion( + contactId: Value(contactId), + message: Value(response.writeToBuffer()), + messageId: Value(messageId), + ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), ), ); - if (messageId == null) return; - - if (pushNotification != null && !pushNotification.hasReactionContent()) { - pushNotification.messageId = Int64(messageId); + if (receipt != null) { + return tryToSendCompleteMessage( + receipt: receipt, + onlyReturnEncryptedData: onlyReturnEncryptedData, + ); } - - final msg = MessageJson( - kind: MessageKind.textMessage, - messageSenderId: messageId, - content: content, - timestamp: messageSendAt, - ); - - await encryptAndSendMessageAsync( - messageId, - target, - msg, - pushNotification: pushNotification, - ); + return null; } Future notifyContactAboutOpeningMessage( - int fromUserId, - List messageOtherIds, + int contactId, + List messageOtherIds, ) async { var biggestMessageId = messageOtherIds.first; for (final messageOtherId in messageOtherIds) { - if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId; - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.opened, - messageReceiverId: messageOtherId, - content: MessageContent(), - timestamp: DateTime.now(), + if (isUUIDNewer(messageOtherId, biggestMessageId)) { + biggestMessageId = messageOtherId; + } + } + Log.info('Opened messages: $messageOtherIds'); + + final actionAt = DateTime.now(); + + await sendCipherText( + contactId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.OPENED, + multipleTargetMessageIds: messageOtherIds, + timestamp: Int64(actionAt.millisecondsSinceEpoch), + ), + ), + ); + for (final messageId in messageOtherIds) { + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion( + openedAt: Value(actionAt), + openedByAll: Value(actionAt), ), ); } - await updateLastMessageId(fromUserId, biggestMessageId); + await updateLastMessageId(contactId, biggestMessageId); } -Future notifyContactsAboutProfileChange() async { +Future notifyContactsAboutProfileChange({int? onlyToContact}) async { + if (gUser.avatarSvg == null) return; + + final encryptedContent = pb.EncryptedContent( + contactUpdate: pb.EncryptedContent_ContactUpdate( + type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, + avatarSvgCompressed: gzip.encode(utf8.encode(gUser.avatarSvg!)), + displayName: gUser.displayName, + username: gUser.username, + ), + ); + + if (onlyToContact != null) { + await sendCipherText(onlyToContact, encryptedContent); + return; + } + final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); - final user = await getUser(); - if (user == null) return; - if (user.avatarSvg == null) return; - for (final contact in contacts) { - if (contact.myAvatarCounter < user.avatarCounter) { - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( - myAvatarCounter: Value(user.avatarCounter), - ), - ); - await encryptAndSendMessageAsync( - null, - contact.userId, - MessageJson( - kind: MessageKind.profileChange, - content: ProfileContent( - avatarSvg: user.avatarSvg!, - displayName: user.displayName, - ), - timestamp: DateTime.now(), - ), - ); - } + await sendCipherText(contact.userId, encryptedContent); } } diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index dbe4f7b..58c37f8 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,490 +1,328 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; +import 'package:hashlib/random.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; -import 'package:twonly/src/services/api/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; +import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; +import 'package:twonly/src/services/api/client2client/media.c2c.dart'; +import 'package:twonly/src/services/api/client2client/messages.c2c.dart'; +import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart'; +import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart'; +import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; +import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; -import 'package:twonly/src/services/signal/identity.signal.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/animate_icon.dart'; final lockHandleServerMessage = Mutex(); Future handleServerMessage(server.ServerToClient msg) async { - return lockHandleServerMessage.protect(() async { - client.Response? response; + /// Returns means, that the server can delete the message from the server. + final ok = client.Response_Ok()..none = true; + var response = client.Response()..ok = ok; - try { - if (msg.v0.hasRequestNewPreKeys()) { - response = await handleRequestNewPreKey(); - } else if (msg.v0.hasNewMessage()) { - final body = Uint8List.fromList(msg.v0.newMessage.body); - final fromUserId = msg.v0.newMessage.fromUserId.toInt(); - response = await handleNewMessage(fromUserId, body); - } else { - Log.error('Got a unknown message from the server: $msg'); - response = client.Response()..error = ErrorCode.InternalError; - } - } catch (e) { - response = client.Response()..error = ErrorCode.InternalError; + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + final body = Uint8List.fromList(msg.v0.newMessage.body); + final fromUserId = msg.v0.newMessage.fromUserId.toInt(); + await handleClient2ClientMessage(fromUserId, body); + } else { + Log.error('Unknown server message: $msg'); } - - final v0 = client.V0() - ..seq = msg.v0.seq - ..response = response; - - await apiService.sendResponse(ClientToServer()..v0 = v0); - }); -} - -DateTime lastSignalDecryptMessage = - DateTime.now().subtract(const Duration(hours: 1)); -DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); - -bool messageGetsAck(MessageKind kind) { - return kind != MessageKind.pushKey && kind != MessageKind.ack; -} - -Future handleNewMessage(int fromUserId, Uint8List body) async { - final message = await signalDecryptMessage(fromUserId, body); - if (message == null) { - final encryptedHash = (await Sha256().hash(body)).bytes; - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.signalDecryptError, - content: SignalDecryptErrorContent(encryptedHash: encryptedHash), - timestamp: DateTime.now(), - ), - ); - - Log.error('Could not decrypt others message!'); - - // Message is not valid, so server can delete it - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; + } catch (e) { + Log.error(e); } - client.Response? result; + final v0 = client.V0() + ..seq = msg.v0.seq + ..response = response; - Log.info('Got: ${message.kind} from $fromUserId'); + await apiService.sendResponse(ClientToServer()..v0 = v0); +} - switch (message.kind) { - case MessageKind.ack: - final content = message.content; - if (content is AckContent) { - if (content.messageIdToAck != null) { - const update = MessagesCompanion( - acknowledgeByUser: Value(true), - errorWhileSending: Value(false), - ); - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - content.messageIdToAck!, - update, +DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); + +Mutex protectReceiptCheck = Mutex(); + +Future handleClient2ClientMessage(int fromUserId, Uint8List body) async { + final message = Message.fromBuffer(body); + final receiptId = message.receiptId; + + await protectReceiptCheck.protect(() async { + if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { + Log.error('Got duplicated message from the server. Ignoring it.'); + return; + } + await twonlyDB.receiptsDao.gotReceipt(receiptId); + }); + + switch (message.type) { + case Message_Type.SENDER_DELIVERY_RECEIPT: + Log.info('Got delivery receipt for $receiptId!'); + await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); + + case Message_Type.PLAINTEXT_CONTENT: + var retry = false; + if (message.hasPlaintextContent()) { + if (message.plaintextContent.hasDecryptionErrorMessage()) { + Log.info( + 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', ); + retry = true; + } + if (message.plaintextContent.hasRetryControlError()) { + Log.info( + 'Got access control error for $receiptId. Resending message.', + ); + retry = true; } - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById(content.retransIdToAck); } - case MessageKind.signalDecryptError: - Log.error( - 'Got signal decrypt error from other user! Sending it again.', - ); - final content = message.content; - if (content is SignalDecryptErrorContent) { - final hash = Uint8List.fromList(content.encryptedHash); - await twonlyDB.messageRetransmissionDao.resetAckStatusFor( - fromUserId, - hash, + if (retry) { + final newReceiptId = uuid.v4(); + await twonlyDB.receiptsDao.updateReceipt( + receiptId, + ReceiptsCompanion( + receiptId: Value(newReceiptId), + ackByServerAt: const Value(null), + ), ); - final message = await twonlyDB.messageRetransmissionDao - .getRetransmissionFromHash(fromUserId, hash); - if (message != null) { - unawaited(sendRetransmitMessage(message.retransmissionId)); - } + await tryToSendCompleteMessage(receiptId: newReceiptId); } - case MessageKind.contactRequest: - await handleContactRequest(fromUserId, message); + case Message_Type.CIPHERTEXT: + case Message_Type.PREKEY_BUNDLE: + if (message.hasEncryptedContent()) { + final encryptedContentRaw = + Uint8List.fromList(message.encryptedContent); - case MessageKind.flameSync: + if (await twonlyDB.contactsDao + .getContactByUserId(fromUserId) + .getSingleOrNull() == + null) { + /// In case the user does not exists, just create a dummy user which was deleted by the user, so the message + /// can be inserted into the receipts database + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + userId: Value(fromUserId), + deletedByUser: const Value(true), + username: const Value('[deleted]'), + ), + ); + } + + final responsePlaintextContent = await handleEncryptedMessage( + fromUserId, + encryptedContentRaw, + message.type, + ); + Message response; + if (responsePlaintextContent != null) { + response = Message() + ..receiptId = receiptId + ..type = Message_Type.PLAINTEXT_CONTENT + ..plaintextContent = responsePlaintextContent; + Log.error('Sending decryption error ($receiptId)'); + } else { + response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT; + } + + await twonlyDB.receiptsDao.insertReceipt( + ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(fromUserId), + message: Value(response.writeToBuffer()), + contactWillSendsReceipt: const Value(false), + ), + ); + await tryToSendCompleteMessage(receiptId: receiptId); + } + case Message_Type.TEST_NOTIFICATION: + return; + } +} + +Future handleEncryptedMessage( + int fromUserId, + Uint8List encryptedContentRaw, + Message_Type messageType, +) async { + final (content, decryptionErrorType) = await signalDecryptMessage( + fromUserId, + encryptedContentRaw, + messageType.value, + ); + + if (content == null) { + return PlaintextContent() + ..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() + ..type = decryptionErrorType!); + } + + final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); + + if (content.hasContactRequest()) { + if (!await handleContactRequest(fromUserId, content.contactRequest)) { + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); + } + return null; + } + + if (content.hasContactUpdate()) { + await handleContactUpdate( + fromUserId, + content.contactUpdate, + senderProfileCounter, + ); + return null; + } + + if (content.hasFlameSync()) { + await handleFlameSync(fromUserId, content.flameSync); + return null; + } + + if (content.hasPushKeys()) { + await handlePushKey(fromUserId, content.pushKeys); + return null; + } + + if (content.hasMessageUpdate()) { + await handleMessageUpdate( + fromUserId, + content.messageUpdate, + ); + return null; + } + + if (content.hasMediaUpdate()) { + await handleMediaUpdate( + fromUserId, + content.mediaUpdate, + ); + return null; + } + + if (!content.hasGroupId()) { + Log.error('Messages should have a groupId $fromUserId.'); + return null; + } + + if (content.hasGroupCreate()) { + await handleGroupCreate( + fromUserId, + content.groupId, + content.groupCreate, + ); + return null; + } + + /// Verify that the user is (still) in that group... + if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { + if (getUUIDforDirectChat(gUser.userId, fromUserId) == content.groupId) { final contact = await twonlyDB.contactsDao .getContactByUserId(fromUserId) .getSingleOrNull(); - if (contact != null && contact.lastFlameCounterChange != null) { - final content = message.content; - if (content is FlameSyncContent) { - var updates = ContactsCompanion( - alsoBestFriend: Value(content.bestFriend), - ); - if (isToday(contact.lastFlameCounterChange!) && - isToday(content.lastFlameCounterChange)) { - if (content.flameCounter > contact.flameCounter) { - updates = ContactsCompanion( - flameCounter: Value(content.flameCounter), - ); - } - } - await twonlyDB.contactsDao.updateContact(fromUserId, updates); - } - } - - case MessageKind.receiveMediaError: - if (message.messageReceiverId != null) { - final openedMessage = await twonlyDB.messagesDao - .getMessageByIdAndContactId(fromUserId, message.messageReceiverId!) - .getSingleOrNull(); - - if (openedMessage != null) { - /// message found - - /// checks if - /// 1. this was a media upload - /// 2. the media was not already retransmitted - /// 3. the media was send in the last two days - if (openedMessage.mediaUploadId != null && - openedMessage.mediaRetransmissionState == - MediaRetransmitting.none && - openedMessage.sendAt - .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { - // reset the media upload state to pending, - // this will cause the media to be re-encrypted again - await twonlyDB.mediaUploadsDao.updateMediaUpload( - openedMessage.mediaUploadId!, - const MediaUploadsCompanion( - state: Value( - UploadState.pending, - ), - ), - ); - // reset the message upload so the upload will be done again - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - downloadState: Value(DownloadState.pending), - mediaRetransmissionState: - Value(MediaRetransmitting.retransmitted), - ), - ); - unawaited(retryMediaUpload(false)); - } else { - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } - } - } - - case MessageKind.opened: - if (message.messageReceiverId != null) { - final update = MessagesCompanion( - openedAt: Value(message.timestamp), - errorWhileSending: const Value(false), - ); - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - update, - ); - final openedMessage = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageReceiverId!) - .getSingleOrNull(); - if (openedMessage != null && - openedMessage.kind == MessageKind.textMessage) { - await twonlyDB.messagesDao.openedAllNonMediaMessagesFromOtherUser( - fromUserId, - ); - } - } - - case MessageKind.rejectRequest: - await deleteContact(fromUserId); - - case MessageKind.acceptRequest: - const update = ContactsCompanion(accepted: Value(true)); - await twonlyDB.contactsDao.updateContact(fromUserId, update); - unawaited(notifyContactsAboutProfileChange()); - - case MessageKind.profileChange: - final content = message.content; - if (content is ProfileContent) { - final update = ContactsCompanion( - avatarSvg: Value(content.avatarSvg), - displayName: Value(content.displayName), - ); - await twonlyDB.contactsDao.updateContact(fromUserId, update); - } - unawaited(createPushAvatars()); - - case MessageKind.requestPushKey: - if (lastPushKeyRequest - .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { - lastPushKeyRequest = DateTime.now(); - unawaited(setupNotificationWithUsers(forceContact: fromUserId)); - } - - case MessageKind.pushKey: - if (message.content != null) { - final pushKey = message.content!; - if (pushKey is PushKeyContent) { - await handleNewPushKey(fromUserId, pushKey); - } - } - - // ignore: no_default_cases - default: - if (message.messageSenderId == null) { - Log.error('Messageid not defined $message'); - } else if ([ - MessageKind.textMessage, - MessageKind.media, - MessageKind.storedMediaFile, - MessageKind.reopenedMedia, - ].contains(message.kind)) { - result = await handleMediaOrTextMessage(fromUserId, message); - } else { - Log.error('Got unknown MessageKind $message'); - } - } - - if (messageGetsAck(message.kind) && message.retransId != null) { - Log.info('Sending ACK for ${message.kind}'); - - /// ACK every message - await encryptAndSendMessageAsync( - null, - fromUserId, - MessageJson( - kind: MessageKind.ack, - content: AckContent( - messageIdToAck: message.messageSenderId, - retransIdToAck: message.retransId!, - ), - timestamp: DateTime.now(), - ), - ); - } - - if (result != null) { - return result; - } - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; -} - -Future handleMediaOrTextMessage( - int fromUserId, - MessageJson message, -) async { - if (message.kind == MessageKind.storedMediaFile) { - if (message.messageReceiverId != null) { - /// stored media file just updates the message - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageReceiverId!, - const MessagesCompanion( - mediaStored: Value(true), - errorWhileSending: Value(false), - ), - ); - final msg = await twonlyDB.messagesDao - .getMessageByIdAndContactId( - fromUserId, - message.messageReceiverId!, - ) - .getSingleOrNull(); - if (msg != null && msg.mediaUploadId != null) { - final filePath = await getMediaFilePath(msg.mediaUploadId, 'send'); - if (filePath.contains('mp4')) { - unawaited(createThumbnailsForVideo(File(filePath))); - } else { - unawaited(createThumbnailsForImage(File(filePath))); - } - } - } - } else if (message.content != null) { - final content = message.content!; - // when a message is received doubled ignore it... - - final openedMessage = await twonlyDB.messagesDao - .getMessageByOtherMessageId(fromUserId, message.messageSenderId!) - .getSingleOrNull(); - - if (openedMessage != null) { - if (openedMessage.errorWhileSending) { - await twonlyDB.messagesDao - .deleteMessagesByMessageId(openedMessage.messageId); - } else { + if (contact == null || contact.deletedByUser) { Log.error( - 'Got a duplicated message from other user: ${message.messageSenderId!}', + 'User tries to send message to direct chat while the user does not exists !', ); - final ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; + return null; } - } - - int? responseToMessageId; - int? responseToOtherMessageId; - int? messageId; - - var acknowledgeByUser = false; - DateTime? openedAt; - - if (message.kind == MessageKind.reopenedMedia) { - acknowledgeByUser = true; - openedAt = DateTime.now(); - } - - if (content is TextMessageContent) { - responseToMessageId = content.responseToMessageId; - responseToOtherMessageId = content.responseToOtherMessageId; - - if (responseToMessageId != null || responseToOtherMessageId != null) { - // reactions are shown in the notification directly... - if (isEmoji(content.text)) { - openedAt = DateTime.now(); - } - } - } - if (content is ReopenedMediaFileContent) { - responseToMessageId = content.messageId; - } - - if (responseToMessageId != null) { - await twonlyDB.messagesDao.updateMessageByOtherUser( + Log.info( + 'Creating new DirectChat between two users', + ); + await twonlyDB.groupsDao.createNewDirectChat( fromUserId, - responseToMessageId, - MessagesCompanion( - errorWhileSending: const Value(false), - openedAt: Value( - DateTime.now(), - ), // when a user reacted to the media file, it should be marked as opened + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), ), ); - } - - final contentJson = jsonEncode(content.toJson()); - final update = MessagesCompanion( - contactId: Value(fromUserId), - kind: Value(message.kind), - messageOtherId: Value(message.messageSenderId), - contentJson: Value(contentJson), - acknowledgeByServer: const Value(true), - acknowledgeByUser: Value(acknowledgeByUser), - responseToMessageId: Value(responseToMessageId), - responseToOtherMessageId: Value(responseToOtherMessageId), - openedAt: Value(openedAt), - downloadState: Value( - message.kind == MessageKind.media - ? DownloadState.pending - : DownloadState.downloaded, - ), - sendAt: Value(message.timestamp), - ); - - messageId = await twonlyDB.messagesDao.insertMessage( - update, - ); - - if (messageId == null) { - Log.error('could not insert message into db'); - return client.Response()..error = ErrorCode.InternalError; - } - - Log.info('Inserted a new message with id: $messageId'); - - if (message.kind == MessageKind.media) { - await twonlyDB.contactsDao.incFlameCounter( - fromUserId, - true, - message.timestamp, - ); - - final msg = await twonlyDB.messagesDao - .getMessageByMessageId(messageId) - .getSingleOrNull(); - if (msg != null) { - unawaited(startDownloadMedia(msg, false)); + } else { + if (content.hasGroupJoin()) { + Log.error( + 'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.', + ); + // In case the group join was received before the GroupCreate the sender should send it later again. + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); } + + Log.error('User $fromUserId tried to access group ${content.groupId}.'); + return null; } - } else { - Log.error('Content is not defined $message'); } - // unarchive contact when receiving a new message - await twonlyDB.contactsDao.updateContact( - fromUserId, - const ContactsCompanion( - archived: Value(false), - ), - ); + if (content.hasGroupUpdate()) { + await handleGroupUpdate( + fromUserId, + content.groupId, + content.groupUpdate, + ); + return null; + } + + if (content.hasGroupJoin()) { + if (!await handleGroupJoin( + fromUserId, + content.groupId, + content.groupJoin, + )) { + return PlaintextContent() + ..retryControlError = PlaintextContent_RetryErrorMessage(); + } + return null; + } + + if (content.hasResendGroupPublicKey()) { + await handleResendGroupPublicKey( + fromUserId, + content.groupId, + content.groupJoin, + ); + return null; + } + + if (content.hasTextMessage()) { + await handleTextMessage( + fromUserId, + content.groupId, + content.textMessage, + ); + return null; + } + + if (content.hasReaction()) { + await handleReaction( + fromUserId, + content.groupId, + content.reaction, + ); + return null; + } + + if (content.hasMedia()) { + await handleMedia( + fromUserId, + content.groupId, + content.media, + ); + return null; + } + return null; } - -Future handleRequestNewPreKey() async { - final localPreKeys = await signalGetPreKeys(); - - final prekeysList = []; - for (var i = 0; i < localPreKeys.length; i++) { - prekeysList.add( - client.Response_PreKey() - ..id = Int64(localPreKeys[i].id) - ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(), - ); - } - final prekeys = client.Response_Prekeys(prekeys: prekeysList); - final ok = client.Response_Ok()..prekeys = prekeys; - return client.Response()..ok = ok; -} - -Future handleContactRequest( - int fromUserId, - MessageJson message, -) async { - // request the username by the server so an attacker can not - // forge the displayed username in the contact request - final username = await apiService.getUsername(fromUserId); - if (username.isSuccess) { - final name = username.value.userdata.username as Uint8List; - await twonlyDB.contactsDao.insertContact( - ContactsCompanion( - username: Value(utf8.decode(name)), - userId: Value(fromUserId), - requested: const Value(true), - ), - ); - } - await setupNotificationWithUsers(); -} diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 3c42df9..7357d06 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -1,17 +1,17 @@ import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' + hide Message; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/signal/session.signal.dart'; class Result { Result.error(this.error) : value = null; @@ -24,6 +24,10 @@ class Result { bool get isError => error != null; } +DateTime fromTimestamp(Int64 timeStamp) { + return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt()); +} + // ignore: strict_raw_type Result asResult(server.ServerToClient? msg) { if (msg == null) { @@ -52,45 +56,25 @@ ClientToServer createClientToServerFromApplicationData( return ClientToServer()..v0 = v0; } -Future deleteContact(int contactId) async { - await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId); - await twonlyDB.signalDao.deleteAllByContactId(contactId); - await deleteSessionWithTarget(contactId); - await twonlyDB.contactsDao.deleteContactByUserId(contactId); -} - -Future rejectUser(int contactId) async { - await encryptAndSendMessageAsync( - null, - contactId, - MessageJson( - kind: MessageKind.rejectRequest, - timestamp: DateTime.now(), - content: MessageContent(), +Future handleMediaError(MediaFile media) async { + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + const MediaFilesCompanion( + downloadState: Value(DownloadState.reuploadRequested), ), ); -} - -Future handleMediaError(Message message) async { - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - mediaRetransmissionState: Value( - MediaRetransmitting.requested, + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); + if (messages.length != 1) return; + final message = messages.first; + if (message.senderId == null) return; + await sendCipherText( + message.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, + targetMessageId: message.messageId, ), ), ); - if (message.messageOtherId != null) { - await encryptAndSendMessageAsync( - null, - message.contactId, - MessageJson( - kind: MessageKind.receiveMediaError, - timestamp: DateTime.now(), - content: MessageContent(), - messageReceiverId: message.messageOtherId, - ), - ); - } } diff --git a/lib/src/services/fcm.service.dart b/lib/src/services/fcm.service.dart index 899936f..33ffaa8 100644 --- a/lib/src/services/fcm.service.dart +++ b/lib/src/services/fcm.service.dart @@ -81,10 +81,18 @@ Future handleRemoteMessage(RemoteMessage message) async { if (!Platform.isAndroid) { Log.error('Got message in Dart while on iOS'); } + if (message.notification != null && globalIsAppInBackground) { + Log.error( + 'Got notification but app is in background, so the SDK already have shown the message.', + ); + return; + } - if (message.notification != null) { - final title = message.notification!.title ?? ''; - final body = message.notification!.body ?? ''; + if (message.notification != null || message.data['title'] != null) { + final title = + message.notification?.title ?? message.data['title'] as String? ?? ''; + final body = + message.notification?.body ?? message.data['body'] as String? ?? ''; await customLocalPushNotification(title, body); } else if (message.data['push_data'] != null) { await handlePushData(message.data['push_data'] as String); diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 01edd40..8eff7fb 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -1,60 +1,64 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.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_database.dart'; -import 'package:twonly/src/model/json/message.dart' as my; +import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -Future syncFlameCounters() async { - final user = await getUser(); - if (user == null) return; - - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); - if (contacts.isEmpty) return; - final maxMessageCounter = contacts.map((x) => x.totalMediaCounter).max; +Future syncFlameCounters({String? forceForGroup}) async { + final groups = await twonlyDB.groupsDao.getAllDirectChats(); + if (groups.isEmpty) return; + final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; final bestFriend = - contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); + groups.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); - if (user.myBestFriendContactId != bestFriend.userId) { + if (gUser.myBestFriendGroupId != bestFriend.groupId) { await updateUserdata((user) { - user.myBestFriendContactId = bestFriend.userId; + user.myBestFriendGroupId = bestFriend.groupId; return user; }); } - for (final contact in contacts) { - if (contact.lastFlameCounterChange == null || contact.deleted) continue; - if (!isToday(contact.lastFlameCounterChange!)) continue; - if (contact.lastFlameSync != null) { - if (isToday(contact.lastFlameSync!)) continue; + for (final group in groups) { + if (group.lastFlameCounterChange == null) continue; + if (!isToday(group.lastFlameCounterChange!)) continue; + if (forceForGroup == null || group.groupId != forceForGroup) { + if (group.lastFlameSync != null) { + if (isToday(group.lastFlameSync!)) continue; + } } - final flameCounter = getFlameCounterFromContact(contact) - 1; + final flameCounter = getFlameCounterFromGroup(group) - 1; - // only sync when flame counter is higher than three days - if (flameCounter < 1 && bestFriend.userId != contact.userId) continue; + // only sync when flame counter is higher than three days or when they are bestFriends + if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue; - await encryptAndSendMessageAsync( - null, - contact.userId, - my.MessageJson( - kind: MessageKind.flameSync, - content: my.FlameSyncContent( - flameCounter: flameCounter, - lastFlameCounterChange: contact.lastFlameCounterChange!, - bestFriend: contact.userId == bestFriend.userId, + final groupMembers = + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); + if (groupMembers.length != 1) { + continue; // flame sync is only done for groups of two + } + + await sendCipherText( + groupMembers.first.contactId, + EncryptedContent( + flameSync: EncryptedContent_FlameSync( + flameCounter: Int64(flameCounter), + lastFlameCounterChange: + Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), + bestFriend: group.groupId == bestFriend.groupId, + forceUpdate: group.groupId == forceForGroup, ), - timestamp: DateTime.now(), ), ); - await twonlyDB.contactsDao.updateContact( - contact.userId, - ContactsCompanion( + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( lastFlameSync: Value(DateTime.now()), ), ); diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart new file mode 100644 index 0000000..3d81340 --- /dev/null +++ b/lib/src/services/group.services.dart @@ -0,0 +1,945 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:collection/collection.dart'; +import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hashlib/random.dart'; +import 'package:http/http.dart' as http; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +// ignore: implementation_imports +import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +String getGroupStateUrl() { + return 'http${apiService.apiSecure}://${apiService.apiHost}/api/group/state'; +} + +String getGroupChallengeUrl() { + return 'http${apiService.apiSecure}://${apiService.apiHost}/api/group/challenge'; +} + +Future createNewGroup(String groupName, List members) async { + // First: Upload new State to the server..... + // if (groupName) return; + + final groupId = uuid.v4(); + + final memberIds = members.map((x) => Int64(x.userId)).toList(); + + final groupState = EncryptedGroupState( + memberIds: [Int64(gUser.userId)] + memberIds, + adminIds: [Int64(gUser.userId)], + groupName: groupName, + deleteMessagesAfterMilliseconds: + Int64(defaultDeleteMessagesAfterMilliseconds), + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + final stateEncryptionKey = getRandomUint8List(32); + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final secretBox = await chacha20.encrypt( + groupState.writeToBuffer(), + secretKey: SecretKey(stateEncryptionKey), + nonce: encryptionNonce, + ); + + final encryptedGroupState = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + final myGroupKey = generateIdentityKeyPair(); + + { + // Upload the group state, if this fails, the group can not be created. + + final newGroupState = NewGroupState( + groupId: groupId, + versionId: Int64(1), + encryptedGroupState: encryptedGroupState.writeToBuffer(), + publicKey: myGroupKey.getPublicKey().serialize(), + ); + + final response = await http + .post( + Uri.parse(getGroupStateUrl()), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not upload group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + + final group = await twonlyDB.groupsDao.createNewGroup( + GroupsCompanion( + groupId: Value(groupId), + groupName: Value(groupName), + isGroupAdmin: const Value(true), + stateEncryptionKey: Value(stateEncryptionKey), + stateVersionId: const Value(1), + myGroupPrivateKey: Value(myGroupKey.serialize()), + joinedGroup: const Value(true), + ), + ); + + if (group == null) { + Log.error('Could not insert group into database.'); + return false; + } + + Log.info('Created new group: ${group.groupId}'); + + for (final member in members) { + await twonlyDB.groupsDao.insertOrUpdateGroupMember( + GroupMembersCompanion( + groupId: Value(group.groupId), + contactId: Value(member.userId), + memberState: const Value(MemberState.normal), + ), + ); + } + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(groupId), + type: const Value(GroupActionType.createdGroup), + ), + ); + + // Notify members about the new group :) + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupCreate: EncryptedContent_GroupCreate( + stateKey: stateEncryptionKey, + groupPublicKey: myGroupKey.getPublicKey().serialize(), + ), + ), + ); + + return true; +} + +Future fetchGroupStatesForUnjoinedGroups() async { + final groups = await twonlyDB.groupsDao.getAllNotJoinedGroups(); + + for (final group in groups) { + await fetchGroupState(group); + } +} + +Future fetchMissingGroupPublicKey() async { + final members = await twonlyDB.groupsDao.getAllGroupMemberWithoutPublicKey(); + + for (final member in members) { + if (member.lastMessage == null) continue; + // only request if the users has send a message in the last two days. + if (member.lastMessage! + .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + await sendCipherText( + member.contactId, + EncryptedContent( + groupId: member.groupId, + resendGroupPublicKey: EncryptedContent_ResendGroupPublicKey(), + ), + ); + } + } +} + +Future?> _decryptEnvelop( + Group group, + List encryptedGroupState, +) async { + try { + final envelope = EncryptedGroupStateEnvelop.fromBuffer( + encryptedGroupState, + ); + final chacha20 = FlutterChacha20.poly1305Aead(); + + final secretBox = SecretBox( + envelope.encryptedGroupState, + nonce: envelope.nonce, + mac: Mac(envelope.mac), + ); + + final encryptedGroupStateRaw = await chacha20.decrypt( + secretBox, + secretKey: SecretKey(group.stateEncryptionKey!), + ); + + return encryptedGroupStateRaw; + } catch (e) { + Log.error(e); + return null; + } +} + +Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { + try { + var isSuccess = true; + + final response = await http + .get( + Uri.parse('${getGroupStateUrl()}/${group.groupId}'), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + if (response.statusCode == 404) { + // group does not exists any more. + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + leftGroup: Value(true), + ), + ); + } + Log.error( + 'Could not load group state. Got status code ${response.statusCode} from server.', + ); + return null; + } + + final groupStateServer = GroupState.fromBuffer(response.bodyBytes); + + final encryptedStateRaw = + await _decryptEnvelop(group, groupStateServer.encryptedGroupState); + if (encryptedStateRaw == null) return null; + + final encryptedGroupState = + EncryptedGroupState.fromBuffer(encryptedStateRaw); + + if (group.stateVersionId >= groupStateServer.versionId.toInt()) { + Log.info( + 'Group ${group.groupId} has already newest group state from the server!', + ); + } + + final memberIds = List.from(encryptedGroupState.memberIds); + final adminIds = List.from(encryptedGroupState.adminIds); + + for (final appendedState in groupStateServer.appendedGroupStates) { + final identityKey = IdentityKey.fromBytes( + Uint8List.fromList(appendedState.appendTBS.publicKey), + 0, + ); + + final valid = Curve.verifySignature( + identityKey.publicKey, + appendedState.appendTBS.writeToBuffer(), + Uint8List.fromList(appendedState.signature), + ); + + if (!valid) { + Log.error('Invalid signature for the appendedState'); + continue; + } + + final encryptedStateRaw = await _decryptEnvelop( + group, + appendedState.appendTBS.encryptedGroupStateAppend, + ); + if (encryptedStateRaw == null) continue; + + final appended = + EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw); + if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) { + final keyPair = + IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final appendedPubKey = appendedState.appendTBS.publicKey; + final myPubKey = keyPair.getPublicKey().serialize().toList(); + + if (listEquals(appendedPubKey, myPubKey)) { + adminIds.remove(Int64(gUser.userId)); + memberIds + .remove(Int64(gUser.userId)); // -> Will remove the user later... + } else { + Log.info('A non admin left the group!!!'); + + final member = await twonlyDB.groupsDao + .getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey)); + if (member == null) { + Log.error('Member is already not in this group...'); + continue; + } + adminIds.remove(Int64(member.contactId)); + memberIds.remove(Int64(member.contactId)); + } + } + } + + if (!memberIds.contains(Int64(gUser.userId))) { + // OH no, I am no longer a member of this group... + // Return from the group... + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + leftGroup: Value(true), + ), + ); + return (groupStateServer.versionId.toInt(), encryptedGroupState); + } + + final isGroupAdmin = + adminIds.firstWhereOrNull((t) => t.toInt() == gUser.userId) != null; + + if (!listEquals(memberIds, encryptedGroupState.memberIds)) { + if (isGroupAdmin) { + try { + // this removes the appended_group_state from the server and merges the changes into the main group state + final newState = EncryptedGroupState( + groupName: encryptedGroupState.groupName, + deleteMessagesAfterMilliseconds: + encryptedGroupState.deleteMessagesAfterMilliseconds, + memberIds: memberIds, + adminIds: adminIds, + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + // send new state to the server + if (!await _updateGroupState( + group, + newState, + versionId: groupStateServer.versionId.toInt() + 1, + )) { + // could not update the group state... + Log.error('Update the state to remove the appended state...'); + return null; + } + // the state is now updated and the appended_group_state should be removed on the server, so just call this + // function again, to sync the local database + return fetchGroupState(group); + } catch (e) { + Log.error(e); + return null; + } + } + // in case this is not an admin, just work with the new memberIds and adminIds... + } + + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + groupName: Value(encryptedGroupState.groupName), + deleteMessagesAfterMilliseconds: Value( + encryptedGroupState.deleteMessagesAfterMilliseconds.toInt(), + ), + isGroupAdmin: Value(isGroupAdmin), + ), + ); + + var currentGroupMembers = + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); + + // First find and insert NEW members + for (final memberId in memberIds) { + if (memberId == Int64(gUser.userId)) { + continue; + } + if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) { + // User is already in the database + continue; + } + Log.info('New member in the GROUP state: $memberId'); + + var inContacts = true; + + if (await twonlyDB.contactsDao.getContactById(memberId.toInt()) == null) { + // User is not yet in the contacts, add him in the hidden. So he is not in the contact list / needs to be + // requested separately. + if (!await addNewHiddenContact(memberId.toInt())) { + Log.error('Could not request member ID will retry later.'); + isSuccess = false; + inContacts = false; + } + } + if (inContacts) { + // User is already a contact, so just add him to the group members list + await twonlyDB.groupsDao.insertOrUpdateGroupMember( + GroupMembersCompanion( + groupId: Value(group.groupId), + contactId: Value(memberId.toInt()), + memberState: const Value(MemberState.normal), + ), + ); + } + + // Send the new user my public group key + if (group.myGroupPrivateKey != null) { + final keyPair = + IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + await sendCipherText( + memberId.toInt(), + EncryptedContent( + groupJoin: EncryptedContent_GroupJoin( + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); + } + } + + // check if there is a member which is not in the server list... + + // update the current members list + currentGroupMembers = + await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); + + for (final member in currentGroupMembers) { + // Member is not any more in the members list + if (!encryptedGroupState.memberIds.contains(Int64(member.contactId))) { + await twonlyDB.groupsDao.removeMember(group.groupId, member.contactId); + continue; + } + + MemberState? newMemberState; + + if (adminIds.contains(Int64(member.contactId))) { + if (member.memberState == MemberState.normal) { + // user was promoted + newMemberState = MemberState.admin; + } + } else if (member.memberState == MemberState.admin) { + // user was demoted + newMemberState = MemberState.normal; + } + + if (newMemberState != null) { + await twonlyDB.groupsDao.updateMember( + group.groupId, + member.contactId, + GroupMembersCompanion( + memberState: Value(newMemberState), + ), + ); + } + } + + if (isSuccess) { + // in case not all members could be loaded from the server, + // this will ensure it will be tried again later + await twonlyDB.groupsDao.updateGroup( + group.groupId, + GroupsCompanion( + stateVersionId: Value(groupStateServer.versionId.toInt()), + joinedGroup: const Value(true), + ), + ); + } + return (groupStateServer.versionId.toInt(), encryptedGroupState); + } catch (e) { + Log.error(e); + return null; + } +} + +Future addNewHiddenContact(int contactId) async { + final userData = await apiService.getUserById(contactId); + if (userData == null) { + Log.error('Could not load contact informations'); + return false; + } + await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(userData.username)), + userId: Value(contactId), + deletedByUser: + const Value(true), // this will hide the contact in the contact list + ), + ); + await createNewSignalSession(userData); + unawaited(setupNotificationWithUsers(forceContact: contactId)); + return true; +} + +Future _updateGroupState( + Group group, + EncryptedGroupState state, { + Uint8List? addAdmin, + Uint8List? removeAdmin, + int? versionId, +}) async { + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final secretBox = await chacha20.encrypt( + state.writeToBuffer(), + secretKey: SecretKey(group.stateEncryptionKey!), + nonce: encryptionNonce, + ); + + final encryptedGroupState = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + { + // Upload the group state, if this fails, the group can not be created. + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final nonce = await getNonce(keyPair.getPublicKey().serialize()); + if (nonce == null) return false; + + final updateTBS = UpdateGroupState_UpdateTBS( + versionId: Int64(versionId ?? group.stateVersionId + 1), + encryptedGroupState: encryptedGroupState.writeToBuffer(), + publicKey: keyPair.getPublicKey().serialize(), + nonce: nonce, + addAdmin: addAdmin, + removeAdmin: removeAdmin, + ); + + final random = getRandomUint8List(32); + final signature = sign( + keyPair.getPrivateKey().serialize(), + updateTBS.writeToBuffer(), + random, + ); + + final newGroupState = UpdateGroupState( + update: updateTBS, + signature: signature, + ); + + final response = await http + .patch( + Uri.parse(getGroupStateUrl()), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not patch group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + return true; +} + +Future manageAdminState( + Group group, + Uint8List groupPublicKey, + int contactId, + bool remove, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + final userId = Int64(contactId); + + Uint8List? addAdmin; + Uint8List? removeAdmin; + + if (remove) { + if (state.adminIds.contains(userId)) { + state.adminIds.remove(userId); + removeAdmin = groupPublicKey; + } else { + Log.info('User was already removed as admin.'); + return true; + } + } else { + if (!state.adminIds.contains(userId)) { + state.adminIds.add(userId); + addAdmin = groupPublicKey; + } else { + Log.info('User is already admin.'); + return true; + } + } + + if (addAdmin == null && removeAdmin == null) { + Log.info('User does not have a group public key.'); + return false; + } + + // send new state to the server + if (!await _updateGroupState( + group, + state, + addAdmin: addAdmin, + removeAdmin: removeAdmin, + )) { + return false; + } + + final groupActionType = + remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin; + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: groupActionType.name, + affectedContactId: Int64(contactId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: Value(groupActionType), + affectedContactId: Value(contactId), + ), + ); + + // Updates the memberState :) + return (await fetchGroupState(group)) != null; +} + +Future updateGroupName(Group group, String groupName) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + state.groupName = groupName; + + // send new state to the server + if (!await _updateGroupState(group, state)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.updatedGroupName.name, + newGroupName: groupName, + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.updatedGroupName), + oldGroupName: Value(group.groupName), + newGroupName: Value(groupName), + ), + ); + + // Updates the groupName :) + return (await fetchGroupState(group)) != null; +} + +Future updateChatDeletionTime( + Group group, + int deleteMessagesAfterMilliseconds, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + state.deleteMessagesAfterMilliseconds = + Int64(deleteMessagesAfterMilliseconds); + + // send new state to the server + if (!await _updateGroupState(group, state)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.changeDisplayMaxTime.name, + newDeleteMessagesAfterMilliseconds: Int64( + deleteMessagesAfterMilliseconds, + ), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.changeDisplayMaxTime), + newDeleteMessagesAfterMilliseconds: + Value(deleteMessagesAfterMilliseconds), + ), + ); + + // Updates the groupName :) + return (await fetchGroupState(group)) != null; +} + +Future addNewGroupMembers( + Group group, + List newGroupMemberIds, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + var memberIds = state.memberIds + newGroupMemberIds.map(Int64.new).toList(); + memberIds = memberIds.toSet().toList(); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: memberIds, + adminIds: state.adminIds, + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState)) { + return false; + } + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + for (final newMember in newGroupMemberIds) { + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.addMember.name, + affectedContactId: Int64(newMember), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.addMember), + affectedContactId: Value(newMember), + ), + ); + + await sendCipherText( + newMember, + EncryptedContent( + groupId: group.groupId, + groupCreate: EncryptedContent_GroupCreate( + stateKey: group.stateEncryptionKey, + groupPublicKey: keyPair.getPublicKey().serialize(), + ), + ), + ); + } + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; +} + +Future removeMemberFromGroup( + Group group, + Uint8List groupPublicKey, + int removeContactId, +) async { + // ensure the latest state is used + final currentState = await fetchGroupState(group); + if (currentState == null) return false; + final (versionId, state) = currentState; + + final contactId = Int64(removeContactId); + + final membersIdSet = state.memberIds.toSet(); + final adminIdSet = state.adminIds.toSet(); + Uint8List? removeAdmin; + if (!membersIdSet.contains(contactId)) { + Log.info('User was already removed from the group!'); + return true; + } + if (adminIdSet.contains(contactId)) { + // if (member.groupPublicKey == null) { + // // If the admin public key is not removed, that the user could potentially still update the group state. So only + // // allow the user removal, if this key is known. It is better the users can not remove the other user, then + // // the he can but the other user, could still update the group state. + // Log.error( + // 'Could not remove user. User is admin, but groupPublicKey is unknown.', + // ); + // return false; + // } + removeAdmin = groupPublicKey; + } + + membersIdSet.remove(contactId); + adminIdSet.remove(contactId); + + final newState = EncryptedGroupState( + groupName: state.groupName, + deleteMessagesAfterMilliseconds: state.deleteMessagesAfterMilliseconds, + memberIds: membersIdSet.toList(), + adminIds: adminIdSet.toList(), + padding: List.generate(Random().nextInt(80), (_) => 0), + ); + + // send new state to the server + if (!await _updateGroupState(group, newState, removeAdmin: removeAdmin)) { + return false; + } + + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.removedMember.name, + affectedContactId: Int64(removeContactId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(GroupActionType.removedMember), + affectedContactId: Value( + removeContactId == gUser.userId ? null : removeContactId, + ), + ), + ); + + // Updates the groupMembers table :) + return (await fetchGroupState(group)) != null; +} + +Future getNonce(Uint8List publicKey) async { + final publicKeyHex = uint8ListToHex(publicKey); + + final responseNonce = await http + .get( + Uri.parse('${getGroupChallengeUrl()}/$publicKeyHex'), + ) + .timeout(const Duration(seconds: 10)); + + if (responseNonce.statusCode != 200) { + Log.error( + 'Could not load nonce. Got status code ${responseNonce.statusCode} from server.', + ); + return null; + } + return responseNonce.bodyBytes; +} + +Future leaveAsNonAdminFromGroup(Group group) async { + final currentState = await fetchGroupState(group); + if (currentState == null) { + Log.error('Could not load current state'); + return false; + } + + final (version, _) = currentState; + if (group.stateVersionId != version) { + Log.error('Version is not valid. Just retry.'); + return false; + } + + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionNonce = chacha20.newNonce(); + + final state = EncryptedAppendedGroupState( + type: EncryptedAppendedGroupState_Type.LEFT_GROUP, + ); + + final secretBox = await chacha20.encrypt( + state.writeToBuffer(), + secretKey: SecretKey(group.stateEncryptionKey!), + nonce: encryptionNonce, + ); + + final encryptedGroupStateAppend = EncryptedGroupStateEnvelop( + nonce: encryptionNonce, + encryptedGroupState: secretBox.cipherText, + mac: secretBox.mac.bytes, + ); + + { + // Upload the group state, if this fails, the group can not be created. + + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + + final nonce = await getNonce(keyPair.getPublicKey().serialize()); + if (nonce == null) return false; + + final appendTBS = AppendGroupState_AppendTBS( + publicKey: keyPair.getPublicKey().serialize(), + encryptedGroupStateAppend: encryptedGroupStateAppend.writeToBuffer(), + groupId: group.groupId, + nonce: nonce, + ); + + final random = getRandomUint8List(32); + final signature = sign( + keyPair.getPrivateKey().serialize(), + appendTBS.writeToBuffer(), + random, + ); + + final newGroupState = AppendGroupState( + versionId: Int64(group.stateVersionId + 1), + appendTBS: appendTBS, + signature: signature, + ); + + final response = await http + .post( + Uri.parse('${getGroupStateUrl()}/append'), + body: newGroupState.writeToBuffer(), + ) + .timeout(const Duration(seconds: 10)); + + if (response.statusCode != 200) { + Log.error( + 'Could not patch group state. Got status code ${response.statusCode} from server.', + ); + return false; + } + } + const groupActionType = GroupActionType.leftGroup; + await sendCipherTextToGroup( + group.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: groupActionType.name, + affectedContactId: Int64(gUser.userId), + ), + ), + ); + + await twonlyDB.groupsDao.insertGroupAction( + GroupHistoriesCompanion( + groupId: Value(group.groupId), + type: const Value(groupActionType), + ), + ); + + // Updates the table :) + return (await fetchGroupState(group)) != null; +} diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart new file mode 100644 index 0000000..e1da17c --- /dev/null +++ b/lib/src/services/mediafiles/compression.service.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:drift/drift.dart' show Value; +import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new/return_code.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future compressImage( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + // // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp + + try { + var compressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 90, + ); + + if (compressedBytes == null) { + throw Exception( + 'Could not compress media file: $sourceFile. Sending original file.', + ); + } + + Log.info('Compressed images size in bytes: ${compressedBytes.length}'); + + if (compressedBytes.length >= 1 * 1000 * 1000) { + // if the media file is over 1MB compress it with 60% + final tmpCompressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 60, + ); + if (tmpCompressedBytes != null) { + Log.error( + 'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.', + ); + compressedBytes = tmpCompressedBytes; + } + } + + await destinationFile.writeAsBytes(compressedBytes); + } catch (e) { + Log.error('$e'); + sourceFile.copySync(destinationFile.path); + } + + stopwatch.stop(); + + Log.info( + 'Compression of the image took: ${stopwatch.elapsedMilliseconds} milliseconds.', + ); +} + +Future compressAndOverlayVideo(MediaFileService media) async { + if (media.tempPath.existsSync()) { + media.tempPath.deleteSync(); + } + if (media.ffmpegOutputPath.existsSync()) { + media.ffmpegOutputPath.deleteSync(); + } + + final stopwatch = Stopwatch()..start(); + var command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"'; + + if (media.removeAudio) { + command = + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"'; + } + + final session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + media.ffmpegOutputPath.copySync(media.tempPath.path); + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video', + ); + } else { + Log.info(command); + Log.error('Compression failed for the video with exit code $returnCode.'); + Log.error(await session.getAllLogsAsString()); + // This should not happen, but in case "notify" the user that the video was not send... This is absolutely bad, but + // better this way then sending an uncompressed media file which potentially is 100MB big :/ + // Hopefully the user will report the strange behavior <3 + await twonlyDB.messagesDao.updateMessagesByMediaId( + media.mediaFile.mediaId, + const MessagesCompanion(isDeletedFromSender: Value(true)), + ); + media.fullMediaRemoval(); + await media.setUploadState(UploadState.uploaded); + } +} diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart new file mode 100644 index 0000000..00bed5a --- /dev/null +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:drift/drift.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/compression.service.dart'; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; +import 'package:twonly/src/utils/log.dart'; + +class MediaFileService { + MediaFileService(this.mediaFile, {required this.applicationSupportDirectory}); + MediaFile mediaFile; + + final Directory applicationSupportDirectory; + + static Future fromMedia(MediaFile media) async { + return MediaFileService( + media, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + static Future fromMediaId(String mediaId) async { + final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); + if (mediaFile == null) return null; + return MediaFileService( + mediaFile, + applicationSupportDirectory: await getApplicationSupportDirectory(), + ); + } + + static Future purgeTempFolder() async { + final tempDirectory = MediaFileService._buildDirectoryPath( + 'tmp', + await getApplicationSupportDirectory(), + ); + + final files = tempDirectory.listSync(); + for (final file in files) { + final mediaId = basename(file.path).split('.').first; + + var delete = true; + + final service = await MediaFileService.fromMediaId(mediaId); + + if (service == null) { + Log.error( + 'Purging media file, as it is not in the database $mediaId.', + ); + } else { + if (service.mediaFile.isDraftMedia) { + delete = false; + } + + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); + + // in case messages in empty the file will be deleted, as delete is true by default + + for (final message in messages) { + if (message.senderId == null) { + // Media was send by me + if (message.openedAt == null) { + // Message was not yet opened from all persons, so wait... + delete = false; + } else if (service.mediaFile.requiresAuthentication || + service.mediaFile.displayLimitInMilliseconds != null) { + // Message was opened by all persons, and they can not reopen the image. + // delete = true; // do not overwrite a previous delete = false + // this is just to make it easier to understand :) + } else if (message.openedAt! + .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + // Message was opened by all persons, as it can be reopened and then stored by a other person keep it for + // two day just to be sure. + delete = false; + } + } else { + // this media was received from another person + if (message.openedAt == null) { + // Message was not yet opened, so do not remove it. + delete = false; + } + if (service.mediaFile.type == MediaType.audio) { + delete = false; // do not delete voice messages + } + } + } + } + + if (delete) { + Log.info('Purging media file $mediaId'); + file.deleteSync(); + } + } + } + + Future updateFromDB() async { + final updated = + await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); + if (updated != null) { + mediaFile = updated; + } + } + + Future setDisplayLimit(int? displayLimitInMilliseconds) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + ), + ); + await updateFromDB(); + } + + bool get removeAudio => mediaFile.removeAudio ?? false; + + Future toggleRemoveAudio() async { + // var removeAudio = false; + // if (mediaFile.removeAudio != null) { + // removeAudio = !mediaFile.removeAudio!; + // } + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + removeAudio: Value(!removeAudio), + ), + ); + await updateFromDB(); + } + + Future setUploadState(UploadState uploadState) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + uploadState: Value(uploadState), + ), + ); + await updateFromDB(); + } + + Future setEncryptedMac(Uint8List encryptionMac) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + encryptionMac: Value(encryptionMac), + ), + ); + await updateFromDB(); + } + + Future setRequiresAuth(bool requiresAuthentication) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + requiresAuthentication: Value(requiresAuthentication), + displayLimitInMilliseconds: + requiresAuthentication ? const Value(12) : const Value.absent(), + ), + ); + await updateFromDB(); + } + + Future createThumbnail() async { + if (!storedPath.existsSync()) { + Log.error('Could not create Thumbnail as stored media does not exists.'); + return; + } + switch (mediaFile.type) { + case MediaType.gif: + case MediaType.audio: + case MediaType.image: + // all images are already compress.. + break; + case MediaType.video: + await createThumbnailsForVideo(storedPath, thumbnailPath); + } + } + + Future compressMedia() async { + if (!originalPath.existsSync()) { + Log.error('Could not compress as original media does not exists.'); + return; + } + + switch (mediaFile.type) { + case MediaType.image: + await compressImage(originalPath, tempPath); + case MediaType.video: + await compressAndOverlayVideo(this); + case MediaType.audio: + case MediaType.gif: + originalPath.copySync(tempPath.path); + } + } + + void fullMediaRemoval() { + final pathsToRemove = [ + tempPath, + encryptedPath, + originalPath, + storedPath, + thumbnailPath, + uploadRequestPath, + ]; + + for (final path in pathsToRemove) { + if (path.existsSync()) { + path.deleteSync(); + } + } + } + + bool get imagePreviewAvailable => + thumbnailPath.existsSync() || storedPath.existsSync(); + + Future storeMediaFile() async { + Log.info('Storing media file ${mediaFile.mediaId}'); + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + const MediaFilesCompanion( + stored: Value(true), + ), + ); + await twonlyDB.messagesDao.updateMessagesByMediaId( + mediaFile.mediaId, + const MessagesCompanion( + mediaStored: Value(true), + ), + ); + + if (originalPath.existsSync()) { + await originalPath.copy(tempPath.path); + await compressMedia(); + } + if (tempPath.existsSync()) { + await tempPath.copy(storedPath.path); + } else { + Log.error( + 'Could not store image neither tempPath nor originalPath exists.', + ); + } + unawaited(createThumbnail()); + await updateFromDB(); + } + + static Directory _buildDirectoryPath( + String directory, + Directory applicationSupportDirectory, + ) { + final mediaBaseDir = Directory( + join( + applicationSupportDirectory.path, + 'mediafiles', + directory, + ), + ); + if (!mediaBaseDir.existsSync()) { + mediaBaseDir.createSync(recursive: true); + } + return mediaBaseDir; + } + + File _buildFilePath( + String directory, { + String namePrefix = '', + String extensionParam = '', + }) { + var extension = extensionParam; + if (extension == '') { + switch (mediaFile.type) { + case MediaType.image: + extension = 'webp'; + case MediaType.video: + extension = 'mp4'; + case MediaType.gif: + extension = 'gif'; + case MediaType.audio: + extension = 'm4a'; + } + } + final mediaBaseDir = + _buildDirectoryPath(directory, applicationSupportDirectory); + return File( + join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), + ); + } + + File get tempPath => _buildFilePath('tmp'); + File get storedPath => _buildFilePath('stored'); + File get thumbnailPath => _buildFilePath( + 'stored', + namePrefix: '.thumbnail', + extensionParam: 'webp', + ); + File get encryptedPath => _buildFilePath( + 'tmp', + namePrefix: '.encrypted', + ); + File get uploadRequestPath => _buildFilePath( + 'tmp', + namePrefix: '.upload', + ); + File get originalPath => _buildFilePath( + 'tmp', + namePrefix: '.original', + ); + File get ffmpegOutputPath => _buildFilePath( + 'tmp', + namePrefix: '.ffmpeg', + ); + File get overlayImagePath => _buildFilePath( + 'tmp', + namePrefix: '.overlay', + extensionParam: 'png', + ); +} diff --git a/lib/src/services/mediafiles/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart new file mode 100644 index 0000000..df7397c --- /dev/null +++ b/lib/src/services/mediafiles/thumbnail.service.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_new/return_code.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future createThumbnailsForVideo( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + final command = + '-i "${sourceFile.path}" -ss 00:00:00 -vframes 1 -vf "scale=iw:ih:flags=lanczos" -c:v libwebp -q:v 100 -compression_level 6 "${destinationFile.path}"'; + + final session = await FFmpegKit.execute(command); + final returnCode = await session.getReturnCode(); + + if (ReturnCode.isSuccess(returnCode)) { + stopwatch.stop(); + Log.info( + 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', + ); + } else { + Log.info(command); + Log.error( + 'Thumbnail creation failed for the video with exit code $returnCode.', + ); + Log.error(await session.getAllLogsAsString()); + } +} diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 294fd03..2d6613f 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -7,32 +7,38 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/localization/generated/app_localizations.dart'; +import 'package:twonly/src/localization/generated/app_localizations_de.dart'; +import 'package:twonly/src/localization/generated/app_localizations_en.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart' - show gMediaShowInfinite; +import 'package:twonly/src/utils/misc.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future customLocalPushNotification(String title, String msg) async { - const androidNotificationDetails = AndroidNotificationDetails( + final androidNotificationDetails = AndroidNotificationDetails( '1', 'System', channelDescription: 'System messages.', - importance: Importance.max, - priority: Priority.max, + importance: Importance.high, + priority: Priority.high, + styleInformation: BigTextStyleInformation(msg), + icon: 'ic_launcher_foreground', ); const darwinNotificationDetails = DarwinNotificationDetails(); - const notificationDetails = NotificationDetails( + final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); + final id = Random.secure().nextInt(9999); + await flutterLocalNotificationsPlugin.show( - gMediaShowInfinite + Random.secure().nextInt(9999), + id, title, msg, notificationDetails, @@ -75,7 +81,10 @@ Future handlePushData(String pushDataB64) async { ); } else if (foundPushUser != null) { if (pushNotification.hasMessageId()) { - if (pushNotification.messageId <= foundPushUser.lastMessageId) { + if (isUUIDNewer( + foundPushUser.lastMessageId, + pushNotification.messageId, + )) { Log.info( 'Got a push notification for a message which was already opened.', ); @@ -89,11 +98,11 @@ Future handlePushData(String pushDataB64) async { } } } catch (e) { + Log.error(e); await customLocalPushNotification( 'Du hast eine neue Nachricht.', 'Öffne twonly um mehr zu erfahren.', ); - Log.error(e); } } @@ -145,14 +154,17 @@ Future showLocalPushNotification( styleInformation = FilePathAndroidBitmap(avatarPath); } + final lang = getLocalizations(); + final androidNotificationDetails = AndroidNotificationDetails( '0', - 'Messages', - channelDescription: 'Messages from other users.', + lang.notificationCategoryMessageTitle, + channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, ticker: 'You got a new message.', largeIcon: styleInformation, + icon: 'ic_launcher_foreground', ); const darwinNotificationDetails = DarwinNotificationDetails(); @@ -166,32 +178,36 @@ Future showLocalPushNotification( title, body, notificationDetails, - payload: pushNotification.kind.name, + // payload: pushNotification.kind.name, ); } Future showLocalPushNotificationWithoutUserId( PushNotification pushNotification, ) async { - String? title; String? body; - body = getPushNotificationTextWithoutUserId(pushNotification.kind); + body = getPushNotificationText(pushNotification); + + final lang = getLocalizations(); + + final title = lang.notificationTitleUnknownUser; + if (body == '') { Log.error('No push notification type defined!'); } - const androidNotificationDetails = AndroidNotificationDetails( + final androidNotificationDetails = AndroidNotificationDetails( '0', - 'Messages', - channelDescription: 'Messages from other users.', + lang.notificationCategoryMessageTitle, + channelDescription: lang.notificationCategoryMessageDesc, importance: Importance.max, priority: Priority.max, ticker: 'You got a new message.', ); const darwinNotificationDetails = DarwinNotificationDetails(); - const notificationDetails = NotificationDetails( + final notificationDetails = NotificationDetails( android: androidNotificationDetails, iOS: darwinNotificationDetails, ); @@ -216,104 +232,45 @@ Future getAvatarIcon(int contactId) async { return null; } -String getPushNotificationTextWithoutUserId(PushKind pushKind) { - Map pushNotificationText; - +AppLocalizations getLocalizations() { final systemLanguage = Platform.localeName; - - if (systemLanguage.contains('de')) { - pushNotificationText = { - PushKind.text.name: 'Du hast eine neue Nachricht erhalten.', - PushKind.twonly.name: 'Du hast ein neues twonly erhalten.', - PushKind.video.name: 'Du hast ein neues Video erhalten.', - PushKind.image.name: 'Du hast ein neues Bild erhalten.', - PushKind.contactRequest.name: - 'Du hast eine neue Kontaktanfrage erhalten.', - PushKind.acceptRequest.name: 'Deine Kontaktanfrage wurde angenommen.', - PushKind.storedMediaFile.name: 'Dein Bild wurde gespeichert.', - PushKind.reaction.name: 'Du hast eine Reaktion auf dein Bild erhalten.', - PushKind.reopenedMedia.name: 'Dein Bild wurde erneut geöffnet.', - PushKind.reactionToVideo.name: - 'Du hast eine Reaktion auf dein Video erhalten.', - PushKind.reactionToText.name: - 'Du hast eine Reaktion auf deinen Text erhalten.', - PushKind.reactionToImage.name: - 'Du hast eine Reaktion auf dein Bild erhalten.', - PushKind.response.name: 'Du hast eine Antwort erhalten.', - }; - } else { - pushNotificationText = { - PushKind.text.name: 'You have received a new message.', - PushKind.twonly.name: 'You have received a new twonly.', - PushKind.video.name: 'You have received a new video.', - PushKind.image.name: 'You have received a new image.', - PushKind.contactRequest.name: 'You have received a new contact request.', - PushKind.acceptRequest.name: 'Your contact request has been accepted.', - PushKind.storedMediaFile.name: 'Your image has been saved.', - PushKind.reaction.name: 'You have received a reaction to your image.', - PushKind.reopenedMedia.name: 'Your image has been reopened.', - PushKind.reactionToVideo.name: - 'You have received a reaction to your video.', - PushKind.reactionToText.name: - 'You have received a reaction to your text.', - PushKind.reactionToImage.name: - 'You have received a reaction to your image.', - PushKind.response.name: 'You have received a response.', - }; - } - return pushNotificationText[pushKind.name] ?? ''; + if (systemLanguage.contains('de')) return AppLocalizationsDe(); + return AppLocalizationsEn(); } String getPushNotificationText(PushNotification pushNotification) { - final systemLanguage = Platform.localeName; + final lang = getLocalizations(); - Map pushNotificationText; + var inGroup = ''; - if (systemLanguage.contains('de')) { - pushNotificationText = { - PushKind.text.name: 'hat dir eine Nachricht gesendet.', - PushKind.twonly.name: 'hat dir ein twonly gesendet.', - PushKind.video.name: 'hat dir ein Video gesendet.', - PushKind.image.name: 'hat dir ein Bild gesendet.', - PushKind.contactRequest.name: 'möchte sich mit dir vernetzen.', - PushKind.acceptRequest.name: 'ist jetzt mit dir vernetzt.', - PushKind.storedMediaFile.name: 'hat dein Bild gespeichert.', - PushKind.reaction.name: 'hat auf dein Bild reagiert.', - PushKind.reopenedMedia.name: 'hat dein Bild erneut geöffnet.', - PushKind.reactionToVideo.name: - 'hat mit {{reaction}} auf dein Video reagiert.', - PushKind.reactionToText.name: - 'hat mit {{reaction}} auf deine Nachricht reagiert.', - PushKind.reactionToImage.name: - 'hat mit {{reaction}} auf dein Bild reagiert.', - PushKind.response.name: 'hat dir geantwortet.', - }; - } else { - pushNotificationText = { - PushKind.text.name: 'has sent you a message.', - PushKind.twonly.name: 'has sent you a twonly.', - PushKind.video.name: 'has sent you a video.', - PushKind.image.name: 'has sent you an image.', - PushKind.contactRequest.name: 'wants to connect with you.', - PushKind.acceptRequest.name: 'is now connected with you.', - PushKind.storedMediaFile.name: 'has stored your image.', - PushKind.reaction.name: 'has reacted to your image.', - PushKind.reopenedMedia.name: 'has reopened your image.', - PushKind.reactionToVideo.name: - 'has reacted with {{reaction}} to your video.', - PushKind.reactionToText.name: - 'has reacted with {{reaction}} to your message.', - PushKind.reactionToImage.name: - 'has reacted with {{reaction}} to your image.', - PushKind.response.name: 'has responded.', - }; + if (pushNotification.hasAdditionalContent()) { + inGroup = + ' ${lang.notificationFillerIn} ${pushNotification.additionalContent}'; } - var contentText = pushNotificationText[pushNotification.kind.name] ?? ''; - if (pushNotification.hasReactionContent()) { - contentText = contentText.replaceAll( - '{{reaction}}', - pushNotification.reactionContent, - ); - } - return contentText; + + final pushNotificationText = { + PushKind.text.name: lang.notificationText(inGroup), + PushKind.twonly.name: lang.notificationTwonly(inGroup), + PushKind.video.name: lang.notificationVideo(inGroup), + PushKind.image.name: lang.notificationImage(inGroup), + PushKind.audio.name: lang.notificationAudio(inGroup), + PushKind.contactRequest.name: lang.notificationContactRequest, + PushKind.acceptRequest.name: lang.notificationAcceptRequest, + PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, + PushKind.reaction.name: lang.notificationReaction, + PushKind.reopenedMedia.name: lang.notificationReopenedMedia, + PushKind.reactionToVideo.name: + lang.notificationReactionToVideo(pushNotification.additionalContent), + PushKind.reactionToAudio.name: + lang.notificationReactionToAudio(pushNotification.additionalContent), + PushKind.reactionToText.name: + lang.notificationReactionToText(pushNotification.additionalContent), + PushKind.reactionToImage.name: + lang.notificationReactionToImage(pushNotification.additionalContent), + PushKind.response.name: lang.notificationResponse(inGroup), + PushKind.addedToGroup.name: + lang.notificationAddedToGroup(pushNotification.additionalContent), + }; + + return pushNotificationText[pushNotification.kind.name] ?? ''; } diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index 5488cc1..b02ab0f 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -7,15 +7,17 @@ import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.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_database.dart'; -import 'package:twonly/src/model/json/message.dart' as my; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; /// This function must be called after the database is setup Future setupNotificationWithUsers({ @@ -49,7 +51,7 @@ Future setupNotificationWithUsers({ final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); - if (pushUser != null) { + if (pushUser != null && pushUser.pushKeys.isNotEmpty) { // make it harder to predict the change of the key final timeBefore = DateTime.now().subtract(Duration(days: 5 + random.nextInt(5))); @@ -104,19 +106,14 @@ Future setupNotificationWithUsers({ } Future sendNewPushKey(int userId, PushKey pushKey) async { - await encryptAndSendMessageAsync( - null, + await sendCipherText( userId, - my.MessageJson( - kind: MessageKind.pushKey, - content: my.PushKeyContent( - keyId: pushKey.id.toInt(), - key: pushKey.key, - ), - timestamp: DateTime.fromMillisecondsSinceEpoch( - pushKey.createdAtUnixTimestamp.toInt(), - ), - ), + EncryptedContent() + ..pushKeys = (EncryptedContent_PushKeys() + ..type = EncryptedContent_PushKeys_Type.UPDATE + ..key = pushKey.key + ..keyId = pushKey.id + ..createdAt = pushKey.createdAtUnixTimestamp), ); } @@ -132,7 +129,7 @@ Future updatePushUser(Contact contact) async { displayName: getContactDisplayName(contact), pushKeys: [], blocked: contact.blocked, - lastMessageId: Int64(), + lastMessageId: uuid.v7(), ), ); } else { @@ -144,7 +141,7 @@ Future updatePushUser(Contact contact) async { await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys); } -Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { +Future handleNewPushKey(int fromUserId, int keyId, List key) async { final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); @@ -160,7 +157,7 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { displayName: getContactDisplayName(contact), pushKeys: [], blocked: contact.blocked, - lastMessageId: Int64(), + lastMessageId: uuid.v7(), ), ); pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); @@ -174,8 +171,8 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { pushUser!.pushKeys.clear(); pushUser.pushKeys.add( PushKey( - id: Int64(pushKey.keyId), - key: pushKey.key, + id: Int64(keyId), + key: key, createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), ), ); @@ -183,7 +180,7 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async { await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys); } -Future updateLastMessageId(int fromUserId, int messageId) async { +Future updateLastMessageId(int fromUserId, String messageId) async { final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId); @@ -192,15 +189,126 @@ Future updateLastMessageId(int fromUserId, int messageId) async { return; } - if (pushUser.lastMessageId < Int64(messageId)) { - pushUser.lastMessageId = Int64(messageId); + if (isUUIDNewer(messageId, pushUser.lastMessageId)) { + pushUser.lastMessageId = messageId; await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); } } +Future getPushNotificationFromEncryptedContent( + int toUserId, + String? messageId, + EncryptedContent content, +) async { + PushKind? kind; + String? additionalContent; + + if (content.hasReaction()) { + if (content.reaction.remove) return null; + + final msg = await twonlyDB.messagesDao + .getMessageById(content.reaction.targetMessageId) + .getSingleOrNull(); + if (msg == null || msg.senderId == null || msg.senderId != toUserId) { + return null; + } + if (msg.content != null) { + kind = PushKind.reactionToText; + } else if (msg.mediaId != null) { + final media = await twonlyDB.mediaFilesDao.getMediaFileById(msg.mediaId!); + if (media == null) return null; + switch (media.type) { + case MediaType.image: + kind = PushKind.reactionToImage; + case MediaType.audio: + kind = PushKind.reactionToAudio; + case MediaType.video: + kind = PushKind.reactionToVideo; + case MediaType.gif: + kind = PushKind.reaction; + } + } + additionalContent = content.reaction.emoji; + } + + if (content.hasTextMessage()) { + kind = PushKind.text; + if (content.textMessage.hasQuoteMessageId()) { + kind = PushKind.response; + } + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + if (group != null && !group.isDirectChat) { + additionalContent = group.groupName; + } + } + if (content.hasMedia()) { + switch (content.media.type) { + case EncryptedContent_Media_Type.REUPLOAD: + return null; + case EncryptedContent_Media_Type.IMAGE: + kind = PushKind.image; + case EncryptedContent_Media_Type.VIDEO: + kind = PushKind.video; + case EncryptedContent_Media_Type.GIF: + kind = PushKind.image; + case EncryptedContent_Media_Type.AUDIO: + kind = PushKind.audio; + } + if (content.media.requiresAuthentication) { + kind = PushKind.twonly; + } + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + if (group != null && !group.isDirectChat) { + additionalContent = group.groupName; + } + } + + if (content.hasContactRequest()) { + switch (content.contactRequest.type) { + case EncryptedContent_ContactRequest_Type.REQUEST: + kind = PushKind.contactRequest; + case EncryptedContent_ContactRequest_Type.ACCEPT: + kind = PushKind.acceptRequest; + case EncryptedContent_ContactRequest_Type.REJECT: + return null; + } + } + + if (content.hasMediaUpdate()) { + switch (content.mediaUpdate.type) { + case EncryptedContent_MediaUpdate_Type.REOPENED: + kind = PushKind.reopenedMedia; + case EncryptedContent_MediaUpdate_Type.STORED: + kind = PushKind.storedMediaFile; + case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: + return null; + } + } + + if (content.hasGroupCreate()) { + kind = PushKind.addedToGroup; + final group = await twonlyDB.groupsDao.getGroup(content.groupId); + additionalContent = group!.groupName; + } + + if (kind == null) return null; + + final pushNotification = PushNotification()..kind = kind; + if (additionalContent != null) { + pushNotification.additionalContent = additionalContent; + } + if (messageId != null) { + pushNotification.messageId = messageId; + } + return pushNotification; +} + /// this will trigger a push notification /// push notification only containing the message kind and username -Future getPushData(int toUserId, PushNotification content) async { +Future encryptPushNotification( + int toUserId, + PushNotification content, +) async { final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); var key = 'InsecureOnlyUsedForAddingContact'.codeUnits; @@ -218,14 +326,12 @@ Future getPushData(int toUserId, PushNotification content) async { // this will be enforced after every app uses this system... :/ // return null; Log.error('Using insecure key as the receiver does not send a push key!'); - await encryptAndSendMessageAsync( - null, + + await sendCipherText( toUserId, - my.MessageJson( - kind: MessageKind.requestPushKey, - content: my.MessageContent(), - timestamp: DateTime.now(), - ), + EncryptedContent() + ..pushKeys = (EncryptedContent_PushKeys() + ..type = EncryptedContent_PushKeys_Type.REQUEST), ); } } else { diff --git a/lib/src/services/notifications/setup.notifications.dart b/lib/src/services/notifications/setup.notifications.dart index 03ff634..1181354 100644 --- a/lib/src/services/notifications/setup.notifications.dart +++ b/lib/src/services/notifications/setup.notifications.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_svg/svg.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/misc.dart'; final StreamController selectNotificationStream = StreamController.broadcast(); @@ -61,10 +62,11 @@ Future createPushAvatars() async { final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); for (final contact in contacts) { - if (contact.avatarSvg == null) return; + if (contact.avatarSvgCompressed == null) continue; - final pictureInfo = - await vg.loadPicture(SvgStringLoader(contact.avatarSvg!), null); + final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!); + + final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null); final image = await pictureInfo.picture.toImage(300, 300); diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 8087fac..50aad34 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -1,24 +1,21 @@ -import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:mutex/mutex.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; /// This caused some troubles, so protection the encryption... final lockingSignalEncryption = Mutex(); -Future signalEncryptMessage( +Future signalEncryptMessage( int target, Uint8List plaintextContent, ) async { - return lockingSignalEncryption.protect(() async { + return lockingSignalEncryption.protect(() async { try { final signalStore = (await getSignalStore())!; final address = SignalProtocolAddress(target.toString(), defaultDeviceId); @@ -77,14 +74,7 @@ Future signalEncryptMessage( Log.error('did not get the identity of the remote address'); } } - - final ciphertext = await session.encrypt(plaintextContent); - - final b = BytesBuilder() - ..add(ciphertext.serialize()) - ..add(intToBytes(ciphertext.getType())); - - return b.takeBytes(); + return await session.encrypt(plaintextContent); } catch (e) { Log.error(e.toString()); return null; @@ -92,42 +82,40 @@ Future signalEncryptMessage( }); } -Future signalDecryptMessage(int source, Uint8List msg) async { +Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> + signalDecryptMessage( + int source, + Uint8List encryptedContentRaw, + int type, +) async { try { - final signalStore = (await getSignalStore())!; - final session = SessionCipher.fromStore( - signalStore, + (await getSignalStore())!, SignalProtocolAddress(source.toString(), defaultDeviceId), ); - final msgs = removeLastXBytes(msg, 4); - if (msgs == null) { - Log.error('Message requires at least 4 bytes.'); - return null; - } - final body = msgs[0]; - final type = bytesToInt(msgs[1]); Uint8List plaintext; - if (type == CiphertextMessage.prekeyType) { - final pre = PreKeySignalMessage(body); - plaintext = await session.decrypt(pre); - } else if (type == CiphertextMessage.whisperType) { - final signalMsg = SignalMessage.fromSerialized(body); - plaintext = await session.decryptFromSignal(signalMsg); - } else { - Log.error('Type not known: $type'); - return null; + + switch (type) { + case CiphertextMessage.prekeyType: + plaintext = await session.decrypt( + PreKeySignalMessage(encryptedContentRaw), + ); + case CiphertextMessage.whisperType: + plaintext = await session.decryptFromSignal( + SignalMessage.fromSerialized(encryptedContentRaw), + ); + default: + Log.error('Unknown Message Decryption Type: $type'); + return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } - return MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(plaintext), - ), - ) as Map, - ); + + return (EncryptedContent.fromBuffer(plaintext), null); + } on InvalidKeyIdException catch (e) { + Log.error(e); + return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN); } catch (e) { - Log.error(e.toString()); - return null; + Log.error(e); + return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); } } diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 866b66c..f2e3129 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -20,13 +20,11 @@ Future getSignalIdentityKeyPair() async { // This function runs after the clients authenticated with the server. // It then checks if it should update a new session key Future signalHandleNewServerConnection() async { - final user = await getUser(); - if (user == null) return; - if (user.signalLastSignedPreKeyUpdated != null) { + if (gUser.signalLastSignedPreKeyUpdated != null) { final fortyEightHoursAgo = DateTime.now().subtract(const Duration(hours: 48)); final isYoungerThan48Hours = - (user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); + (gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); if (isYoungerThan48Hours) { // The key does live for 48 hours then it expires and a new key is generated. return; @@ -63,7 +61,8 @@ Future> signalGetPreKeys() async { final start = user.currentPreKeyIndexStart; await updateUserdata((user) { - user.currentPreKeyIndexStart += 200; + user.currentPreKeyIndexStart = + (user.currentPreKeyIndexStart + 200) % maxValue; return user; }); final preKeys = generatePreKeys(start, 200); diff --git a/lib/src/services/signal/prekeys.signal.dart b/lib/src/services/signal/prekeys.signal.dart index 9ce59be..e70d239 100644 --- a/lib/src/services/signal/prekeys.signal.dart +++ b/lib/src/services/signal/prekeys.signal.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/utils/log.dart'; @@ -31,13 +31,13 @@ Future requestNewPrekeysForContact(int contactId) async { .isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { return; } - Log.info('Requesting new PREKEYS for $contactId'); + Log.info('[PREKEY] Requesting new PREKEYS for $contactId'); lastPreKeyRequest = DateTime.now(); await requestNewKeys.protect(() async { final otherKeys = await apiService.getPreKeysByUserId(contactId); if (otherKeys != null) { Log.info( - 'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!', + '[PREKEY] Got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!', ); final preKeys = otherKeys.preKeys .map( @@ -50,7 +50,8 @@ Future requestNewPrekeysForContact(int contactId) async { .toList(); await twonlyDB.signalDao.insertPreKeys(preKeys); } else { - Log.error('could not load new pre keys for user $contactId'); + // 104400 + Log.error('[PREKEY] Could not load new pre keys for user $contactId'); } }); } diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 19ee1a0..48480c7 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -1,10 +1,10 @@ import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/storage.dart'; Future createNewSignalSession(Response_UserData userData) async { final SignalProtocolStore? signalStore = await getSignalStore(); @@ -84,8 +84,7 @@ Future deleteSessionWithTarget(int target) async { Future generateSessionFingerPrint(int target) async { final signalStore = await getSignalStore(); - final user = await getUser(); - if (signalStore == null || user == null) return null; + if (signalStore == null) return null; try { final targetIdentity = await signalStore .getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId)); @@ -93,7 +92,7 @@ Future generateSessionFingerPrint(int target) async { final generator = NumericFingerprintGenerator(5200); final localFingerprint = generator.createFor( 1, - Uint8List.fromList([user.userId]), + Uint8List.fromList([gUser.userId]), (await signalStore.getIdentityKeyPair()).getPublicKey(), Uint8List.fromList([target]), targetIdentity, diff --git a/lib/src/services/subscription.service.dart b/lib/src/services/subscription.service.dart new file mode 100644 index 0000000..c36797a --- /dev/null +++ b/lib/src/services/subscription.service.dart @@ -0,0 +1,35 @@ +// ignore_for_file: constant_identifier_names + +import 'package:twonly/globals.dart'; + +enum SubscriptionPlan { + Free, + Tester, + Family, + Pro, + Plus, +} + +bool isAdditionalAccount(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus; +} + +bool isPayingUser(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Family || + plan == SubscriptionPlan.Pro || + plan == SubscriptionPlan.Tester; +} + +SubscriptionPlan planFromString(String value) { + final input = value.trim().toLowerCase(); + for (final v in SubscriptionPlan.values) { + final name = v.name; + final compareName = name.toLowerCase(); + if (compareName == input) return v; + } + return SubscriptionPlan.Free; +} + +SubscriptionPlan getCurrentPlan() { + return planFromString(gUser.subscriptionPlan); +} diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/thumbnail.service.dart deleted file mode 100644 index e1311e7..0000000 --- a/lib/src/services/thumbnail.service.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:path/path.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:video_thumbnail/video_thumbnail.dart'; - -Future createThumbnailsForImage(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); - if (fileExtension != 'png') { - Log.error('Could not create thumbnail for image. $fileExtension != .png'); - return; - } - - try { - final imageBytesCompressed = await FlutterImageCompress.compressWithFile( - minHeight: 800, - minWidth: 450, - file.path, - format: CompressFormat.webp, - quality: 50, - ); - - if (imageBytesCompressed == null) { - Log.error('Could not compress the image'); - return; - } - - final thumbnailFile = getThumbnailPath(file); - await thumbnailFile.writeAsBytes(imageBytesCompressed); - } catch (e) { - Log.error('Could not compress the image got :$e'); - } -} - -Future createThumbnailsForVideo(File file) async { - final fileExtension = file.path.split('.').last.toLowerCase(); - if (fileExtension != 'mp4') { - Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); - return; - } - - try { - await VideoThumbnail.thumbnailFile( - video: file.path, - thumbnailPath: getThumbnailPath(file).path, - maxWidth: 450, - quality: 75, - ); - } catch (e) { - Log.error('Could not create the video thumbnail: $e'); - } -} - -File getThumbnailPath(File file) { - final originalFileName = file.uri.pathSegments.last; - final fileNameWithoutExtension = originalFileName.split('.').first; - var fileExtension = originalFileName.split('.').last; - if (fileExtension == 'mp4') { - fileExtension = 'png'; - } - final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension'; - Directory(file.parent.path).createSync(); - return File(join(file.parent.path, newFileName)); -} diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 27d2189..530328a 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -3,17 +3,16 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:hashlib/hashlib.dart'; import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future enableTwonlySafe(String password) async { - final user = await getUser(); - if (user == null) return; - - final (backupId, encryptionKey) = await getMasterKey(password, user.username); + final (backupId, encryptionKey) = + await getMasterKey(password, gUser.username); await updateUserdata((user) { user.twonlySafeBackup = TwonlySafeBackup( @@ -25,7 +24,7 @@ Future enableTwonlySafe(String password) async { unawaited(performTwonlySafeBackup(force: true)); } -Future disableTwonlySafe() async { +Future removeTwonlySafeFromServer() async { final serverUrl = await getTwonlySafeBackupUrl(); if (serverUrl != null) { try { @@ -41,10 +40,6 @@ Future disableTwonlySafe() async { Log.error('Could not connect to the server.'); } } - await updateUserdata((user) { - user.twonlySafeBackup = null; - return user; - }); } Future<(Uint8List, Uint8List)> getMasterKey( diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 5d18482..21212ec 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -10,30 +10,29 @@ import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart'; Future performTwonlySafeBackup({bool force = false}) async { - final user = await getUser(); - - if (user == null || user.twonlySafeBackup == null || user.isDemoUser) { + if (gUser.twonlySafeBackup == null) { return; } - if (user.twonlySafeBackup!.backupUploadState == + if (gUser.twonlySafeBackup!.backupUploadState == LastBackupUploadState.pending) { Log.warn('Backup upload is already pending.'); return; } - final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone; + final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone; if (!force && lastUpdateTime != null) { if (lastUpdateTime .isAfter(DateTime.now().subtract(const Duration(days: 1)))) { @@ -41,27 +40,26 @@ Future performTwonlySafeBackup({bool force = false}) async { } } - Log.info('Starting new twonly Safe-Backup!'); + Log.info('Starting new twonly Backup!'); final baseDir = (await getApplicationSupportDirectory()).path; final backupDir = Directory(join(baseDir, 'backup_twonly_safe/')); await backupDir.create(recursive: true); - final backupDatabaseFile = - File(join(backupDir.path, 'twonly_database.backup.sqlite')); + final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); final backupDatabaseFileCleaned = - File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite')); + File(join(backupDir.path, 'twonly.backup.cleaned.sqlite')); // copy database - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.copy(backupDatabaseFile.path); driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - final backupDB = TwonlyDatabase( + final backupDB = TwonlyDB( driftDatabase( - name: 'twonly_database.backup', + name: 'twonly.backup', native: DriftNativeOptions( databaseDirectory: () async { return backupDir; @@ -118,8 +116,8 @@ Future performTwonlySafeBackup({bool force = false}) async { final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); - if (user.twonlySafeBackup!.lastBackupDone == null || - user.twonlySafeBackup!.lastBackupDone! + if (gUser.twonlySafeBackup!.lastBackupDone == null || + gUser.twonlySafeBackup!.lastBackupDone! .isAfter(DateTime.now().subtract(const Duration(days: 90)))) { force = true; } @@ -145,7 +143,7 @@ Future performTwonlySafeBackup({bool force = false}) async { final secretBox = await chacha20.encrypt( backupBytes, - secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey), + secretKey: SecretKey(gUser.twonlySafeBackup!.encryptionKey), nonce: nonce, ); @@ -163,11 +161,11 @@ Future performTwonlySafeBackup({bool force = false}) async { await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); Log.info( - 'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.', + 'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.', ); - if (user.backupServer != null) { - if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) { + if (gUser.backupServer != null) { + if (encryptedBackupBytes.length > gUser.backupServer!.maxBackupBytes) { Log.error('Backup is to big for the alternative backup server.'); await updateUserdata((user) { user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; @@ -189,7 +187,7 @@ Future performTwonlySafeBackup({bool force = false}) async { }, ); if (await FileDownloader().enqueue(task)) { - Log.info('Starting upload from twonly Safe backup.'); + Log.info('Starting upload from twonly Backup.'); await updateUserdata((user) { user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; user.twonlySafeBackup!.lastBackupDone = DateTime.now(); @@ -198,7 +196,7 @@ Future performTwonlySafeBackup({bool force = false}) async { }); gUpdateBackupView(); } else { - Log.error('Error starting UploadTask for twonly Safe.'); + Log.error('Error starting UploadTask for twonly Backup.'); } } @@ -206,7 +204,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async { if (update.status == TaskStatus.failed || update.status == TaskStatus.canceled) { Log.error( - 'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}', + 'twonly Backup upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}', ); await updateUserdata((user) { if (user.twonlySafeBackup != null) { @@ -216,7 +214,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async { }); } else if (update.status == TaskStatus.complete) { Log.error( - 'twonly Safe uploaded with status code ${update.responseStatusCode}', + 'twonly Backup uploaded with status code ${update.responseStatusCode}', ); await updateUserdata((user) { if (user.twonlySafeBackup != null) { diff --git a/lib/src/services/twonly_safe/restore.twonly_safe.dart b/lib/src/services/twonly_safe/restore.twonly_safe.dart index 471bef2..7669090 100644 --- a/lib/src/services/twonly_safe/restore.twonly_safe.dart +++ b/lib/src/services/twonly_safe/restore.twonly_safe.dart @@ -10,10 +10,8 @@ import 'package:http/http.dart' as http; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/model/protobuf/backup/backup.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -90,44 +88,9 @@ Future handleBackupData( ); final baseDir = (await getApplicationSupportDirectory()).path; - final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); + final originalDatabase = File(join(baseDir, 'twonly.sqlite')); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); - /// When restoring the last message ID must be increased otherwise - /// receivers would mark them as duplicates as they where already - /// send. - final database = TwonlyDatabase(); - var lastMessageSend = 0; - int? randomUserId; - - final contacts = await database.contactsDao.getAllNotBlockedContacts(); - for (final contact in contacts) { - randomUserId = contact.userId; - final days = DateTime.now().difference(contact.lastMessageExchange).inDays; - if (days < lastMessageSend) { - lastMessageSend = days; - } - } - - if (randomUserId != null) { - // for each day add 400 message ids - final dummyMessagesCounter = (lastMessageSend + 1) * 400; - Log.info( - 'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.', - ); - for (var i = 0; i < dummyMessagesCounter; i++) { - await database.messagesDao.insertMessage( - MessagesCompanion( - contactId: Value(randomUserId), - kind: const Value(MessageKind.ack), - acknowledgeByServer: const Value(true), - errorWhileSending: const Value(true), - ), - ); - } - await database.messagesDao.deleteAllMessagesByContactId(randomUserId); - } - const storage = FlutterSecureStorage(); final secureStorage = jsonDecode(backupContent.secureStorageJson); diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 8917f23..af28bc6 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -9,7 +9,8 @@ void initLogger() { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) async { await _writeLogToFile(record); - if (kDebugMode) { + if (!kReleaseMode) { + // ignore: avoid_print print( '${record.level.name} [twonly] ${record.loggerName} > ${record.message}', ); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 41b1847..27b151e 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,24 +1,26 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:math'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; import 'package:intl/intl.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; -import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; - TwonlyDatabase get db => Provider.of(this); + TwonlyDB get db => Provider.of(this); ColorScheme get color => Theme.of(this).colorScheme; } @@ -65,6 +67,16 @@ Uint8List getRandomUint8List(int length) { return randomBytes; } +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; +Random _rnd = Random(); + +String getRandomString(int length) => String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)), + ), + ); + String errorCodeToText(BuildContext context, ErrorCode code) { // ignore: exhaustive_cases switch (code) { @@ -87,27 +99,26 @@ String errorCodeToText(BuildContext context, ErrorCode code) { case ErrorCode.PlanUpgradeNotYearly: return context.lang.errorPlanUpgradeNotYearly; } - return code.toString(); // Fallback for unrecognized keys + return code.toString(); } -String formatDuration(int seconds) { +String formatDuration(BuildContext context, int seconds) { if (seconds < 60) { - return '$seconds Sec.'; + return '$seconds ${context.lang.durationShortSecond}'; } else if (seconds < 3600) { final minutes = seconds ~/ 60; - return '$minutes Min.'; + return '$minutes ${context.lang.durationShortMinute}'; } else if (seconds < 86400) { final hours = seconds ~/ 3600; - return '$hours Hrs.'; // Assuming "Stu." is for hours + return '$hours ${context.lang.durationShortHour}'; } else { final days = seconds ~/ 86400; - return '$days Days'; + return context.lang.durationShortDays(days); } } InputDecoration getInputDecoration(BuildContext context, String hintText) { - final primaryColor = - Theme.of(context).colorScheme.primary; // Get the primary color + final primaryColor = Theme.of(context).colorScheme.primary; return InputDecoration( hintText: hintText, focusedBorder: OutlineInputBorder( @@ -245,25 +256,111 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; } -String getMessageText(Message message) { +bool isUUIDNewer(String uuid1, String uuid2) { try { - if (message.contentJson == null) return ''; - return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map) - .text; + final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); + final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); + return timestamp1 > timestamp2; } catch (e) { Log.error(e); - return ''; + return false; } } -MediaMessageContent? getMediaContent(Message message) { - try { - if (message.contentJson == null) return null; - return MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - } catch (e) { - Log.error(e); - return null; - } +String uint8ListToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); +} + +Uint8List hexToUint8List(String hex) => Uint8List.fromList( + List.generate( + hex.length ~/ 2, + (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), + ), + ); + +Color getMessageColorFromType( + Message message, + MediaFile? mediaFile, + BuildContext context, +) { + Color color; + + if (message.type == MessageType.text) { + color = Colors.blueAccent; + } else if (mediaFile != null) { + if (mediaFile.requiresAuthentication) { + color = context.color.primary; + } else { + if (mediaFile.type == MediaType.video) { + color = const Color.fromARGB(255, 243, 33, 208); + } else if (mediaFile.type == MediaType.audio) { + color = const Color.fromARGB(255, 252, 149, 85); + } else { + color = Colors.redAccent; + } + } + } else { + return (isDarkMode(context)) ? Colors.white : Colors.black; + } + return color; +} + +String getUUIDforDirectChat(int a, int b) { + if (a < 0 || b < 0) { + throw ArgumentError('Inputs must be non-negative integers.'); + } + if (a > integerMax || b > integerMax) { + throw ArgumentError('Inputs must be <= 0x7fffffff.'); + } + + // Mask to 64 bits in case inputs exceed 64 bits + final mask64 = (BigInt.one << 64) - BigInt.one; + final ai = BigInt.from(a) & mask64; + final bi = BigInt.from(b) & mask64; + + // Ensure the bigger integer is in front (high 64 bits) + final hi = ai >= bi ? ai : bi; + final lo = ai >= bi ? bi : ai; + + final combined = (hi << 64) | lo; + + final hex = combined.toRadixString(16).padLeft(32, '0'); + + final parts = [ + hex.substring(0, 8), + hex.substring(8, 12), + hex.substring(12, 16), + hex.substring(16, 20), + hex.substring(20, 32), + ]; + return parts.join('-'); +} + +String friendlyDateTime( + BuildContext context, + DateTime dt, { + bool includeSeconds = false, + Locale? locale, +}) { + // Build date part + final datePart = + DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt); + + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + var timePart = ''; + if (use24Hour) { + timePart = + DateFormat.jm(Localizations.localeOf(context).toString()).format(dt); + } else { + timePart = + DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt); + } + + return '$timePart $datePart'; +} + +String getAvatarSvg(Uint8List avatarSvgCompressed) { + final raw = gzip.decode(avatarSvgCompressed); + return utf8.decode(raw); } diff --git a/lib/src/utils/pow.dart b/lib/src/utils/pow.dart index 67d2ebe..b932a79 100644 --- a/lib/src/utils/pow.dart +++ b/lib/src/utils/pow.dart @@ -1,12 +1,11 @@ import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; bool isValid(int difficulty, List digest) { final bits = digest.map((i) => i.toRadixString(2).padLeft(8, '0')).join(); return bits.startsWith('0' * difficulty); } -Future calculatePoW(Uint8List prefix, int difficulty) async { +Future calculatePoW(String prefix, int difficulty) async { var i = 0; while (true) { i++; diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index b4b2e04..9466769 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -4,9 +4,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; Future isUserCreated() async { @@ -14,6 +16,7 @@ Future isUserCreated() async { if (user == null) { return false; } + gUser = user; return true; } @@ -33,16 +36,19 @@ Future getUser() async { } } -Future updateUsersPlan(BuildContext context, String planId) async { - context.read().plan = planId; +Future updateUsersPlan( + BuildContext context, + SubscriptionPlan plan, +) async { + context.read().plan = plan; await updateUserdata((user) { - user.subscriptionPlan = planId; + user.subscriptionPlan = plan.name; return user; }); if (!context.mounted) return; - await context.read().updatePlan(planId); + await context.read().updatePlan(plan); } Mutex updateProtection = Mutex(); @@ -50,14 +56,23 @@ Mutex updateProtection = Mutex(); Future updateUserdata( UserData Function(UserData userData) updateUser, ) async { - return updateProtection.protect(() async { + final userData = await updateProtection.protect(() async { final user = await getUser(); if (user == null) return null; final updated = updateUser(user); await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); - return user; + gUser = updated; + return updated; }); + try { + for (final callBack in globalUserDataChangedCallBack.values) { + callBack(); + } + } catch (e) { + Log.error(e); + } + return userData; } Future deleteLocalUserData() async { diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index b522422..4a2be05 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -1,30 +1,22 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; - import 'package:flutter/material.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:path/path.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; class SaveToGalleryButton extends StatefulWidget { const SaveToGalleryButton({ - required this.getMergedImage, + required this.storeImageAsOriginal, required this.isLoading, required this.displayButtonLabel, + required this.mediaService, super.key, - this.mediaUploadId, - this.videoFilePath, }); - final Future Function() getMergedImage; + final Future Function() storeImageAsOriginal; final bool displayButtonLabel; - final File? videoFilePath; - final int? mediaUploadId; + final MediaFileService mediaService; final bool isLoading; @override @@ -53,45 +45,26 @@ class SaveToGalleryButtonState extends State { _imageSaving = true; }); + if (widget.mediaService.mediaFile.type == MediaType.image || + widget.mediaService.mediaFile.type == MediaType.gif) { + await widget.storeImageAsOriginal(); + } + String? res; - var memoryPath = await getMediaBaseFilePath('memories'); - if (widget.mediaUploadId != null) { - memoryPath = join(memoryPath, '${widget.mediaUploadId!}'); - } else { - final random = Random(); - final token = uint8ListToHex( - List.generate(32, (i) => random.nextInt(256)), - ); - memoryPath = join(memoryPath, token); - } - final user = await getUser(); - if (user == null) return; - final storeToGallery = user.storeMediaFilesInGallery; + final storedMediaPath = widget.mediaService.storedPath; - if (widget.videoFilePath != null) { - memoryPath += '.mp4'; - await File(widget.videoFilePath!.path).copy(memoryPath); - unawaited(createThumbnailsForVideo(File(memoryPath))); - if (storeToGallery) { - res = await saveVideoToGallery(widget.videoFilePath!.path); - } - } else { - final imageBytes = await widget.getMergedImage(); - if (imageBytes == null || !mounted) return; - final webPImageBytes = - await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - imageBytes, - quality: 100, - ); - memoryPath += '.png'; - await File(memoryPath).writeAsBytes(webPImageBytes); - unawaited(createThumbnailsForImage(File(memoryPath))); - if (storeToGallery) { - res = await saveImageToGallery(imageBytes); - } + final storeToGallery = gUser.storeMediaFilesInGallery; + + await widget.mediaService.storeMediaFile(); + + if (storeToGallery) { + res = await saveVideoToGallery(storedMediaPath.path); } + + await widget.mediaService.compressMedia(); + await widget.mediaService.createThumbnail(); + if (res == null) { setState(() { _imageSaved = true; diff --git a/lib/src/views/camera/camera_preview_components/send_to.dart b/lib/src/views/camera/camera_preview_components/send_to.dart index b061a9b..76e7a7c 100644 --- a/lib/src/views/camera/camera_preview_components/send_to.dart +++ b/lib/src/views/camera/camera_preview_components/send_to.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; class SendToWidget extends StatelessWidget { @@ -40,7 +41,7 @@ class SendToWidget extends StatelessWidget { style: textStyle, ), Text( - sendTo, + substringBy(sendTo, 20), textAlign: TextAlign.center, style: boldTextStyle, // Use the bold text style here ), @@ -48,9 +49,4 @@ class SendToWidget extends StatelessWidget { ), ); } - - String getContactDisplayName(String contact) { - // Replace this with your actual logic to get the contact display name - return contact; // Placeholder implementation - } } diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index 499a335..856224e 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; class CameraZoomButtons extends StatefulWidget { @@ -23,8 +23,7 @@ class CameraZoomButtons extends StatefulWidget { final double scaleFactor; final Function updateScaleFactor; final SelectedCameraDetails selectedCameraDetails; - final Future Function(int sCameraId, bool init, bool enableAudio) - selectCamera; + final Future Function(int sCameraId, bool init) selectCamera; @override State createState() => _CameraZoomButtonsState(); @@ -51,6 +50,7 @@ class _CameraZoomButtonsState extends State { Future initAsync() async { showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; + Log.info('Found ${gCameras.length} cameras for zoom.'); if (!showWideAngleZoom && Platform.isIOS && gCameras.length == 3) { showWideAngleZoomIOS = true; } @@ -106,7 +106,7 @@ class _CameraZoomButtonsState extends State { ), onPressed: () async { if (showWideAngleZoomIOS) { - await widget.selectCamera(2, true, false); + await widget.selectCamera(2, true); } else { final level = await widget.controller.getMinZoomLevel(); widget.updateScaleFactor(level); @@ -130,7 +130,7 @@ class _CameraZoomButtonsState extends State { onPressed: () async { if (showWideAngleZoomIOS && widget.selectedCameraDetails.cameraId == 2) { - await widget.selectCamera(0, true, false); + await widget.selectCamera(0, true); } else { widget.updateScaleFactor(1.0); } diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 40ded4d..3c7019a 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -1,16 +1,18 @@ import 'dart:async'; import 'dart:io'; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; +import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:screenshot/screenshot.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -23,13 +25,12 @@ import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; -int maxVideoRecordingTime = 15; +int maxVideoRecordingTime = 60; Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( SelectedCameraDetails details, int sCameraId, bool init, - bool enableAudio, ) async { var cameraId = sCameraId; if (cameraId >= gCameras.length) return null; @@ -49,7 +50,7 @@ Future<(SelectedCameraDetails, CameraController)?> initializeCameraController( final cameraController = CameraController( gCameras[cameraId], ResolutionPreset.high, - enableAudio: enableAudio, + enableAudio: await Permission.microphone.isGranted, ); await cameraController.initialize().then((_) async { @@ -89,18 +90,17 @@ class CameraPreviewControllerView extends StatelessWidget { required this.selectCamera, required this.selectedCameraDetails, required this.screenshotController, + required this.isVisible, super.key, - this.sendTo, + this.sendToGroup, }); - final Contact? sendTo; - final Future Function( - int sCameraId, - bool init, - bool enableAudio, - ) selectCamera; + final Group? sendToGroup; + final Future Function(int sCameraId, bool init) + selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; + final bool isVisible; @override Widget build(BuildContext context) { @@ -110,17 +110,17 @@ class CameraPreviewControllerView extends StatelessWidget { if (snap.hasData) { if (snap.data!) { return CameraPreviewView( - sendTo: sendTo, + sendToGroup: sendToGroup, selectCamera: selectCamera, cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, + isVisible: isVisible, ); } else { return PermissionHandlerView( onSuccess: () { - // setState(() {}); - selectCamera(0, true, false); + selectCamera(0, true); }, ); } @@ -138,71 +138,118 @@ class CameraPreviewView extends StatefulWidget { required this.cameraController, required this.selectedCameraDetails, required this.screenshotController, + required this.isVisible, super.key, - this.sendTo, + this.sendToGroup, }); - final Contact? sendTo; + final Group? sendToGroup; final Future Function( int sCameraId, bool init, - bool enableAudio, ) selectCamera; final CameraController? cameraController; final SelectedCameraDetails selectedCameraDetails; final ScreenshotController screenshotController; + final bool isVisible; @override State createState() => _CameraPreviewViewState(); } class _CameraPreviewViewState extends State { - bool sharePreviewIsShown = false; - bool galleryLoadedImageIsShown = false; - bool showSelfieFlash = false; - double basePanY = 0; - double baseScaleFactor = 0; - bool cameraLoaded = false; - bool isVideoRecording = false; - bool hasAudioPermission = true; - bool videoWithAudio = true; - DateTime? videoRecordingStarted; - Timer? videoRecordingTimer; + bool _sharePreviewIsShown = false; + bool _galleryLoadedImageIsShown = false; + bool _showSelfieFlash = false; + double _basePanY = 0; + double _baseScaleFactor = 0; + bool _isVideoRecording = false; + bool _hasAudioPermission = true; + DateTime? _videoRecordingStarted; + Timer? _videoRecordingTimer; - DateTime currentTime = DateTime.now(); + DateTime _currentTime = DateTime.now(); final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); + StreamSubscription? androidVolumeDownSub; + @override void initState() { super.initState(); + initVolumeControl(); initAsync(); } - Future initAsync() async { - hasAudioPermission = await Permission.microphone.isGranted; - - if (!hasAudioPermission) { - final user = await getUser(); - if (user != null) { - if (!user.requestedAudioPermission) { - await updateUserdata((u) { - u.requestedAudioPermission = true; - return u; - }); - await requestMicrophonePermission(); - } + @override + void didUpdateWidget(covariant CameraPreviewView oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isVisible != widget.isVisible) { + if (widget.isVisible) { + initVolumeControl(); + } else { + deInitVolumeControl(); } } - if (!mounted) return; - setState(() {}); } @override void dispose() { - videoRecordingTimer?.cancel(); + _videoRecordingTimer?.cancel(); + deInitVolumeControl(); super.dispose(); } + Future initVolumeControl() async { + if (Platform.isIOS) { + await FlutterVolumeController.updateShowSystemUI(false); + double? startedVolume; + + FlutterVolumeController.addListener( + (volume) async { + if (startedVolume == null) { + startedVolume = volume; + return; + } + if (startedVolume == volume) { + return; + } + // reset the volume back to the original value + await FlutterVolumeController.setVolume(startedVolume!); + await takePicture(); + }, + ); + } + if (Platform.isAndroid) { + androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) { + takePicture(); + }); + } + } + + Future deInitVolumeControl() async { + if (Platform.isIOS) { + await FlutterVolumeController.updateShowSystemUI(true); + FlutterVolumeController.removeListener(); + } + if (Platform.isAndroid) { + await androidVolumeDownSub?.cancel(); + } + } + + Future initAsync() async { + _hasAudioPermission = await Permission.microphone.isGranted; + + if (!_hasAudioPermission && !gUser.requestedAudioPermission) { + await updateUserdata((u) { + u.requestedAudioPermission = true; + return u; + }); + await requestMicrophonePermission(); + } + if (!mounted) return; + setState(() {}); + } + Future requestMicrophonePermission() async { final statuses = await [ Permission.microphone, @@ -210,7 +257,7 @@ class _CameraPreviewViewState extends State { if (statuses[Permission.microphone]!.isPermanentlyDenied) { await openAppSettings(); } else { - hasAudioPermission = await Permission.microphone.isGranted; + _hasAudioPermission = await Permission.microphone.isGranted; setState(() {}); } } @@ -253,16 +300,16 @@ class _CameraPreviewViewState extends State { } Future takePicture() async { - if (sharePreviewIsShown || isVideoRecording) return; + if (_sharePreviewIsShown || _isVideoRecording) return; late Future imageBytes; setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); if (widget.selectedCameraDetails.isFlashOn) { if (isFront) { setState(() { - showSelfieFlash = true; + _showSelfieFlash = true; }); } else { await widget.cameraController?.setFlashMode(FlashMode.torch); @@ -290,7 +337,7 @@ class _CameraPreviewViewState extends State { return; } setState(() { - sharePreviewIsShown = false; + _sharePreviewIsShown = false; }); } @@ -298,18 +345,43 @@ class _CameraPreviewViewState extends State { Future? imageBytes, File? videoFilePath, { bool sharedFromGallery = false, + MediaType? mediaType, }) async { + final type = mediaType ?? + ((videoFilePath != null) ? MediaType.video : MediaType.image); + final mediaFileService = await initializeMediaUpload( + type, + gUser.defaultShowTime, + isDraftMedia: true, + ); + if (!mounted) return true; + + if (mediaFileService == null) { + Log.error('Could not generate media file service'); + return false; + } + + if (videoFilePath != null) { + videoFilePath + ..copySync(mediaFileService.originalPath.path) + ..deleteSync(); + + // Start with compressing the video, to speed up the process in case the video is not changed. + // unawaited(mediaFileService.compressMedia()); + } + + await deInitVolumeControl(); + if (!mounted) return true; + final shouldReturn = await Navigator.push( context, PageRouteBuilder( opaque: false, pageBuilder: (context, a1, a2) => ShareImageEditorView( - videoFilePath: videoFilePath, - imageBytes: imageBytes, + imageBytesFuture: imageBytes, sharedFromGallery: sharedFromGallery, - sendTo: widget.sendTo, - mirrorVideo: isFront && Platform.isAndroid, - useHighQuality: true, + sendToGroup: widget.sendToGroup, + mediaFileService: mediaFileService, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; @@ -320,16 +392,17 @@ class _CameraPreviewViewState extends State { ) as bool?; if (mounted) { setState(() { - sharePreviewIsShown = false; - showSelfieFlash = false; + _sharePreviewIsShown = false; + _showSelfieFlash = false; }); } if (!mounted) return true; + await initVolumeControl(); // shouldReturn is null when the user used the back button if (shouldReturn != null && shouldReturn) { - if (widget.sendTo == null) { + if (widget.sendToGroup == null) { globalUpdateOfHomeViewPageIndex(0); - } else { + } else if (mounted) { Navigator.pop(context); } return true; @@ -337,7 +410,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( widget.selectedCameraDetails.cameraId, false, - false, ); return false; } @@ -355,9 +427,9 @@ class _CameraPreviewViewState extends State { return; } - widget.selectedCameraDetails.scaleFactor = (baseScaleFactor + + widget.selectedCameraDetails.scaleFactor = (_baseScaleFactor + // ignore: avoid_dynamic_calls - (basePanY - (details.localPosition.dy as double)) / 30) + (_basePanY - (details.localPosition.dy as double)) / 30) .clamp(1, widget.selectedCameraDetails.maxAvailableZoom); await widget.cameraController! @@ -369,23 +441,51 @@ class _CameraPreviewViewState extends State { Future pickImageFromGallery() async { setState(() { - galleryLoadedImageIsShown = true; - sharePreviewIsShown = true; + _galleryLoadedImageIsShown = true; + _sharePreviewIsShown = true; }); final picker = ImagePicker(); - final pickedFile = await picker.pickImage(source: ImageSource.gallery); + final pickedFile = await picker.pickMedia(); if (pickedFile != null) { - final imageFile = File(pickedFile.path); + final imageExtensions = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.heic', + '.heif', + '.avif', + ]; + + Log.info('Picket from gallery: ${pickedFile.path}'); + + File? videoFilePath; + Future? imageBytes; + MediaType? mediaType; + + final isImage = + imageExtensions.any((ext) => pickedFile.name.contains(ext)); + if (isImage) { + if (pickedFile.name.contains('.gif')) { + mediaType = MediaType.gif; + } + imageBytes = pickedFile.readAsBytes(); + } else { + videoFilePath = File(pickedFile.path); + } + await pushMediaEditor( - imageFile.readAsBytes(), - null, + imageBytes, + videoFilePath, sharedFromGallery: true, + mediaType: mediaType, ); } setState(() { - galleryLoadedImageIsShown = false; - sharePreviewIsShown = false; + _galleryLoadedImageIsShown = false; + _sharePreviewIsShown = false; }); } @@ -394,41 +494,32 @@ class _CameraPreviewViewState extends State { widget.cameraController!.value.isRecordingVideo) { return; } - var cameraController = widget.cameraController; - if (hasAudioPermission && videoWithAudio) { - cameraController = await widget.selectCamera( - widget.selectedCameraDetails.cameraId, - false, - await Permission.microphone.isGranted && videoWithAudio, - ); - } - setState(() { - isVideoRecording = true; + _isVideoRecording = true; }); try { - await cameraController?.startVideoRecording(); - videoRecordingTimer = + await widget.cameraController?.startVideoRecording(); + _videoRecordingTimer = Timer.periodic(const Duration(milliseconds: 15), (timer) { setState(() { - currentTime = DateTime.now(); + _currentTime = DateTime.now(); }); - if (videoRecordingStarted != null && - currentTime.difference(videoRecordingStarted!).inSeconds >= + if (_videoRecordingStarted != null && + _currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) { timer.cancel(); - videoRecordingTimer = null; + _videoRecordingTimer = null; stopVideoRecording(); } }); setState(() { - videoRecordingStarted = DateTime.now(); - isVideoRecording = true; + _videoRecordingStarted = DateTime.now(); + _isVideoRecording = true; }); } on CameraException catch (e) { setState(() { - isVideoRecording = false; + _isVideoRecording = false; }); _showCameraException(e); return; @@ -436,14 +527,14 @@ class _CameraPreviewViewState extends State { } Future stopVideoRecording() async { - if (videoRecordingTimer != null) { - videoRecordingTimer?.cancel(); - videoRecordingTimer = null; + if (_videoRecordingTimer != null) { + _videoRecordingTimer?.cancel(); + _videoRecordingTimer = null; } setState(() { - videoRecordingStarted = null; - isVideoRecording = false; + _videoRecordingStarted = null; + _isVideoRecording = false; }); if (widget.cameraController == null || @@ -452,23 +543,14 @@ class _CameraPreviewViewState extends State { } setState(() { - sharePreviewIsShown = true; + _sharePreviewIsShown = true; }); try { - File? videoPathFile; final videoPath = await widget.cameraController?.stopVideoRecording(); - if (videoPath != null) { - if (Platform.isAndroid) { - // see https://github.com/flutter/flutter/issues/148335 - await File(videoPath.path).rename('${videoPath.path}.mp4'); - videoPathFile = File('${videoPath.path}.mp4'); - } else { - videoPathFile = File(videoPath.path); - } - } + if (videoPath == null) return; await widget.cameraController?.pausePreview(); - if (await pushMediaEditor(null, videoPathFile)) { + if (await pushMediaEditor(null, File(videoPath.path))) { return; } } on CameraException catch (e) { @@ -505,15 +587,15 @@ class _CameraPreviewViewState extends State { return; } setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); }, onLongPressMoveUpdate: onPanUpdate, onLongPressStart: (details) { setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = widget.selectedCameraDetails.scaleFactor; + _basePanY = details.localPosition.dy; + _baseScaleFactor = widget.selectedCameraDetails.scaleFactor; }); // Get the position of the pointer final renderBox = @@ -536,7 +618,7 @@ class _CameraPreviewViewState extends State { onPanUpdate: onPanUpdate, child: Stack( children: [ - if (galleryLoadedImageIsShown) + if (_galleryLoadedImageIsShown) Center( child: SizedBox( width: 20, @@ -547,11 +629,11 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!sharePreviewIsShown && - widget.sendTo != null && - !isVideoRecording) - SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), - if (!sharePreviewIsShown && !isVideoRecording) + if (!_sharePreviewIsShown && + widget.sendToGroup != null && + !_isVideoRecording) + SendToWidget(sendTo: widget.sendToGroup!.groupName), + if (!_sharePreviewIsShown && !_isVideoRecording) Positioned( right: 5, top: 0, @@ -569,7 +651,6 @@ class _CameraPreviewViewState extends State { await widget.selectCamera( (widget.selectedCameraDetails.cameraId + 1) % 2, false, - false, ); }, ), @@ -594,7 +675,7 @@ class _CameraPreviewViewState extends State { setState(() {}); }, ), - if (!hasAudioPermission) + if (!_hasAudioPermission) ActionButton( Icons.mic_off_rounded, color: Colors.white.withAlpha(160), @@ -602,27 +683,12 @@ class _CameraPreviewViewState extends State { 'Allow microphone access for video recording.', onPressed: requestMicrophonePermission, ), - if (hasAudioPermission) - ActionButton( - videoWithAudio - ? Icons.volume_up_rounded - : Icons.volume_off_rounded, - tooltipText: 'Record video with audio.', - color: videoWithAudio - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - setState(() { - videoWithAudio = !videoWithAudio; - }); - }, - ), ], ), ), ), ), - if (!sharePreviewIsShown) + if (!_sharePreviewIsShown) Positioned( bottom: 30, left: 0, @@ -634,7 +700,7 @@ class _CameraPreviewViewState extends State { if (widget.cameraController!.value.isInitialized && widget.selectedCameraDetails.isZoomAble && !isFront && - !isVideoRecording) + !_isVideoRecording) SizedBox( width: 120, child: CameraZoomButtons( @@ -651,7 +717,7 @@ class _CameraPreviewViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!isVideoRecording) + if (!_isVideoRecording) GestureDetector( onTap: pickImageFromGallery, child: Align( @@ -683,7 +749,7 @@ class _CameraPreviewViewState extends State { shape: BoxShape.circle, border: Border.all( width: 7, - color: isVideoRecording + color: _isVideoRecording ? Colors.red : Colors.white, ), @@ -691,7 +757,7 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!isVideoRecording) const SizedBox(width: 80), + if (!_isVideoRecording) const SizedBox(width: 80), ], ), ], @@ -699,10 +765,10 @@ class _CameraPreviewViewState extends State { ), ), VideoRecordingTimer( - videoRecordingStarted: videoRecordingStarted, + videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!sharePreviewIsShown && widget.sendTo != null) + if (!_sharePreviewIsShown && widget.sendToGroup != null) Positioned( left: 5, top: 10, @@ -714,7 +780,7 @@ class _CameraPreviewViewState extends State { }, ), ), - if (showSelfieFlash) + if (_showSelfieFlash) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(22), diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index b29c109..578c955 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -3,13 +3,13 @@ import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; class CameraSendToView extends StatefulWidget { - const CameraSendToView(this.sendTo, {super.key}); - final Contact sendTo; + const CameraSendToView(this.sendToGroup, {super.key}); + final Group sendToGroup; @override State createState() => CameraSendToViewState(); } @@ -22,7 +22,7 @@ class CameraSendToViewState extends State { @override void initState() { super.initState(); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); } @override @@ -36,13 +36,11 @@ class CameraSendToViewState extends State { Future selectCamera( int sCameraId, bool init, - bool enableAudio, ) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -61,7 +59,7 @@ class CameraSendToViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } @override @@ -77,10 +75,11 @@ class CameraSendToViewState extends State { ), CameraPreviewControllerView( selectCamera: selectCamera, - sendTo: widget.sendTo, + sendToGroup: widget.sendToGroup, cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, + isVisible: true, ), ], ), diff --git a/lib/src/views/camera/image_editor/data/layer.dart b/lib/src/views/camera/image_editor/data/layer.dart index 707e348..7f6bdf8 100755 --- a/lib/src/views/camera/image_editor/data/layer.dart +++ b/lib/src/views/camera/image_editor/data/layer.dart @@ -34,7 +34,9 @@ class BackgroundLayerData extends Layer { ImageItem image; } -class FilterLayerData extends Layer {} +class FilterLayerData extends Layer { + int page = 1; +} /// Attributes used by [EmojiLayer] class EmojiLayerData extends Layer { diff --git a/lib/src/views/camera/image_editor/layers/filter_layer.dart b/lib/src/views/camera/image_editor/layers/filter_layer.dart index c0179d0..13e76a2 100644 --- a/lib/src/views/camera/image_editor/layers/filter_layer.dart +++ b/lib/src/views/camera/image_editor/layers/filter_layer.dart @@ -116,6 +116,7 @@ class _FilterLayerState extends State { } }, onPageChanged: (index) { + widget.layerData.page = index; if (index == 0) { // If the user swipes to the first duplicated page, jump to the last page pageController.jumpToPage(pages.length); diff --git a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart index 5b3ae64..d8ef27f 100644 --- a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart +++ b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart @@ -131,7 +131,7 @@ Future> getStickerIndex() async { final indexFile = File('${directory.path}/stickers.json'); var res = []; - if (indexFile.existsSync() && !kDebugMode) { + if (indexFile.existsSync() && kReleaseMode) { final lastModified = indexFile.lastModifiedSync(); final difference = DateTime.now().difference(lastModified); final content = await indexFile.readAsString(); diff --git a/lib/src/views/camera/image_editor/layers/text_layer.dart b/lib/src/views/camera/image_editor/layers/text_layer.dart index 5cff596..72dc428 100755 --- a/lib/src/views/camera/image_editor/layers/text_layer.dart +++ b/lib/src/views/camera/image_editor/layers/text_layer.dart @@ -24,6 +24,7 @@ class TextLayer extends StatefulWidget { class _TextViewState extends State { double initialRotation = 0; bool deleteLayer = false; + double localBottom = 0; bool isDeleted = false; bool elementIsScaled = false; final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey @@ -35,9 +36,19 @@ class _TextViewState extends State { textController.text = widget.layerData.text; - if (widget.layerData.offset.dy == 0) { - // Set the initial offset to the center of the screen - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final mq = MediaQuery.of(context); + final globalDesiredBottom = mq.viewInsets.bottom + mq.viewPadding.bottom; + final parentBox = context.findRenderObject() as RenderBox?; + if (parentBox != null) { + final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy; + final screenHeight = mq.size.height; + localBottom = (screenHeight - globalDesiredBottom) - + parentTopGlobal - + (parentBox.size.height); + } + + if (widget.layerData.offset.dy == 0) { setState(() { widget.layerData.offset = Offset( 0, @@ -47,17 +58,20 @@ class _TextViewState extends State { ); textController.text = widget.layerData.text; }); - }); - } + } + }); } @override Widget build(BuildContext context) { if (widget.layerData.isDeleted) return Container(); + final bottom = MediaQuery.of(context).viewInsets.bottom + + MediaQuery.of(context).viewPadding.bottom; + if (widget.layerData.isEditing) { return Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom - 100, + bottom: bottom - localBottom, left: 0, right: 0, child: Container( diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index 18775ea..7926084 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -1,111 +1,88 @@ -import 'dart:async'; - +import 'dart:io'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/image_editor/data/data.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -class Emojis extends StatefulWidget { - const Emojis({super.key}); - - @override - State createState() => _EmojisState(); -} - -class _EmojisState extends State { - List lastUsed = emojis; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - setState(() { - lastUsed = user.lastUsedEditorEmojis ?? []; - lastUsed.addAll(emojis); - }); - } - - Future selectEmojis(String emoji) async { - await updateUserdata((user) { - if (user.lastUsedEditorEmojis == null) { - user.lastUsedEditorEmojis = [emoji]; - } else { - if (user.lastUsedEditorEmojis!.contains(emoji)) { - user.lastUsedEditorEmojis!.remove(emoji); - } - user.lastUsedEditorEmojis!.insert(0, emoji); - if (user.lastUsedEditorEmojis!.length > 12) { - user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12); - } - user.lastUsedEditorEmojis!.toSet().toList(); - } - return user; - }); - if (!mounted) return; - Navigator.pop( - context, - EmojiLayerData( - text: emoji, - ), - ); - } +class EmojiPickerBottom extends StatelessWidget { + const EmojiPickerBottom({super.key}); @override Widget build(BuildContext context) { return SingleChildScrollView( child: Container( padding: EdgeInsets.zero, - height: 400, - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(32), topRight: Radius.circular(32), ), - color: Colors.black, + color: context.color.surfaceContainer, boxShadow: [ BoxShadow( blurRadius: 10.9, - color: Color.fromRGBO(0, 0, 0, 0.1), + color: context.color.surfaceContainer.withAlpha(25), ), ], ), - child: Column( - children: [ - const SizedBox(height: 16), - Container( - height: 315, - padding: EdgeInsets.zero, - child: GridView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 60, + child: SafeArea( + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, ), - children: lastUsed.map((String emoji) { - return GridTile( - child: GestureDetector( - onTap: () async { - await selectEmojis(emoji); - }, - child: Container( - padding: EdgeInsets.zero, - alignment: Alignment.center, - child: Text( - emoji, - style: const TextStyle(fontSize: 35), - ), - ), - ), - ); - }).toList(), + height: 3, + width: 60, ), - ), - ], + Expanded( + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + Navigator.pop( + context, + EmojiLayerData( + text: emoji.emoji, + ), + ); + }, + // textEditingController: _textFieldController, + config: Config( + height: 400, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/src/views/camera/share_image_components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart index 86ba2c9..7271518 100644 --- a/lib/src/views/camera/share_image_components/best_friends_selector.dart +++ b/lib/src/views/camera/share_image_components/best_friends_selector.dart @@ -1,37 +1,32 @@ -// ignore_for_file: strict_raw_type - import 'dart:collection'; - import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; class BestFriendsSelector extends StatelessWidget { const BestFriendsSelector({ - required this.users, - required this.isRealTwonly, - required this.updateStatus, - required this.selectedUserIds, + required this.groups, + required this.selectedGroupIds, + required this.updateSelectedGroupIds, required this.title, + required this.showSelectAll, super.key, }); - final List users; - final void Function(int, bool) updateStatus; - final HashSet selectedUserIds; - final bool isRealTwonly; + final List groups; + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; final String title; + final bool showSelectAll; @override Widget build(BuildContext context) { - if (users.isEmpty) { + if (groups.isEmpty) { return Container(); } - return Column( children: [ Row( @@ -39,11 +34,11 @@ class BestFriendsSelector extends StatelessWidget { Expanded( child: HeadLineComponent(title), ), - if (!isRealTwonly) + if (showSelectAll) GestureDetector( onTap: () { - for (final user in users) { - updateStatus(user.userId, true); + for (final group in groups) { + updateSelectedGroupIds(group.groupId, true); } }, child: Container( @@ -70,7 +65,7 @@ class BestFriendsSelector extends StatelessWidget { Column( spacing: 8, children: List.generate( - (users.length + 1) ~/ 2, + (groups.length + 1) ~/ 2, (rowIndex) { final firstUserIndex = rowIndex * 2; final secondUserIndex = firstUserIndex + 1; @@ -79,21 +74,19 @@ class BestFriendsSelector extends StatelessWidget { children: [ Expanded( child: UserCheckbox( - isChecked: selectedUserIds - .contains(users[firstUserIndex].userId), - user: users[firstUserIndex], - onChanged: updateStatus, - isRealTwonly: isRealTwonly, + isChecked: selectedGroupIds + .contains(groups[firstUserIndex].groupId), + group: groups[firstUserIndex], + onChanged: updateSelectedGroupIds, ), ), - if (secondUserIndex < users.length) + if (secondUserIndex < groups.length) Expanded( child: UserCheckbox( - isChecked: selectedUserIds - .contains(users[secondUserIndex].userId), - user: users[secondUserIndex], - onChanged: updateStatus, - isRealTwonly: isRealTwonly, + isChecked: selectedGroupIds + .contains(groups[secondUserIndex].groupId), + group: groups[secondUserIndex], + onChanged: updateSelectedGroupIds, ), ) else @@ -112,28 +105,24 @@ class BestFriendsSelector extends StatelessWidget { class UserCheckbox extends StatelessWidget { const UserCheckbox({ - required this.user, + required this.group, required this.onChanged, - required this.isRealTwonly, required this.isChecked, super.key, }); - final Contact user; - final void Function(int, bool) onChanged; + final Group group; + final void Function(String, bool) onChanged; final bool isChecked; - final bool isRealTwonly; @override Widget build(BuildContext context) { - final displayName = getContactDisplayName(user); - return Container( padding: const EdgeInsets.symmetric( horizontal: 3, ), // Padding inside the container child: GestureDetector( onTap: () { - onChanged(user.userId, !isChecked); + onChanged(group.groupId, !isChecked); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), @@ -149,8 +138,8 @@ class UserCheckbox extends StatelessWidget { ), child: Row( children: [ - ContactAvatar( - contact: user, + AvatarIcon( + group: group, fontSize: 12, ), const SizedBox(width: 8), @@ -160,28 +149,19 @@ class UserCheckbox extends StatelessWidget { Row( children: [ Text( - displayName.length > 8 - ? '${displayName.substring(0, 8)}...' - : displayName, + substringBy(group.groupName, 12), overflow: TextOverflow.ellipsis, ), ], ), - StreamBuilder( - stream: twonlyDB.contactsDao.watchFlameCounter(user.userId), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data! == 0) { - return Container(); - } - return FlameCounterWidget(user, snapshot.data!); - }, - ), + FlameCounterWidget(groupId: group.groupId), ], ), Expanded(child: Container()), Checkbox( value: isChecked, side: WidgetStateBorderSide.resolveWith( + // ignore: strict_raw_type (Set states) { if (states.contains(WidgetState.selected)) { return const BorderSide(width: 0); @@ -192,7 +172,7 @@ class UserCheckbox extends StatelessWidget { }, ), onChanged: (bool? value) { - onChanged(user.userId, value ?? false); + onChanged(group.groupId, value ?? false); }, ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 6dc0239..3db605c 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,15 +2,18 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:io'; - +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -23,71 +26,76 @@ import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/camera/share_image_view.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:video_player/video_player.dart'; List layers = []; List undoLayers = []; List removedLayers = []; -const gMediaShowInfinite = 999999; - class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ - required this.mirrorVideo, - required this.useHighQuality, required this.sharedFromGallery, + required this.mediaFileService, super.key, - this.imageBytes, - this.sendTo, - this.videoFilePath, + this.imageBytesFuture, + this.sendToGroup, }); - final Future? imageBytes; - final File? videoFilePath; - final Contact? sendTo; - final bool mirrorVideo; - final bool useHighQuality; + final Future? imageBytesFuture; + final Group? sendToGroup; final bool sharedFromGallery; + final MediaFileService mediaFileService; @override State createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { - bool _isRealTwonly = false; - int maxShowTime = gMediaShowInfinite; double tabDownPosition = 0; bool sendingOrLoadingImage = true; bool loadingImage = true; bool isDisposed = false; - HashSet selectedUserIds = HashSet(); + HashSet selectedGroupIds = HashSet(); double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; + Uint8List? imageBytes; VideoPlayerController? videoController; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); - /// Media upload variables - int? mediaUploadId; - Future? videoUploadHandler; + MediaFileService get mediaService => widget.mediaFileService; + MediaFile get media => widget.mediaFileService.mediaFile; @override void initState() { super.initState(); - unawaited(initAsync()); - unawaited(initMediaFileUpload()); - layers.add(FilterLayerData()); - if (widget.sendTo != null) { - selectedUserIds.add(widget.sendTo!.userId); + + if (media.type != MediaType.gif) { + layers.add(FilterLayerData()); } - if (widget.imageBytes != null) { - unawaited(loadImage(widget.imageBytes!)); - } else if (widget.videoFilePath != null) { + + if (widget.sendToGroup != null) { + selectedGroupIds.add(widget.sendToGroup!.groupId); + } + + if (widget.mediaFileService.mediaFile.type == MediaType.image || + widget.mediaFileService.mediaFile.type == MediaType.gif) { + if (widget.imageBytesFuture != null) { + loadImage(widget.imageBytesFuture!); + } else { + if (widget.mediaFileService.tempPath.existsSync()) { + loadImage(widget.mediaFileService.tempPath.readAsBytes()); + } else if (widget.mediaFileService.originalPath.existsSync()) { + loadImage(widget.mediaFileService.originalPath.readAsBytes()); + } + } + } + + if (media.type == MediaType.video) { setState(() { sendingOrLoadingImage = false; loadingImage = false; }); - videoController = VideoPlayerController.file(widget.videoFilePath!); + videoController = VideoPlayerController.file(mediaService.originalPath); videoController?.setLooping(true); videoController?.initialize().then((_) async { await videoController!.play(); @@ -97,45 +105,27 @@ class _ShareImageEditorView extends State { } } - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - if (user.defaultShowTime != null) { - setState(() { - maxShowTime = user.defaultShowTime!; - }); - } - } - - Future initMediaFileUpload() async { - // media init was already called... - if (mediaUploadId != null) return; - - mediaUploadId = await initMediaUpload(); - - if (widget.videoFilePath != null && mediaUploadId != null) { - // start with the video compression... - videoUploadHandler = - addVideoToUpload(mediaUploadId!, widget.videoFilePath!); - } - } - @override void dispose() { isDisposed = true; layers.clear(); videoController?.dispose(); + twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); super.dispose(); } - void updateStatus(int userId, bool checked) { + void updateSelectedGroupIds(String groupId, bool checked) { if (checked) { - if (_isRealTwonly) { - selectedUserIds.clear(); + if (media.requiresAuthentication) { + selectedGroupIds.clear(); } - selectedUserIds.add(userId); + selectedGroupIds.add(groupId); } else { - selectedUserIds.remove(userId); + selectedGroupIds.remove(groupId); } setState(() {}); } @@ -147,86 +137,88 @@ class _ShareImageEditorView extends State { return []; } return [ - ActionButton( - Icons.text_fields_rounded, - tooltipText: context.lang.addTextItem, - onPressed: () async { - layers = layers.where((x) => !x.isDeleted).toList(); - if (layers.any((x) => x.isEditing)) return; - undoLayers.clear(); - removedLayers.clear(); - layers.add( - TextLayerData( - textLayersBefore: layers.whereType().length, - ), - ); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.text_fields_rounded, + tooltipText: context.lang.addTextItem, + onPressed: () async { + layers = layers.where((x) => !x.isDeleted).toList(); + if (layers.any((x) => x.isEditing)) return; + undoLayers.clear(); + removedLayers.clear(); + layers.add( + TextLayerData( + textLayersBefore: layers.whereType().length, + ), + ); + setState(() {}); + }, + ), const SizedBox(height: 8), - ActionButton( - Icons.draw_rounded, - tooltipText: context.lang.addDrawing, - onPressed: () async { - undoLayers.clear(); - removedLayers.clear(); - layers.add(DrawLayerData()); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.draw_rounded, + tooltipText: context.lang.addDrawing, + onPressed: () async { + undoLayers.clear(); + removedLayers.clear(); + layers.add(DrawLayerData()); + setState(() {}); + }, + ), const SizedBox(height: 8), - ActionButton( - Icons.add_reaction_outlined, - tooltipText: context.lang.addEmoji, - onPressed: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ) as Layer?; - if (layer == null) return; - undoLayers.clear(); - removedLayers.clear(); - layers.add(layer); - setState(() {}); - }, - ), + if (media.type != MediaType.gif) + ActionButton( + Icons.add_reaction_outlined, + tooltipText: context.lang.addEmoji, + onPressed: () async { + final layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const EmojiPickerBottom(); + }, + ) as Layer?; + if (layer == null) return; + undoLayers.clear(); + removedLayers.clear(); + layers.add(layer); + setState(() {}); + }, + ), const SizedBox(height: 8), NotificationBadge( - count: (widget.videoFilePath != null) + count: (media.type == MediaType.video) ? '0' - : maxShowTime == gMediaShowInfinite + : media.displayLimitInMilliseconds == null ? '∞' - : maxShowTime.toString(), + : media.displayLimitInMilliseconds.toString(), child: ActionButton( - (widget.videoFilePath != null) - ? maxShowTime == gMediaShowInfinite + (media.type == MediaType.video) + ? media.displayLimitInMilliseconds == null ? Icons.repeat_rounded : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, onPressed: () async { - if (widget.videoFilePath != null) { - setState(() { - if (maxShowTime == gMediaShowInfinite) { - maxShowTime = 0; - } else { - maxShowTime = gMediaShowInfinite; - } - }); + if (media.type == MediaType.video) { + await mediaService.setDisplayLimit( + (media.displayLimitInMilliseconds == null) ? 0 : null, + ); + if (!mounted) return; + setState(() {}); return; } - if (maxShowTime == gMediaShowInfinite) { + int? maxShowTime; + if (media.displayLimitInMilliseconds == null) { maxShowTime = 1; - } else if (maxShowTime == 1) { + } else if (media.displayLimitInMilliseconds == 1) { maxShowTime = 5; - } else if (maxShowTime == 5) { + } else if (media.displayLimitInMilliseconds == 5) { maxShowTime = 20; - } else { - maxShowTime = gMediaShowInfinite; } + await mediaService.setDisplayLimit(maxShowTime); + if (!mounted) return; setState(() {}); await updateUserdata((user) { user.defaultShowTime = maxShowTime; @@ -235,19 +227,35 @@ class _ShareImageEditorView extends State { }, ), ), + if (media.type == MediaType.video) + ActionButton( + (mediaService.removeAudio) + ? Icons.volume_off_rounded + : Icons.volume_up_rounded, + tooltipText: 'Enable Audio in Video', + color: (mediaService.removeAudio) + ? Colors.white.withAlpha(160) + : Colors.white, + onPressed: () async { + await mediaService.toggleRemoveAudio(); + if (mediaService.removeAudio) { + await videoController?.setVolume(0); + } else { + await videoController?.setVolume(100); + } + if (mounted) setState(() {}); + }, + ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.shieldHeart, tooltipText: context.lang.protectAsARealTwonly, - color: _isRealTwonly + color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white, onPressed: () async { - _isRealTwonly = !_isRealTwonly; - if (_isRealTwonly) { - maxShowTime = 12; - } - selectedUserIds = HashSet(); + await mediaService.setRequiresAuth(!media.requiresAuthentication); + selectedGroupIds = HashSet(); setState(() {}); }, ), @@ -308,25 +316,18 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - if (mediaUploadId == null) { - await initMediaFileUpload(); - if (mediaUploadId == null) return; - } - final imageBytes = getMergedImage(); + final mediaStoreFuture = storeImageAsOriginal(); + await videoController?.pause(); if (isDisposed || !mounted) return; final wasSend = await Navigator.push( context, MaterialPageRoute( builder: (context) => ShareImageView( - imageBytesFuture: imageBytes, - isRealTwonly: _isRealTwonly, - maxShowTime: maxShowTime, - selectedUserIds: selectedUserIds, - updateStatus: updateStatus, - videoUploadHandler: videoUploadHandler, - mediaUploadId: mediaUploadId!, - mirrorVideo: widget.mirrorVideo, + selectedGroupIds: selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + mediaStoreFuture: mediaStoreFuture, + mediaFileService: mediaService, ), ), ) as bool?; @@ -337,31 +338,69 @@ class _ShareImageEditorView extends State { } } - Future getMergedImage() async { - Uint8List? image; - - if (layers.length > 1 || widget.videoFilePath != null) { - for (final x in layers) { - x.showCustomButtons = false; - } - setState(() {}); - image = await screenshotController.capture( - pixelRatio: (widget.useHighQuality) ? pixelRatio : 1, - ); - for (final x in layers) { - x.showCustomButtons = true; - } - setState(() {}); - } else if (layers.length == 1) { + Future getEditedImageBytes() async { + if (layers.length == 1) { if (layers.first is BackgroundLayerData) { - image = (layers.first as BackgroundLayerData).image.bytes; + final image = (layers.first as BackgroundLayerData).image.bytes; + return image; } } + + for (final x in layers) { + x.showCustomButtons = false; + } + setState(() {}); + final image = await screenshotController.capture( + pixelRatio: pixelRatio, + ); + if (image == null) { + Log.error('screenshotController did not return image bytes'); + return null; + } + + for (final x in layers) { + x.showCustomButtons = true; + } + setState(() {}); return image; } - Future loadImage(Future imageFile) async { - final imageBytes = await imageFile; + Future storeImageAsOriginal() async { + if (mediaService.overlayImagePath.existsSync()) { + mediaService.overlayImagePath.deleteSync(); + } + if (mediaService.tempPath.existsSync()) { + mediaService.tempPath.deleteSync(); + } + if (media.type == MediaType.gif) { + mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + } else { + final imageBytes = await getEditedImageBytes(); + if (imageBytes == null) return false; + if (media.type == MediaType.image || media.type == MediaType.gif) { + mediaService.originalPath.writeAsBytesSync(imageBytes); + } else if (media.type == MediaType.video) { + mediaService.overlayImagePath.writeAsBytesSync(imageBytes); + } else { + Log.error('MediaType not supported: ${media.type}'); + } + } + + // In case the image was already stored, then rename the stored image. + if (mediaService.storedPath.existsSync()) { + final newPath = mediaService.storedPath.absolute.path + .replaceFirst(media.mediaId, uuid.v7()); + mediaService.storedPath.renameSync(newPath); + } + return true; + } + + Future loadImage(Future imageBytesFuture) async { + imageBytes = await imageBytesFuture; + + // store this image so it can be used as a draft in case the app is restarted + mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + await currentImage.load(imageBytes); if (isDisposed) return; @@ -384,56 +423,20 @@ class _ShareImageEditorView extends State { setState(() { sendingOrLoadingImage = true; }); - final imageBytes = await getMergedImage(); + + await storeImageAsOriginal(); + if (!context.mounted) return; - if (imageBytes == null) { + + // Insert media file into the messages database and start uploading process in the background + await insertMediaFileInMessagesTable( + mediaService, + [widget.sendToGroup!.groupId], + ); + + if (context.mounted) { // ignore: use_build_context_synchronously Navigator.pop(context, true); - return; - } - final err = await isAllowedToSend(); - if (!context.mounted) return; - - if (err != null) { - setState(() { - sendingOrLoadingImage = false; - }); - if (mounted) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SubscriptionView( - redirectError: err, - ); - }, - ), - ); - } - } else { - final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes); - - // first finalize the upload - await finalizeUpload( - mediaUploadId!, - [widget.sendTo!.userId], - _isRealTwonly, - widget.videoFilePath != null, - widget.mirrorVideo, - maxShowTime, - ); - - /// then call the upload process in the background - await encryptMediaFiles( - mediaUploadId!, - imageHandler, - videoUploadHandler, - ); - - if (context.mounted) { - // ignore: use_build_context_synchronously - Navigator.pop(context, true); - } } } @@ -478,14 +481,13 @@ class _ShareImageEditorView extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ SaveToGalleryButton( - getMergedImage: getMergedImage, - mediaUploadId: mediaUploadId, - videoFilePath: widget.videoFilePath, - displayButtonLabel: widget.sendTo == null, + storeImageAsOriginal: storeImageAsOriginal, + mediaService: mediaService, + displayButtonLabel: widget.sendToGroup == null, isLoading: loadingImage, ), - if (widget.sendTo != null) const SizedBox(width: 10), - if (widget.sendTo != null) + if (widget.sendToGroup != null) const SizedBox(width: 10), + if (widget.sendToGroup != null) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: Theme.of(context).colorScheme.primary, @@ -495,7 +497,7 @@ class _ShareImageEditorView extends State { onPressed: pushShareImageView, child: const FaIcon(FontAwesomeIcons.userPlus), ), - SizedBox(width: widget.sendTo == null ? 20 : 10), + SizedBox(width: widget.sendToGroup == null ? 20 : 10), FilledButton.icon( icon: sendingOrLoadingImage ? SizedBox( @@ -511,7 +513,9 @@ class _ShareImageEditorView extends State { : const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { if (sendingOrLoadingImage) return; - if (widget.sendTo == null) return pushShareImageView(); + if (widget.sendToGroup == null) { + return pushShareImageView(); + } await sendImageToSinglePerson(); }, style: ButtonStyle( @@ -523,9 +527,9 @@ class _ShareImageEditorView extends State { ), ), label: Text( - (widget.sendTo == null) + (widget.sendToGroup == null) ? context.lang.shareImagedEditorShareWith - : getContactDisplayName(widget.sendTo!), + : substringBy(widget.sendToGroup!.groupName, 15), style: const TextStyle(fontSize: 17), ), ), @@ -539,10 +543,7 @@ class _ShareImageEditorView extends State { children: [ if (videoController != null) Positioned.fill( - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(videoController!), - ), + child: VideoPlayer(videoController!), ), Screenshot( controller: screenshotController, diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index f957ba6..38fa15f 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -2,105 +2,88 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/daos/groups.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/verified_shield.dart'; -import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ - required this.imageBytesFuture, - required this.isRealTwonly, - required this.mirrorVideo, - required this.maxShowTime, - required this.selectedUserIds, - required this.updateStatus, - required this.videoUploadHandler, - required this.mediaUploadId, + required this.selectedGroupIds, + required this.updateSelectedGroupIds, + required this.mediaStoreFuture, + required this.mediaFileService, super.key, - this.enableVideoAudio, }); - final Future imageBytesFuture; - final bool isRealTwonly; - final bool mirrorVideo; - final int maxShowTime; - final HashSet selectedUserIds; - final bool? enableVideoAudio; - final int mediaUploadId; - final void Function(int, bool) updateStatus; - final Future? videoUploadHandler; + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; + final Future? mediaStoreFuture; + final MediaFileService mediaFileService; @override State createState() => _ShareImageView(); } class _ShareImageView extends State { - List contacts = []; - List _otherUsers = []; - List _bestFriends = []; - List _pinnedContacts = []; - Uint8List? imageBytes; + List contacts = []; + List _otherUsers = []; + List _bestFriends = []; + List _pinnedContacts = []; + bool sendingImage = false; + bool mediaStoreFutureReady = false; bool hideArchivedUsers = true; final TextEditingController searchUserName = TextEditingController(); - late StreamSubscription> contactSub; + late StreamSubscription> allGroupSub; String lastQuery = ''; @override void initState() { super.initState(); - final allContacts = twonlyDB.contactsDao.watchContactsForShareView(); - - contactSub = allContacts.listen((allContacts) async { + allGroupSub = + twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async { setState(() { - contacts = allContacts; + contacts = allGroups; }); - await updateUsers(allContacts.where((x) => !x.archived).toList()); + await updateGroups(allGroups.where((x) => !x.archived).toList()); }); unawaited(initAsync()); } Future initAsync() async { - imageBytes = await widget.imageBytesFuture; - if (imageBytes != null) { - final imageHandler = - addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!); - // start with the pre upload of the media file... - await encryptMediaFiles( - widget.mediaUploadId, - imageHandler, - widget.videoUploadHandler, - ); + if (widget.mediaStoreFuture != null) { + await widget.mediaStoreFuture; } + mediaStoreFutureReady = true; + // unawaited(startBackgroundMediaUpload(widget.mediaFileService)); if (!mounted) return; setState(() {}); } @override void dispose() { - unawaited(contactSub.cancel()); + unawaited(allGroupSub.cancel()); super.dispose(); } - Future updateUsers(List users) async { + Future updateGroups(List groups) async { // Sort contacts by flameCounter and then by totalMediaCounter - users.sort((a, b) { + groups.sort((a, b) { // First, compare by flameCounter - final flameComparison = getFlameCounterFromContact(b) - .compareTo(getFlameCounterFromContact(a)); + + final flameComparison = + getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a)); if (flameComparison != 0) { return flameComparison; // Sort by flameCounter in descending order } @@ -111,18 +94,18 @@ class _ShareImageView extends State { }); // Separate best friends and other users - final bestFriends = []; - final otherUsers = []; - final pinnedContacts = users.where((c) => c.pinned).toList(); + final bestFriends = []; + final otherUsers = []; + final pinnedContacts = groups.where((c) => c.pinned).toList(); - for (final contact in users) { - if (contact.pinned) continue; - if (!contact.archived && - (getFlameCounterFromContact(contact)) > 0 && + for (final group in groups) { + if (group.pinned) continue; + if (!group.archived && + (getFlameCounterFromGroup(group)) > 0 && bestFriends.length < 6) { - bestFriends.add(contact); + bestFriends.add(group); } else { - otherUsers.add(contact); + otherUsers.add(group); } } @@ -136,13 +119,13 @@ class _ShareImageView extends State { Future _filterUsers(String query) async { lastQuery = query; if (query.isEmpty) { - await updateUsers( + await updateGroups( contacts .where( (x) => !x.archived || !hideArchivedUsers || - widget.selectedUserIds.contains(x.userId), + widget.selectedGroupIds.contains(x.groupId), ) .toList(), ); @@ -150,16 +133,14 @@ class _ShareImageView extends State { } final usersFiltered = contacts .where( - (user) => getContactDisplayName(user) - .toLowerCase() - .contains(query.toLowerCase()), + (user) => user.groupName.toLowerCase().contains(query.toLowerCase()), ) .toList(); - await updateUsers(usersFiltered); + await updateGroups(usersFiltered); } - void updateStatus(int userId, bool checked) { - widget.updateStatus(userId, checked); + void updateSelectedGroupIds(String groupId, bool checked) { + widget.updateSelectedGroupIds(groupId, checked); setState(() {}); } @@ -187,19 +168,21 @@ class _ShareImageView extends State { ), if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), BestFriendsSelector( - users: _pinnedContacts, - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + groups: _pinnedContacts, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, title: context.lang.shareImagePinnedContacts, + showSelectAll: + !widget.mediaFileService.mediaFile.requiresAuthentication, ), const SizedBox(height: 10), BestFriendsSelector( - users: _bestFriends, - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + groups: _bestFriends, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, title: context.lang.shareImageBestFriends, + showSelectAll: + !widget.mediaFileService.mediaFile.requiresAuthentication, ), const SizedBox(height: 10), if (_otherUsers.isNotEmpty) @@ -243,9 +226,8 @@ class _ShareImageView extends State { Expanded( child: UserList( List.from(_otherUsers), - selectedUserIds: widget.selectedUserIds, - isRealTwonly: widget.isRealTwonly, - updateStatus: updateStatus, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, ), ), ], @@ -260,7 +242,7 @@ class _ShareImageView extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton.icon( - icon: imageBytes == null || sendingImage + icon: !mediaStoreFutureReady || sendingImage ? SizedBox( height: 12, width: 12, @@ -271,50 +253,28 @@ class _ShareImageView extends State { ) : const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { - if (imageBytes == null || widget.selectedUserIds.isEmpty) { + if (!mediaStoreFutureReady || + widget.selectedGroupIds.isEmpty) { return; } - final err = await isAllowedToSend(); - if (!context.mounted) return; + setState(() { + sendingImage = true; + }); - if (err != null) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SubscriptionView( - redirectError: err, - ); - }, - ), - ); - } else { - setState(() { - sendingImage = true; - }); + await insertMediaFileInMessagesTable( + widget.mediaFileService, + widget.selectedGroupIds.toList(), + ); - await finalizeUpload( - widget.mediaUploadId, - widget.selectedUserIds.toList(), - widget.isRealTwonly, - widget.videoUploadHandler != null, - widget.mirrorVideo, - widget.maxShowTime, - ); - - /// trigger the upload of the media file. - unawaited(handleNextMediaUploadSteps(widget.mediaUploadId)); - - if (context.mounted) { - Navigator.pop(context, true); - // if (widget.preselectedUser != null) { - // Navigator.pop(context, true); - // } else { - // Navigator.popUntil(context, (route) => route.isFirst, true); - // globalUpdateOfHomeViewPageIndex(1); - // } - } + if (context.mounted) { + Navigator.pop(context, true); + // if (widget.preselectedUser != null) { + // Navigator.pop(context, true); + // } else { + // Navigator.popUntil(context, (route) => route.isFirst, true); + // globalUpdateOfHomeViewPageIndex(1); + // } } }, style: ButtonStyle( @@ -322,7 +282,7 @@ class _ShareImageView extends State { const EdgeInsets.symmetric(vertical: 10, horizontal: 30), ), backgroundColor: WidgetStateProperty.all( - imageBytes == null || widget.selectedUserIds.isEmpty + mediaStoreFutureReady || widget.selectedGroupIds.isEmpty ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.primary, ), @@ -342,52 +302,42 @@ class _ShareImageView extends State { class UserList extends StatelessWidget { const UserList( - this.users, { - required this.selectedUserIds, - required this.updateStatus, - required this.isRealTwonly, + this.groups, { + required this.selectedGroupIds, + required this.updateSelectedGroupIds, super.key, }); - final void Function(int, bool) updateStatus; - final List users; - final bool isRealTwonly; - final HashSet selectedUserIds; + final void Function(String, bool) updateSelectedGroupIds; + final List groups; + final HashSet selectedGroupIds; @override Widget build(BuildContext context) { // Step 1: Sort the users alphabetically - users + groups .sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); return ListView.builder( restorationId: 'new_message_users_list', - itemCount: users.length, + itemCount: groups.length, itemBuilder: (BuildContext context, int i) { - final user = users[i]; - final flameCounter = getFlameCounterFromContact(user); + final group = groups[i]; return ListTile( title: Row( children: [ - if (isRealTwonly) - Padding( - padding: const EdgeInsets.only(right: 1), - child: VerifiedShield(user), - ), - Text(getContactDisplayName(user)), - if (flameCounter >= 1) - FlameCounterWidget( - user, - flameCounter, - prefix: true, - ), + Text(substringBy(group.groupName, 12)), + FlameCounterWidget( + groupId: group.groupId, + prefix: true, + ), ], ), - leading: ContactAvatar( - contact: user, + leading: AvatarIcon( + group: group, fontSize: 15, ), trailing: Checkbox( - value: selectedUserIds.contains(user.userId), + value: selectedGroupIds.contains(group.groupId), side: WidgetStateBorderSide.resolveWith( (Set states) { if (states.contains(WidgetState.selected)) { @@ -398,11 +348,14 @@ class UserList extends StatelessWidget { ), onChanged: (bool? value) { if (value == null) return; - updateStatus(user.userId, value); + updateSelectedGroupIds(group.groupId, value); }, ), onTap: () { - updateStatus(user.userId, !selectedUserIds.contains(user.userId)); + updateSelectedGroupIds( + group.groupId, + !selectedGroupIds.contains(group.groupId), + ); }, ); }, diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index d6398b4..2b5acf8 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -1,24 +1,19 @@ import 'dart:async'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.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_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/headline.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; class AddNewUserView extends StatefulWidget { const AddNewUserView({super.key}); @@ -52,8 +47,7 @@ class _SearchUsernameView extends State { } Future _addNewUser(BuildContext context) async { - final user = await getUser(); - if (user == null || user.username == searchUserName.text || !mounted) { + if (gUser.username == searchUserName.text) { return; } @@ -87,29 +81,30 @@ class _SearchUsernameView extends State { return; } - final added = await twonlyDB.contactsDao.insertContact( + final added = await twonlyDB.contactsDao.insertOnConflictUpdate( ContactsCompanion( username: Value(searchUserName.text), userId: Value(userdata.userId.toInt()), requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), ), ); if (added > 0) { if (await createNewSignalSession(userdata)) { - // before notifying the other party, add + // 1. Setup notifications keys with the other user await setupNotificationWithUsers( forceContact: userdata.userId.toInt(), ); - await encryptAndSendMessageAsync( - null, + // 2. Then send user request + await sendCipherText( userdata.userId.toInt(), - MessageJson( - kind: MessageKind.contactRequest, - timestamp: DateTime.now(), - content: MessageContent(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), ), - pushNotification: PushNotification(kind: PushKind.contactRequest), ); } } @@ -196,9 +191,9 @@ class ContactsListView extends StatelessWidget { Tooltip( message: context.lang.searchUserNameArchiveUserTooltip, child: IconButton( - icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 15), + icon: const FaIcon(Icons.archive_outlined, size: 15), onPressed: () async { - const update = ContactsCompanion(archived: Value(true)); + const update = ContactsCompanion(requested: Value(false)); await twonlyDB.contactsDao.updateContact(contact.userId, update); }, ), @@ -227,25 +222,48 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () async { - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REJECT, + ), + ), + ); + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); }, ), ), IconButton( icon: const Icon(Icons.check, color: Colors.green), onPressed: () async { - const update = ContactsCompanion(accepted: Value(true)); - await twonlyDB.contactsDao.updateContact(contact.userId, update); - await encryptAndSendMessageAsync( - null, + await twonlyDB.contactsDao.updateContact( contact.userId, - MessageJson( - kind: MessageKind.acceptRequest, - timestamp: DateTime.now(), - content: MessageContent(), + const ContactsCompanion( + accepted: Value(true), + requested: Value(false), + ), + ); + await twonlyDB.groupsDao.createNewDirectChat( + contact.userId, + GroupsCompanion( + groupName: Value(getContactDisplayName(contact)), + ), + ); + await sendCipherText( + contact.userId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.ACCEPT, + ), ), - pushNotification: PushNotification(kind: PushKind.acceptRequest), ); await notifyContactsAboutProfileChange(); }, @@ -259,10 +277,9 @@ class ContactsListView extends StatelessWidget { itemCount: contacts.length, itemBuilder: (context, index) { final contact = contacts[index]; - final displayName = getContactDisplayName(contact); return ListTile( - title: Text(displayName), - leading: ContactAvatar(contact: contact), + title: Text(substringBy(contact.username, 25)), + leading: AvatarIcon(contactId: contact.userId), trailing: Row( mainAxisSize: MainAxisSize.min, children: contact.requested diff --git a/lib/src/views/chats/archived_chats.view.dart b/lib/src/views/chats/archived_chats.view.dart new file mode 100644 index 0000000..0f0847a --- /dev/null +++ b/lib/src/views/chats/archived_chats.view.dart @@ -0,0 +1,55 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; + +class ArchivedChatsView extends StatefulWidget { + const ArchivedChatsView({super.key}); + + @override + State createState() => _ArchivedChatsViewState(); +} + +class _ArchivedChatsViewState extends State { + List _groupsArchived = []; + late StreamSubscription> _contactsSub; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + final stream = twonlyDB.groupsDao.watchGroupsForChatList(); + _contactsSub = stream.listen((groups) { + setState(() { + _groupsArchived = groups.where((x) => x.archived).toList(); + }); + }); + } + + @override + void dispose() { + _contactsSub.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.archivedChats), + ), + body: ListView( + children: _groupsArchived.map((group) { + return GroupListItem( + group: group, + ); + }).toList(), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index fb2157d..c1956c9 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -7,28 +6,19 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.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_database.dart'; -import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; -import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart'; +import 'package:twonly/src/views/chats/archived_chats.view.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; -import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; -import 'package:twonly/src/views/chats/media_viewer.view.dart'; +import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/chats/start_new_chat.view.dart'; -import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/notification_badge.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart'; @@ -42,12 +32,11 @@ class ChatListView extends StatefulWidget { } class _ChatListViewState extends State { - late StreamSubscription> _contactsSub; - List _contacts = []; - List _pinnedContacts = []; - UserData? _user; + late StreamSubscription> _contactsSub; + List _groupsNotPinned = []; + List _groupsPinned = []; + List _groupsArchived = []; - GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey(); Timer? tutorial; bool showFeedbackShortcut = false; @@ -59,11 +48,13 @@ class _ChatListViewState extends State { } Future initAsync() async { - final stream = twonlyDB.contactsDao.watchContactsForChatList(); - _contactsSub = stream.listen((contacts) { + final stream = twonlyDB.groupsDao.watchGroupsForChatList(); + _contactsSub = stream.listen((groups) { setState(() { - _contacts = contacts.where((x) => !x.pinned).toList(); - _pinnedContacts = contacts.where((x) => x.pinned).toList(); + _groupsNotPinned = + groups.where((x) => !x.pinned && !x.archived).toList(); + _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); + _groupsArchived = groups.where((x) => x.archived).toList(); }); }); @@ -72,21 +63,16 @@ class _ChatListViewState extends State { if (!mounted) return; await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); if (!mounted) return; - if (_contacts.isNotEmpty) { - await showChatListTutorialContextMenu(context, firstUserListItemKey); - } + // if (_groupsNotPinned.isNotEmpty) { + // await showChatListTutorialContextMenu(context, firstUserListItemKey); + // } }); - final user = await getUser(); - if (user == null) return; - setState(() { - _user = user; - }); final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLogHash = (await compute(Sha256().hash, changeLog.codeUnits)).bytes; - if (!user.hideChangeLog && - user.lastChangeLogHash.toString() != changeLogHash.toString()) { + if (!gUser.hideChangeLog && + gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { await updateUserdata((u) { u.lastChangeLogHash = changeLogHash; return u; @@ -94,7 +80,7 @@ class _ChatListViewState extends State { if (!mounted) return; // only show changelog to people who already have contacts // this prevents that this is shown directly after the user registered - if (_contacts.isNotEmpty) { + if (_groupsNotPinned.isNotEmpty) { await Navigator.push( context, MaterialPageRoute( @@ -119,7 +105,7 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { final isConnected = context.watch().isConnected; - final planId = context.watch().plan; + final plan = context.watch().plan; return Scaffold( appBar: AppBar( title: Row( @@ -134,19 +120,18 @@ class _ChatListViewState extends State { }, ), ); - _user = await getUser(); if (!mounted) return; - setState(() {}); + setState(() {}); // gUser has updated }, - child: ContactAvatar( - userData: _user, + child: AvatarIcon( + myAvatar: true, fontSize: 14, color: context.color.onSurface.withAlpha(20), ), ), const SizedBox(width: 10), const Text('twonly '), - if (planId != 'Free') + if (plan != SubscriptionPlan.Free) GestureDetector( onTap: () { Navigator.push( @@ -166,7 +151,7 @@ class _ChatListViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: Text( - planId, + plan.name, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, @@ -211,9 +196,8 @@ class _ChatListViewState extends State { builder: (context) => const SettingsMainView(), ), ); - _user = await getUser(); if (!mounted) return; - setState(() {}); + setState(() {}); // gUser may has changed... }, icon: const FaIcon(FontAwesomeIcons.gear, size: 19), ), @@ -228,74 +212,92 @@ class _ChatListViewState extends State { child: isConnected ? Container() : const ConnectionInfo(), ), Positioned.fill( - child: (_contacts.isEmpty && _pinnedContacts.isEmpty) - ? Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - label: Text(context.lang.chatListViewSearchUserNameBtn), + child: RefreshIndicator( + onRefresh: () async { + await apiService.close(() {}); + await apiService.connect(force: true); + await Future.delayed(const Duration(seconds: 1)); + }, + child: (_groupsNotPinned.isEmpty && + _groupsPinned.isEmpty && + _groupsArchived.isEmpty) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: const Icon(Icons.person_add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + label: Text( + context.lang.chatListViewSearchUserNameBtn, + ), + ), ), - ), - ) - : RefreshIndicator( - onRefresh: () async { - await apiService.close(() {}); - await apiService.connect(force: true); - await Future.delayed(const Duration(seconds: 1)); - }, - child: ListView.builder( - itemCount: _pinnedContacts.length + - (_pinnedContacts.isNotEmpty ? 1 : 0) + - _contacts.length + - 1, + ) + : ListView.builder( + itemCount: _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0) + + _groupsNotPinned.length + + (_groupsArchived.isNotEmpty ? 1 : 0), itemBuilder: (context, index) { - if (index == 0) { - return const BackupNoticeCard(); + if (index >= + _groupsNotPinned.length + + _groupsPinned.length + + (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (_groupsArchived.isEmpty) return Container(); + return ListTile( + title: Text( + '${context.lang.archivedChats} (${_groupsArchived.length})', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ArchivedChatsView(); + }, + ), + ); + }, + ); } - index -= 1; // Check if the index is for the pinned users - if (index < _pinnedContacts.length) { - final contact = _pinnedContacts[index]; - return UserListItem( - key: ValueKey(contact.userId), - user: contact, - firstUserListItemKey: (index == 0 || index == 1) - ? firstUserListItemKey - : null, + if (index < _groupsPinned.length) { + final group = _groupsPinned[index]; + return GroupListItem( + key: ValueKey(group.groupId), + group: group, ); } // If there are pinned users, account for the Divider - var adjustedIndex = index - _pinnedContacts.length; - if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) { + var adjustedIndex = index - _groupsPinned.length; + if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { return const Divider(); } // Adjust the index for the contacts list - adjustedIndex -= (_pinnedContacts.isNotEmpty ? 1 : 0); + adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0); // Get the contacts that are not pinned - final contact = _contacts.elementAt( + final group = _groupsNotPinned.elementAt( adjustedIndex, ); - return UserListItem( - key: ValueKey(contact.userId), - user: contact, - firstUserListItemKey: - (index == 0) ? firstUserListItemKey : null, + return GroupListItem( + key: ValueKey(group.groupId), + group: group, ); }, ), - ), + ), ), ], ), @@ -318,219 +320,3 @@ class _ChatListViewState extends State { ); } } - -class UserListItem extends StatefulWidget { - const UserListItem({ - required this.user, - required this.firstUserListItemKey, - super.key, - }); - final Contact user; - final GlobalKey? firstUserListItemKey; - - @override - State createState() => _UserListItem(); -} - -class _UserListItem extends State { - MessageSendState state = MessageSendState.send; - Message? currentMessage; - - List messagesNotOpened = []; - late StreamSubscription> messagesNotOpenedStream; - - List lastMessages = []; - late StreamSubscription> lastMessageStream; - - List previewMessages = []; - bool hasNonOpenedMediaFile = false; - - @override - void initState() { - super.initState(); - initStreams(); - } - - @override - void dispose() { - messagesNotOpenedStream.cancel(); - lastMessageStream.cancel(); - super.dispose(); - } - - void initStreams() { - lastMessageStream = twonlyDB.messagesDao - .watchLastMessage(widget.user.userId) - .listen((update) { - updateState(update, messagesNotOpened); - }); - - messagesNotOpenedStream = twonlyDB.messagesDao - .watchMessageNotOpened(widget.user.userId) - .listen((update) { - updateState(lastMessages, update); - }); - } - - void updateState( - List newLastMessages, - List newMessagesNotOpened, - ) { - if (newLastMessages.isEmpty) { - // there are no messages at all - currentMessage = null; - previewMessages = []; - } else if (newMessagesNotOpened.isEmpty) { - // there are no not opened messages show just the last message in the table - currentMessage = newLastMessages.last; - previewMessages = newLastMessages; - } else { - // filter first for received messages - final receivedMessages = - newMessagesNotOpened.where((x) => x.messageOtherId != null).toList(); - - if (receivedMessages.isNotEmpty) { - previewMessages = receivedMessages; - currentMessage = receivedMessages.first; - } else { - previewMessages = newMessagesNotOpened; - currentMessage = newMessagesNotOpened.first; - } - } - - final msgs = - previewMessages.where((x) => x.kind == MessageKind.media).toList(); - if (msgs.isNotEmpty && - msgs.first.kind == MessageKind.media && - msgs.first.messageOtherId != null && - msgs.first.openedAt == null) { - hasNonOpenedMediaFile = true; - } else { - hasNonOpenedMediaFile = false; - } - - lastMessages = newLastMessages; - messagesNotOpened = newMessagesNotOpened; - setState(() { - // sets lastMessages, messagesNotOpened and currentMessage - }); - } - - Future onTap() async { - if (currentMessage == null) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.user); - }, - ), - ); - return; - } - - if (hasNonOpenedMediaFile) { - final msgs = - previewMessages.where((x) => x.kind == MessageKind.media).toList(); - switch (msgs.first.downloadState) { - case DownloadState.pending: - await startDownloadMedia(msgs.first, true); - return; - case DownloadState.downloaded: - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return MediaViewerView(widget.user); - }, - ), - ); - return; - case DownloadState.downloading: - return; - } - } - if (!mounted) return; - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(widget.user); - }, - ), - ); - } - - @override - Widget build(BuildContext context) { - final flameCounter = getFlameCounterFromContact(widget.user); - - return Stack( - children: [ - Positioned( - top: 0, - bottom: 0, - left: 50, - child: SizedBox( - key: widget.firstUserListItemKey, - height: 20, - width: 20, - ), - ), - UserContextMenu( - contact: widget.user, - child: ListTile( - title: Text( - getContactDisplayName(widget.user), - ), - subtitle: (widget.user.deleted) - ? Text(context.lang.userDeletedAccount) - : (currentMessage == null) - ? Text(context.lang.chatsTapToSend) - : Row( - children: [ - MessageSendStateIcon(previewMessages), - const Text('•'), - const SizedBox(width: 5), - if (currentMessage != null) - LastMessageTime(message: currentMessage!), - if (flameCounter > 0) - FlameCounterWidget( - widget.user, - flameCounter, - prefix: true, - ), - ], - ), - leading: ContactAvatar(contact: widget.user), - trailing: (widget.user.deleted) - ? null - : IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - if (hasNonOpenedMediaFile) { - return ChatMessagesView(widget.user); - } else { - return CameraSendToView(widget.user); - } - }, - ), - ); - }, - icon: FaIcon( - hasNonOpenedMediaFile - ? FontAwesomeIcons.solidComments - : FontAwesomeIcons.camera, - color: context.color.outline.withAlpha(150), - ), - ), - onTap: onTap, - ), - ), - ], - ); - } -} diff --git a/lib/src/views/chats/chat_list_components/backup_notice.card.dart b/lib/src/views/chats/chat_list_components/backup_notice.card.dart deleted file mode 100644 index d5b9a36..0000000 --- a/lib/src/views/chats/chat_list_components/backup_notice.card.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/settings/backup/backup.view.dart'; - -class BackupNoticeCard extends StatefulWidget { - const BackupNoticeCard({super.key}); - - @override - State createState() => _BackupNoticeCardState(); -} - -class _BackupNoticeCardState extends State { - bool showBackupNotice = false; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - final user = await getUser(); - showBackupNotice = false; - if (user != null && - (user.nextTimeToShowBackupNotice == null || - DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) { - if (user.twonlySafeBackup == null) { - showBackupNotice = true; - } - } - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - if (!showBackupNotice) return Container(); - - return Card( - elevation: 4, - margin: const EdgeInsets.all(10), - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.lang.backupNoticeTitle, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 5), - Text( - context.lang.backupNoticeDesc, - style: const TextStyle(fontSize: 14), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () async { - await updateUserdata((user) { - user.nextTimeToShowBackupNotice = - DateTime.now().add(const Duration(days: 7)); - return user; - }); - await initAsync(); - }, - child: Text(context.lang.backupNoticeLater), - ), - const SizedBox(width: 10), - FilledButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BackupView(), - ), - ); - }, - child: Text(context.lang.backupNoticeOpenBackup), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart new file mode 100644 index 0000000..e70ebf8 --- /dev/null +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -0,0 +1,297 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:mutex/mutex.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_send_to_view.dart'; +import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; +import 'package:twonly/src/views/chats/media_viewer.view.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/group_context_menu.component.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; + +class GroupListItem extends StatefulWidget { + const GroupListItem({ + required this.group, + super.key, + }); + final Group group; + + @override + State createState() => _UserListItem(); +} + +class _UserListItem extends State { + Message? _currentMessage; + + List _messagesNotOpened = []; + late StreamSubscription> _messagesNotOpenedStream; + + Message? _lastMessage; + Reaction? _lastReaction; + late StreamSubscription _lastMessageStream; + late StreamSubscription _lastReactionStream; + late StreamSubscription> _lastMediaFilesStream; + + List _previewMessages = []; + final List _previewMediaFiles = []; + bool _hasNonOpenedMediaFile = false; + + @override + void initState() { + super.initState(); + initStreams(); + } + + @override + void dispose() { + _messagesNotOpenedStream.cancel(); + _lastReactionStream.cancel(); + _lastMessageStream.cancel(); + _lastMediaFilesStream.cancel(); + super.dispose(); + } + + void initStreams() { + _lastMessageStream = twonlyDB.messagesDao + .watchLastMessage(widget.group.groupId) + .listen((update) { + protectUpdateState.protect(() async { + await updateState(update, _messagesNotOpened); + }); + }); + + _lastReactionStream = twonlyDB.reactionsDao + .watchLastReactions(widget.group.groupId) + .listen((update) { + setState(() { + _lastReaction = update; + }); + // protectUpdateState.protect(() async { + // await updateState(lastMessage, update, messagesNotOpened); + // }); + }); + + _messagesNotOpenedStream = twonlyDB.messagesDao + .watchMessageNotOpened(widget.group.groupId) + .listen((update) { + protectUpdateState.protect(() async { + await updateState(_lastMessage, update); + }); + }); + + _lastMediaFilesStream = + twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { + for (final mediaFile in mediaFiles) { + final index = _previewMediaFiles + .indexWhere((t) => t.mediaId == mediaFile.mediaId); + if (index >= 0) { + _previewMediaFiles[index] = mediaFile; + } + } + setState(() {}); + }); + } + + Mutex protectUpdateState = Mutex(); + + Future updateState( + Message? newLastMessage, + List newMessagesNotOpened, + ) async { + if (newLastMessage == null) { + // there are no messages at all + _currentMessage = null; + _previewMessages = []; + } else if (newMessagesNotOpened.isNotEmpty) { + // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. + final receivedMessages = + newMessagesNotOpened.where((x) => x.senderId != null).toList(); + + if (receivedMessages.isNotEmpty) { + _previewMessages = receivedMessages; + _currentMessage = receivedMessages.first; + } else { + _previewMessages = newMessagesNotOpened; + _currentMessage = newMessagesNotOpened.first; + } + } else { + // there are no not opened messages show just the last message in the table + _currentMessage = newLastMessage; + _previewMessages = [newLastMessage]; + } + + final msgs = + _previewMessages.where((x) => x.type == MessageType.media).toList(); + if (msgs.isNotEmpty && + msgs.first.type == MessageType.media && + msgs.first.senderId != null && + msgs.first.openedAt == null) { + _hasNonOpenedMediaFile = true; + } else { + _hasNonOpenedMediaFile = false; + } + + for (final message in _previewMessages) { + if (message.mediaId != null && + !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); + if (mediaFile != null) { + _previewMediaFiles.add(mediaFile); + } + } + } + + _lastMessage = newLastMessage; + _messagesNotOpened = newMessagesNotOpened; + if (mounted) setState(() {}); + } + + Future onTap() async { + if (_currentMessage == null) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + return; + } + + if (_hasNonOpenedMediaFile) { + final msgs = + _previewMessages.where((x) => x.type == MessageType.media).toList(); + final mediaFile = + await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); + if (mediaFile?.type != MediaType.audio) { + if (mediaFile?.downloadState == null) return; + if (mediaFile!.downloadState! == DownloadState.pending) { + await startDownloadMedia(mediaFile, true); + return; + } + if (mediaFile.downloadState! == DownloadState.ready) { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MediaViewerView(widget.group); + }, + ), + ); + return; + } + } + } + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(widget.group); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return GroupContextMenu( + group: widget.group, + child: ListTile( + title: Text( + substringBy(widget.group.groupName, 30), + ), + subtitle: (_currentMessage == null) + ? (widget.group.totalMediaCounter == 0) + ? Text(context.lang.chatsTapToSend) + : Row( + children: [ + LastMessageTime( + dateTime: widget.group.lastMessageExchange), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ) + : Row( + children: [ + MessageSendStateIcon( + _previewMessages, + _previewMediaFiles, + lastReaction: _lastReaction, + ), + const Text('•'), + const SizedBox(width: 5), + if (_currentMessage != null) + LastMessageTime(message: _currentMessage), + FlameCounterWidget( + groupId: widget.group.groupId, + prefix: true, + ), + ], + ), + leading: GestureDetector( + onTap: () async { + Widget pushWidget = GroupView(widget.group); + + if (widget.group.isDirectChat) { + final contacts = await twonlyDB.groupsDao + .getGroupContact(widget.group.groupId); + pushWidget = ContactView(contacts.first.userId); + } + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return pushWidget; + }, + ), + ); + }, + child: AvatarIcon(group: widget.group), + ), + trailing: (widget.group.leftGroup) + ? null + : IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + if (_hasNonOpenedMediaFile) { + return ChatMessagesView(widget.group); + } else { + return CameraSendToView(widget.group); + } + }, + ), + ); + }, + icon: FaIcon( + _hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, + color: context.color.outline.withAlpha(150), + ), + ), + onTap: onTap, + ), + ); + } +} diff --git a/lib/src/views/chats/chat_list_components/last_message_time.dart b/lib/src/views/chats/chat_list_components/last_message_time.dart index b1f229f..6b82d30 100644 --- a/lib/src/views/chats/chat_list_components/last_message_time.dart +++ b/lib/src/views/chats/chat_list_components/last_message_time.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; class LastMessageTime extends StatefulWidget { - const LastMessageTime({required this.message, super.key}); + const LastMessageTime({this.message, this.dateTime, super.key}); - final Message message; + final Message? message; + final DateTime? dateTime; @override State createState() => _LastMessageTimeState(); @@ -21,11 +23,19 @@ class _LastMessageTimeState extends State { void initState() { super.initState(); // Change the color every 200 milliseconds - updateTime = Timer.periodic(const Duration(milliseconds: 500), (timer) { - setState(() { + updateTime = + Timer.periodic(const Duration(milliseconds: 500), (timer) async { + if (widget.message != null) { + final lastAction = await twonlyDB.messagesDao + .getLastMessageAction(widget.message!.messageId); lastMessageInSeconds = DateTime.now() - .difference(widget.message.openedAt ?? widget.message.sendAt) + .difference(lastAction?.actionAt ?? widget.message!.createdAt) .inSeconds; + } else if (widget.dateTime != null) { + lastMessageInSeconds = + DateTime.now().difference(widget.dateTime!).inSeconds; + } + setState(() { if (lastMessageInSeconds < 0) { lastMessageInSeconds = 0; } @@ -42,7 +52,7 @@ class _LastMessageTimeState extends State { @override Widget build(BuildContext context) { return Text( - formatDuration(lastMessageInSeconds), + formatDuration(context, lastMessageInSeconds), style: const TextStyle(fontSize: 12), ); } diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index c00c514..4526e8e 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -1,85 +1,92 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; +import 'package:mutex/mutex.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:twonly/globals.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_database.dart'; -import 'package:twonly/src/model/json/message.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/model/memory_item.model.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; -import 'package:twonly/src/views/components/animate_icon.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/tutorial/tutorials.dart'; Color getMessageColor(Message message) { - return (message.messageOtherId == null) + return (message.senderId == null) ? const Color.fromARGB(255, 58, 136, 102) : const Color.fromARGB(233, 68, 137, 255); } -class ChatMessage { - ChatMessage({required this.message, required this.responseTo}); - final Message message; - final Message? responseTo; -} - class ChatItem { - const ChatItem._({this.message, this.date, this.time}); + const ChatItem._({ + this.message, + this.date, + this.lastOpenedPosition, + this.groupAction, + }); factory ChatItem.date(DateTime date) { return ChatItem._(date: date); } - factory ChatItem.time(DateTime time) { - return ChatItem._(time: time); - } - factory ChatItem.message(ChatMessage message) { + factory ChatItem.message(Message message) { return ChatItem._(message: message); } - final ChatMessage? message; + factory ChatItem.lastOpenedPosition(List contacts) { + return ChatItem._(lastOpenedPosition: contacts); + } + factory ChatItem.groupAction(GroupHistory groupAction) { + return ChatItem._(groupAction: groupAction); + } + final GroupHistory? groupAction; + final Message? message; final DateTime? date; - final DateTime? time; + final List? lastOpenedPosition; bool get isMessage => message != null; bool get isDate => date != null; - bool get isTime => time != null; + bool get isGroupAction => groupAction != null; + bool get isLastOpenedPosition => lastOpenedPosition != null; } /// Displays detailed information about a SampleItem. class ChatMessagesView extends StatefulWidget { - const ChatMessagesView(this.contact, {super.key}); + const ChatMessagesView(this.group, {super.key}); - final Contact contact; + final Group group; @override State createState() => _ChatMessagesViewState(); } class _ChatMessagesViewState extends State { - TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); - late Contact user; - String currentInputText = ''; - late StreamSubscription userSub; + late Group group; + late StreamSubscription userSub; late StreamSubscription> messageSub; + StreamSubscription>? groupActionsSub; + StreamSubscription>? contactSub; + StreamSubscription>>? + lastOpenedMessageByContactSub; + + Map userIdToContact = {}; + List messages = []; + List allMessages = []; + List<(Message, Contact)> lastOpenedMessageByContact = []; + List groupActions = []; List galleryItems = []; - Map> emojiReactionsToMessageId = {}; - Message? responseToMessage; + Message? quotesMessage; GlobalKey verifyShieldKey = GlobalKey(); late FocusNode textFieldFocus; Timer? tutorial; @@ -89,7 +96,7 @@ class _ChatMessagesViewState extends State { @override void initState() { super.initState(); - user = widget.contact; + group = widget.group; textFieldFocus = FocusNode(); initStreams(); @@ -104,160 +111,162 @@ class _ChatMessagesViewState extends State { void dispose() { userSub.cancel(); messageSub.cancel(); + contactSub?.cancel(); + groupActionsSub?.cancel(); + lastOpenedMessageByContactSub?.cancel(); tutorial?.cancel(); - textFieldFocus.dispose(); super.dispose(); } + Mutex protectMessageUpdating = Mutex(); + Future initStreams() async { - await twonlyDB.messagesDao.removeOldMessages(); - final contact = twonlyDB.contactsDao.watchContact(widget.contact.userId); - userSub = contact.listen((contact) { - if (contact == null) return; + final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId); + userSub = groupStream.listen((newGroup) { + if (newGroup == null) return; setState(() { - user = contact; + group = newGroup; }); }); - final msgStream = - twonlyDB.messagesDao.watchAllMessagesFrom(widget.contact.userId); - messageSub = msgStream.listen((newMessages) async { - // if (!context.mounted) return; - if (Platform.isAndroid) { - await flutterLocalNotificationsPlugin.cancel(widget.contact.userId); + if (!widget.group.isDirectChat) { + final lastOpenedStream = + twonlyDB.messagesDao.watchLastOpenedMessagePerContact(group.groupId); + lastOpenedMessageByContactSub = + lastOpenedStream.listen((lastActionsFuture) async { + final update = await lastActionsFuture; + lastOpenedMessageByContact = update; + await setMessages(allMessages, update, groupActions); + }); + + final actionsStream = twonlyDB.groupsDao.watchGroupActions(group.groupId); + groupActionsSub = actionsStream.listen((update) async { + groupActions = update; + await setMessages(allMessages, lastOpenedMessageByContact, update); + }); + + final contactsStream = twonlyDB.contactsDao.watchAllContacts(); + contactSub = contactsStream.listen((contacts) { + for (final contact in contacts) { + userIdToContact[contact.userId] = contact; + } + }); + } + + final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); + messageSub = msgStream.listen((update) async { + allMessages = update; + + /// In case a message is not open yet the message is updated, which will trigger this watch to be called again. + /// So as long as the Mutex is locked just return... + if (protectMessageUpdating.isLocked) { + // return; + } + await protectMessageUpdating.protect(() async { + await setMessages(update, lastOpenedMessageByContact, groupActions); + }); + }); + } + + Future setMessages( + List newMessages, + List<(Message, Contact)> lastOpenedMessageByContact, + List groupActions, + ) async { + await flutterLocalNotificationsPlugin.cancelAll(); + + final chatItems = []; + final storedMediaFiles = []; + + DateTime? lastDate; + + final openedMessages = >{}; + final lastOpenedMessageToContact = >{}; + + final myLastMessageIndex = + newMessages.lastIndexWhere((t) => t.senderId == null); + + for (final opened in lastOpenedMessageByContact) { + if (!lastOpenedMessageToContact.containsKey(opened.$1.messageId)) { + lastOpenedMessageToContact[opened.$1.messageId] = [opened.$2]; } else { - await flutterLocalNotificationsPlugin.cancelAll(); + lastOpenedMessageToContact[opened.$1.messageId]!.add(opened.$2); } - final chatItems = []; - final storedMediaFiles = []; - DateTime? lastDate; - final tmpEmojiReactionsToMessageId = >{}; + } + var index = 0; + var groupHistoryIndex = 0; - final openedMessageOtherIds = []; - - final messageOtherMessageIdToMyMessageId = {}; - final messageIdToMessage = {}; - - /// there is probably a better way... - for (final msg in newMessages) { - if (msg.messageOtherId != null) { - messageOtherMessageIdToMyMessageId[msg.messageOtherId!] = - msg.messageId; + for (final msg in newMessages) { + if (groupHistoryIndex < groupActions.length) { + for (; groupHistoryIndex < groupActions.length; groupHistoryIndex++) { + if (msg.createdAt.isAfter(groupActions[groupHistoryIndex].actionAt)) { + chatItems + .add(ChatItem.groupAction(groupActions[groupHistoryIndex])); + // groupHistoryIndex++; + } else { + break; + } } - messageIdToMessage[msg.messageId] = msg; + } + index += 1; + if (msg.type == MessageType.text && + msg.senderId != null && + msg.openedAt == null) { + if (openedMessages[msg.senderId!] == null) { + openedMessages[msg.senderId!] = []; + } + openedMessages[msg.senderId!]!.add(msg.messageId); } - for (final msg in newMessages) { - if (msg.kind == MessageKind.textMessage && - msg.messageOtherId != null && - msg.openedAt == null) { - openedMessageOtherIds.add(msg.messageOtherId!); - } + if (msg.type == MessageType.media && msg.mediaStored) { + storedMediaFiles.add(msg); + } - Message? responseTo; + if (lastDate == null || + msg.createdAt.day != lastDate.day || + msg.createdAt.month != lastDate.month || + msg.createdAt.year != lastDate.year) { + chatItems.add(ChatItem.date(msg.createdAt)); + lastDate = msg.createdAt; + } + chatItems.add(ChatItem.message(msg)); - if (msg.kind == MessageKind.media && msg.mediaStored) { - storedMediaFiles.add(msg); - } - - final responseId = msg.responseToMessageId ?? - messageOtherMessageIdToMyMessageId[msg.responseToOtherMessageId]; - - var isReaction = false; - if (responseId != null) { - responseTo = messageIdToMessage[responseId]; - final content = MessageContent.fromJson( - msg.kind, - jsonDecode(msg.contentJson!) as Map, - ); - if (content is TextMessageContent) { - if (isEmoji(content.text)) { - isReaction = true; - tmpEmojiReactionsToMessageId - .putIfAbsent(responseId, () => []) - .add(msg); - } - } - if (msg.kind == MessageKind.reopenedMedia) { - isReaction = true; - tmpEmojiReactionsToMessageId - .putIfAbsent(responseId, () => []) - .add(msg); - } - } - if (!isReaction) { - if (lastDate == null || - msg.sendAt.day != lastDate.day || - msg.sendAt.month != lastDate.month || - msg.sendAt.year != lastDate.year) { - chatItems.add(ChatItem.date(msg.sendAt)); - lastDate = msg.sendAt; - } else if (msg.sendAt.difference(lastDate).inMinutes >= 20) { - chatItems.add(ChatItem.time(msg.sendAt)); - lastDate = msg.sendAt; - } + if (index <= myLastMessageIndex || index == newMessages.length) { + if (lastOpenedMessageToContact.containsKey(msg.messageId)) { chatItems.add( - ChatItem.message( - ChatMessage( - message: msg, - responseTo: responseTo, - ), + ChatItem.lastOpenedPosition( + lastOpenedMessageToContact[msg.messageId]!, ), ); } } - - if (openedMessageOtherIds.isNotEmpty) { - await notifyContactAboutOpeningMessage( - widget.contact.userId, - openedMessageOtherIds, - ); + } + if (groupHistoryIndex < groupActions.length) { + for (var i = groupHistoryIndex; i < groupActions.length; i++) { + chatItems.add(ChatItem.groupAction(groupActions[i])); } + } - await twonlyDB.messagesDao - .openedAllNonMediaMessages(widget.contact.userId); + for (final contactId in openedMessages.keys) { + await notifyContactAboutOpeningMessage( + contactId, + openedMessages[contactId]!, + ); + } - setState(() { - emojiReactionsToMessageId = tmpEmojiReactionsToMessageId; - messages = chatItems.reversed.toList(); - }); - - final items = await MemoryItem.convertFromMessages(storedMediaFiles); - galleryItems = items.values.toList(); - setState(() {}); + if (!mounted) return; + setState(() { + messages = chatItems.reversed.toList(); }); - } - Future _sendMessage() async { - if (newMessageController.text == '') return; - - await sendTextMessage( - user.userId, - TextMessageContent( - text: newMessageController.text, - responseToMessageId: responseToMessage?.messageOtherId, - responseToOtherMessageId: responseToMessage?.messageId, - ), - PushNotification( - kind: (responseToMessage == null) - ? PushKind.text - : (isEmoji(newMessageController.text)) - ? PushKind.reaction - : PushKind.response, - reactionContent: (isEmoji(newMessageController.text)) - ? newMessageController.text - : null, - ), - ); - newMessageController.clear(); - currentInputText = ''; - responseToMessage = null; + final items = await MemoryItem.convertFromMessages(storedMediaFiles); + galleryItems = items.values.toList(); setState(() {}); } - Future scrollToMessage(int messageId) async { + Future scrollToMessage(String messageId) async { final index = messages.indexWhere( - (x) => x.isMessage && x.message!.message.messageId == messageId, + (x) => x.isMessage && x.message!.messageId == messageId, ); if (index == -1) return; setState(() { @@ -283,20 +292,35 @@ class _ChatMessagesViewState extends State { child: Scaffold( appBar: AppBar( title: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(widget.contact.userId); - }, - ), - ); + onTap: () async { + if (group.isDirectChat) { + final member = + await twonlyDB.groupsDao.getAllGroupMembers(group.groupId); + if (!context.mounted) return; + if (member.isEmpty) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(member.first.contactId); + }, + ), + ); + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return GroupView(group); + }, + ), + ); + } }, child: Row( children: [ - ContactAvatar( - contact: user, + AvatarIcon( + group: group, fontSize: 19, ), const SizedBox(width: 10), @@ -305,10 +329,13 @@ class _ChatMessagesViewState extends State { color: Colors.transparent, child: Row( children: [ - Text(getContactDisplayName(user)), + Text( + substringBy(group.groupName, 20), + ), const SizedBox(width: 10), - if (user.verified) - VerifiedShield(key: verifyShieldKey, user), + VerifiedShield(key: verifyShieldKey, group: group), + const SizedBox(width: 10), + FlameCounterWidget(groupId: group.groupId), ], ), ), @@ -317,181 +344,123 @@ class _ChatMessagesViewState extends State { ), ), ), - body: PieCanvas( - theme: getPieCanvasTheme(context), - child: SafeArea( - child: Column( - children: [ - Expanded( - child: ScrollablePositionedList.builder( - reverse: true, - itemCount: messages.length + 1, - itemScrollController: itemScrollController, - itemBuilder: (context, i) { - if (i == messages.length) { - return const Padding( - padding: EdgeInsetsGeometry.only(top: 10), - ); - } - if (messages[i].isDate || messages[i].isTime) { - return ChatDateChip( - item: messages[i], - ); - } else { - final chatMessage = messages[i].message!; - return Transform.translate( - offset: Offset( - (focusedScrollItem == i) - ? (chatMessage.message.messageOtherId == null) - ? -8 - : 8 - : 0, - 0, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ScrollablePositionedList.builder( + reverse: true, + itemCount: messages.length + 1, + itemScrollController: itemScrollController, + itemBuilder: (context, i) { + if (i == messages.length) { + return const Padding( + padding: EdgeInsetsGeometry.only(top: 10), + ); + } + if (messages[i].isDate) { + return ChatDateChip( + item: messages[i], + ); + } else if (messages[i].isLastOpenedPosition) { + return Wrap( + spacing: 8, + alignment: WrapAlignment.center, + children: messages[i].lastOpenedPosition!.map((w) { + return AvatarIcon( + contactId: w.userId, + fontSize: 12, + ); + }).toList(), + ); + } else if (messages[i].isGroupAction) { + return ChatGroupAction( + key: Key(messages[i].groupAction!.groupHistoryId), + action: messages[i].groupAction!, + ); + } else { + final chatMessage = messages[i].message!; + return Transform.translate( + offset: Offset( + (focusedScrollItem == i) + ? (chatMessage.senderId == null) + ? -8 + : 8 + : 0, + 0, + ), + child: Transform.scale( + scale: (focusedScrollItem == i) ? 1.05 : 1, + child: ChatListEntry( + key: Key(chatMessage.messageId), + message: messages[i].message!, + nextMessage: + (i > 0) ? messages[i - 1].message : null, + prevMessage: ((i + 1) < messages.length) + ? messages[i + 1].message + : null, + group: group, + galleryItems: galleryItems, + userIdToContact: userIdToContact, + scrollToMessage: scrollToMessage, + onResponseTriggered: () { + setState(() { + quotesMessage = chatMessage; + }); + textFieldFocus.requestFocus(); + }, ), - child: Transform.scale( - scale: (focusedScrollItem == i) ? 1.05 : 1, - child: ChatListEntry( - key: - Key(chatMessage.message.messageId.toString()), - chatMessage, - user, - galleryItems, - isLastMessageFromSameUser(messages, i), - emojiReactionsToMessageId[ - chatMessage.message.messageId] ?? - [], - scrollToMessage: scrollToMessage, - onResponseTriggered: () { - setState(() { - responseToMessage = chatMessage.message; - }); - textFieldFocus.requestFocus(); - }, - ), - ), - ); - } - }, - ), + ), + ); + } + }, ), - if (responseToMessage != null && !user.deleted) - Container( - padding: const EdgeInsets.only( - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: ResponsePreview( - message: responseToMessage!, - showBorder: true, - contact: user, - ), - ), - IconButton( - onPressed: () { - setState(() { - responseToMessage = null; - }); - }, - icon: const FaIcon( - FontAwesomeIcons.xmark, - size: 16, - ), - ), - ], - ), - ), - Padding( + ), + if (quotesMessage != null) + Container( padding: const EdgeInsets.only( - bottom: 30, left: 20, right: 20, top: 10, ), child: Row( - children: (user.deleted) - ? [] - : [ - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: inputTextMessageDeco(context), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.contact); - }, - ), - ); - }, - ), - ], + children: [ + Expanded( + child: ResponsePreview( + message: quotesMessage, + showBorder: true, + group: group, + ), + ), + IconButton( + onPressed: () { + setState(() { + quotesMessage = null; + }); + }, + icon: const FaIcon( + FontAwesomeIcons.xmark, + size: 16, + ), + ), + ], ), ), - ], - ), + if (!group.leftGroup) + MessageInput( + group: group, + quotesMessage: quotesMessage, + textFieldFocus: textFieldFocus, + onMessageSend: () { + setState(() { + quotesMessage = null; + }); + }, + ), + ], ), ), ), ); } } - -bool isLastMessageFromSameUser(List messages, int index) { - if (index <= 0) { - return true; // If there is no previous message, return true - } - - final lastMessage = messages[index - 1]; - final currentMessage = messages[index]; - - if (lastMessage.isMessage && currentMessage.isMessage) { - // Check if both messages have the same messageOtherId (or both are null) - return (lastMessage.message!.message.messageOtherId == null && - currentMessage.message!.message.messageOtherId == null) || - (lastMessage.message!.message.messageOtherId != null && - currentMessage.message!.message.messageOtherId != null); - } - return false; -} - -double calculateNumberOfLines(String text, double width, double fontSize) { - final textPainter = TextPainter( - text: TextSpan( - text: text, - style: TextStyle(fontSize: fontSize), - ), - textDirection: TextDirection.ltr, - )..layout(maxWidth: width - 32); - return textPainter.computeLineMetrics().length.toDouble(); -} diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart new file mode 100644 index 0000000..011c294 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; + +class AllReactionsView extends StatefulWidget { + const AllReactionsView({required this.message, super.key}); + + final Message message; + + @override + State createState() => _AllReactionsViewState(); +} + +class _AllReactionsViewState extends State { + StreamSubscription>? reactionsSub; + List<(Reaction, Contact?)> reactionsUsers = []; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + reactionsSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + final stream = twonlyDB.reactionsDao + .watchReactionWithContacts(widget.message.messageId); + + reactionsSub = stream.listen((update) { + setState(() { + reactionsUsers = update; + }); + }); + setState(() {}); + } + + Future removeReaction(String emoji) async { + await twonlyDB.reactionsDao.updateMyReaction( + widget.message.messageId, + emoji, + true, + ); + await sendCipherTextToGroup( + widget.message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: widget.message.messageId, + emoji: emoji, + remove: true, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 400, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + color: context.color.surface, + boxShadow: const [ + BoxShadow( + blurRadius: 10.9, + color: Color.fromRGBO(0, 0, 0, 0.1), + ), + ], + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: ListView( + children: reactionsUsers.map((entry) { + return GestureDetector( + onTap: (entry.$2 != null) + ? null + : () { + removeReaction(entry.$1.emoji); + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 30, + ), + color: Colors.transparent, + margin: const EdgeInsets.only(left: 4), + child: Row( + children: [ + AvatarIcon( + contactId: entry.$2?.userId, + myAvatar: entry.$2 == null, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (entry.$2 == null) + ? context.lang.you + : getContactDisplayName(entry.$2!), + style: const TextStyle(fontSize: 17), + ), + if (entry.$2 == null) + Text( + context.lang.tabToRemoveEmoji, + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), + if (EmojiAnimation.animatedIcons + .containsKey(entry.$1.emoji)) + SizedBox( + height: 25, + child: EmojiAnimation(emoji: entry.$1.emoji), + ) + else + SizedBox( + height: 24, + child: Center( + child: Text( + entry.$1.emoji, + style: const TextStyle(fontSize: 22), + strutStyle: const StrutStyle( + forceStrutHeight: true, + height: 1.6, + ), + ), + ), + ), + // Text( + // entry.$1.emoji, + // style: const TextStyle(fontSize: 25), + // ), + ], + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart new file mode 100644 index 0000000..7569305 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; + +class MessageHistoryView extends StatelessWidget { + const MessageHistoryView({ + required this.message, + required this.group, + required this.changes, + super.key, + }); + + final Message message; + final Group group; + final List changes; + + @override + Widget build(BuildContext context) { + final json = message.toJson(); + json['createdAt'] = message.modifiedAt; + final currentMessage = Message.fromJson(json); + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.zero, + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + color: context.color.surface, + boxShadow: const [ + BoxShadow( + blurRadius: 10.9, + color: Color.fromRGBO(0, 0, 0, 0.1), + ), + ], + ), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: ListView( + children: [ + ChatListEntry( + group: group, + message: currentMessage, + hideReactions: true, + ), + ...changes.map( + (change) { + final json = message.toJson(); + json['content'] = change.content; + json['createdAt'] = change.createdAt; + final msgChanged = Message.fromJson(json); + return ChatListEntry( + group: group, + message: msgChanged, + hideReactions: true, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart new file mode 100644 index 0000000..e711ded --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class ChatGroupAction extends StatefulWidget { + const ChatGroupAction({ + required this.action, + super.key, + }); + + final GroupHistory action; + + @override + State createState() => _ChatGroupActionState(); +} + +class _ChatGroupActionState extends State { + Contact? contact; + Contact? affectedContact; + + @override + void initState() { + initAsync(); + super.initState(); + } + + Future initAsync() async { + if (widget.action.contactId != null) { + contact = + await twonlyDB.contactsDao.getContactById(widget.action.contactId!); + } + + if (widget.action.affectedContactId != null) { + affectedContact = await twonlyDB.contactsDao + .getContactById(widget.action.affectedContactId!); + } + + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + var text = ''; + IconData? icon; + + final affected = (affectedContact == null) + ? context.lang.groupActionYou + : getContactDisplayName(affectedContact!); + final affectedR = + (affectedContact == null) ? context.lang.groupActionYour : affected; + final maker = (contact == null) ? '' : getContactDisplayName(contact!); + + switch (widget.action.type) { + case GroupActionType.changeDisplayMaxTime: + final time = formatDuration( + context, + (widget.action.newDeleteMessagesAfterMilliseconds ?? 0) ~/ 1000, + ); + text = (contact == null) + ? context.lang.youChangedDisplayMaxTime(time) + : context.lang.changeDisplayMaxTime(time, maker); + icon = FontAwesomeIcons.stopwatch20; + case GroupActionType.updatedGroupName: + text = (contact == null) + ? context.lang.youChangedGroupName(widget.action.newGroupName!) + : context.lang + .makerChangedGroupName(maker, widget.action.newGroupName!); + icon = FontAwesomeIcons.pencil; + case GroupActionType.createdGroup: + icon = FontAwesomeIcons.penToSquare; + text = (contact == null) + ? context.lang.youCreatedGroup + : context.lang.makerCreatedGroup(maker); + case GroupActionType.removedMember: + icon = FontAwesomeIcons.userMinus; + text = (contact == null) + ? context.lang.youRemovedMember(affected) + : context.lang.makerRemovedMember(affected, maker); + case GroupActionType.addMember: + icon = FontAwesomeIcons.userPlus; + text = (contact == null) + ? context.lang.youAddedMember(affected) + : context.lang.makerAddedMember(affected, maker); + case GroupActionType.promoteToAdmin: + icon = FontAwesomeIcons.key; + text = (contact == null) + ? context.lang.youMadeAdmin(affected) + : context.lang.makerMadeAdmin(affected, maker); + case GroupActionType.demoteToMember: + icon = FontAwesomeIcons.key; + text = (contact == null) + ? context.lang.youRevokedAdminRights(affected) + : context.lang.makerRevokedAdminRights(affectedR, maker); + case GroupActionType.leftGroup: + icon = FontAwesomeIcons.userMinus; + text = (contact == null) + ? context.lang.youLeftGroup + : context.lang.makerLeftGroup(maker); + } + + // switch (widget.action.type) { + // case GroupActionType.updatedGroupName: + // text = (contact == null) + // ? 'You have changed the group name to "${widget.action.newGroupName}".' + // : '$maker has changed the group name to "${widget.action.newGroupName}".'; + // icon = FontAwesomeIcons.pencil; + // case GroupActionType.createdGroup: + // icon = FontAwesomeIcons.penToSquare; + // text = (contact == null) + // ? 'You have created the group.' + // : '$maker has created the group.'; + // case GroupActionType.removedMember: + // icon = FontAwesomeIcons.userMinus; + // text = (contact == null) + // ? 'You have removed $affected from the group.' + // : '$maker has removed $affected from the group.'; + // case GroupActionType.addMember: + // icon = FontAwesomeIcons.userPlus; + // text = (contact == null) + // ? 'You have added $affected to the group.' + // : '$maker has added $affected to the group.'; + // case GroupActionType.promoteToAdmin: + // icon = FontAwesomeIcons.key; + // text = (contact == null) + // ? 'You made $affected an admin.' + // : '$maker made $affected an admin.'; + // case GroupActionType.demoteToMember: + // icon = FontAwesomeIcons.key; + // text = (contact == null) + // ? 'You revoked $affectedR admin rights.' + // : '$maker revoked $affectedR admin rights.'; + // case GroupActionType.leftGroup: + // icon = FontAwesomeIcons.userMinus; + // text = (contact == null) + // ? 'You have left the group.' + // : '$maker has left the group.'; + // } + + return Padding( + padding: const EdgeInsets.all(8), + child: Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: FaIcon( + icon, + size: 10, + color: Colors.grey, + ), + ), + const WidgetSpan(child: SizedBox(width: 8)), + TextSpan( + text: text, + style: const TextStyle(color: Colors.grey), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 4ba5c6b..496d97f 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -1,117 +1,285 @@ -import 'dart:convert'; +import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart' + hide MessageActions; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; class ChatListEntry extends StatefulWidget { - const ChatListEntry( - this.msg, - this.contact, - this.galleryItems, - this.lastMessageFromSameUser, - this.otherReactions, { - required this.onResponseTriggered, - required this.scrollToMessage, + const ChatListEntry({ + required this.group, + required this.message, + this.galleryItems = const [], + this.scrollToMessage, + this.onResponseTriggered, + this.prevMessage, + this.nextMessage, + this.userIdToContact, + this.hideReactions = false, super.key, }); - final ChatMessage msg; - final Contact contact; - final bool lastMessageFromSameUser; - final List otherReactions; + final Message? prevMessage; + final Message? nextMessage; + final Message message; + final Group group; + final Map? userIdToContact; + final bool hideReactions; final List galleryItems; - final void Function(int) scrollToMessage; - final void Function() onResponseTriggered; + final void Function(String)? scrollToMessage; + final void Function()? onResponseTriggered; @override State createState() => _ChatListEntryState(); } class _ChatListEntryState extends State { - MessageContent? content; - String? textMessage; + MediaFileService? mediaService; + + List reactions = []; + StreamSubscription>? reactionsSub; + + StreamSubscription? mediaFileSub; @override void initState() { + initAsync(); super.initState(); - final msgContent = MessageContent.fromJson( - widget.msg.message.kind, - jsonDecode(widget.msg.message.contentJson!) as Map, - ); - if (msgContent is TextMessageContent) { - textMessage = msgContent.text; + } + + @override + void dispose() { + mediaFileSub?.cancel(); + reactionsSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + if (widget.message.mediaId != null) { + final mediaFileStream = + twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!); + mediaFileSub = mediaFileStream.listen((mediaFiles) async { + if (mediaFiles != null) { + mediaService = await MediaFileService.fromMedia(mediaFiles); + if (mounted) setState(() {}); + } + }); } - content = msgContent; + final stream = + twonlyDB.reactionsDao.watchReactions(widget.message.messageId); + + reactionsSub = stream.listen((update) { + setState(() { + reactions = update; + }); + }); + setState(() {}); } @override Widget build(BuildContext context) { - if (content == null) return Container(); - final right = widget.msg.message.messageOtherId == null; + final right = widget.message.senderId == null; + + final (padding, borderRadius, hideContactAvatar) = getMessageLayout( + widget.message, + widget.prevMessage, + widget.nextMessage, + reactions.isNotEmpty, + ); + + final seen = {}; + var reactionsForWidth = + reactions.where((t) => seen.add(t.emoji)).toList().length; + if (reactionsForWidth > 4) reactionsForWidth = 4; + + Widget child = Stack( + // overflow: Overflow.visible, + // clipBehavior: Clip.none, + alignment: right ? Alignment.centerRight : Alignment.centerLeft, + children: [ + if (widget.message.isDeletedFromSender) + ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + userIdToContact: widget.userIdToContact, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + else + Column( + children: [ + ResponseContainer( + msg: widget.message, + group: widget.group, + mediaService: mediaService, + borderRadius: borderRadius, + scrollToMessage: widget.scrollToMessage, + child: (widget.message.type == MessageType.text) + ? ChatTextEntry( + message: widget.message, + nextMessage: widget.nextMessage, + prevMessage: widget.prevMessage, + userIdToContact: widget.userIdToContact, + borderRadius: borderRadius, + minWidth: reactionsForWidth * 43, + ) + : (mediaService == 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( + message: widget.message, + group: widget.group, + mediaService: mediaService!, + galleryItems: widget.galleryItems, + minWidth: reactionsForWidth * 43, + ), + ), + if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10), + ], + ), + if (!widget.message.isDeletedFromSender && !widget.hideReactions) + Positioned( + bottom: -20, + left: 5, + right: 5, + child: ReactionRow( + message: widget.message, + reactions: reactions, + ), + ), + ], + ); + + if (widget.onResponseTriggered != null) { + child = MessageActions( + message: widget.message, + onResponseTriggered: widget.onResponseTriggered!, + child: child, + ); + + child = MessageContextMenu( + message: widget.message, + group: widget.group, + onResponseTriggered: widget.onResponseTriggered!, + galleryItems: widget.galleryItems, + child: Container( + child: child, + ), + ); + } return Align( alignment: right ? Alignment.centerRight : Alignment.centerLeft, child: Padding( - padding: widget.lastMessageFromSameUser - ? const EdgeInsets.only(top: 5, right: 10, left: 10) - : const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), - child: MessageContextMenu( - message: widget.msg.message, - onResponseTriggered: widget.onResponseTriggered, - child: Column( - mainAxisAlignment: - right ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: - right ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - MessageActions( - message: widget.msg.message, - onResponseTriggered: widget.onResponseTriggered, - child: Stack( - alignment: - right ? Alignment.centerRight : Alignment.centerLeft, - children: [ - ResponseContainer( - msg: widget.msg, - contact: widget.contact, - scrollToMessage: widget.scrollToMessage, - child: (textMessage != null) - ? ChatTextEntry( - message: widget.msg, - text: textMessage!, - hasReaction: widget.otherReactions.isNotEmpty, - ) - : ChatMediaEntry( - message: widget.msg.message, - contact: widget.contact, - galleryItems: widget.galleryItems, - content: content!, - ), - ), - Positioned( - bottom: 5, - left: 5, - right: 5, - child: ReactionRow( - otherReactions: widget.otherReactions, - message: widget.msg.message, + padding: padding, + child: Row( + mainAxisAlignment: + right ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + if (!right && !widget.group.isDirectChat) + hideContactAvatar + ? const SizedBox(width: 24) + : GestureDetector( + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ContactView(widget.message.senderId!), + ), + ); + }, + child: AvatarIcon( + contactId: widget.message.senderId, + fontSize: 12, ), ), - ], - ), - ), - ], - ), + child, + ], ), ), ); } } + +(EdgeInsetsGeometry, BorderRadius, bool) getMessageLayout( + Message message, + Message? prevMessage, + Message? nextMessage, + bool hasReactions, +) { + var bottom = 10.0; + var top = 10.0; + var hideContactAvatar = false; + + var topLeft = 12.0; + var topRight = 12.0; + var bottomRight = 12.0; + var bottomLeft = 12.0; + + if (nextMessage != null) { + if (message.senderId == nextMessage.senderId) { + bottom = 3; + } + } + + if (prevMessage != null) { + final combinesWidthNext = combineTextMessageWithNext(prevMessage, message); + if (combinesWidthNext) { + top = 1; + topLeft = 5.0; + } + } + + final combinesWidthNext = combineTextMessageWithNext(message, nextMessage); + if (combinesWidthNext) { + bottom = 0; + bottomLeft = 5.0; + hideContactAvatar = true; + } + + if (message.senderId == null) { + final tmp = topLeft; + topLeft = topRight; + topRight = tmp; + + final tmp2 = bottomLeft; + bottomLeft = bottomRight; + bottomRight = tmp2; + hideContactAvatar = true; + } + + return ( + EdgeInsets.only(top: top, bottom: bottom, right: 10, left: 10), + BorderRadius.only( + topLeft: Radius.circular(topLeft), + topRight: Radius.circular(topRight), + bottomRight: Radius.circular(bottomRight), + bottomLeft: Radius.circular(bottomLeft), + ), + hideContactAvatar + ); +} diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart deleted file mode 100644 index 65208f0..0000000 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart' show Value; -import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; -import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; -import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart'; -import 'package:twonly/src/views/chats/media_viewer.view.dart'; -import 'package:twonly/src/views/tutorial/tutorials.dart'; - -class ChatMediaEntry extends StatefulWidget { - const ChatMediaEntry({ - required this.message, - required this.contact, - required this.content, - required this.galleryItems, - super.key, - }); - - final Message message; - final Contact contact; - final MessageContent content; - final List galleryItems; - - @override - State createState() => _ChatMediaEntryState(); -} - -class _ChatMediaEntryState extends State { - GlobalKey reopenMediaFile = GlobalKey(); - bool canBeReopened = false; - - @override - void initState() { - super.initState(); - unawaited(checkIfTutorialCanBeShown()); - } - - Future checkIfTutorialCanBeShown() async { - if (widget.message.openedAt == null && - widget.message.messageOtherId != null || - widget.message.mediaStored) { - return; - } - final content = getMediaContent(widget.message); - if (content == null || - content.isRealTwonly || - content.maxShowTime != gMediaShowInfinite) { - return; - } - if (await received.existsMediaFile(widget.message.messageId, 'png')) { - if (mounted) { - setState(() { - canBeReopened = true; - }); - } - Future.delayed(const Duration(seconds: 1), () async { - if (!mounted) return; - await showReopenMediaFilesTutorial(context, reopenMediaFile); - }); - } - } - - Future onDoubleTap() async { - if (widget.message.openedAt == null && - widget.message.messageOtherId != null || - widget.message.mediaStored) { - return; - } - if (await received.existsMediaFile(widget.message.messageId, 'png')) { - await encryptAndSendMessageAsync( - null, - widget.contact.userId, - MessageJson( - kind: MessageKind.reopenedMedia, - messageSenderId: widget.message.messageId, - content: ReopenedMediaFileContent( - messageId: widget.message.messageOtherId!, - ), - timestamp: DateTime.now(), - ), - pushNotification: PushNotification( - kind: PushKind.reopenedMedia, - ), - ); - await twonlyDB.messagesDao.updateMessageByMessageId( - widget.message.messageId, - const MessagesCompanion(openedAt: Value(null)), - ); - } - } - - Future onTap() async { - if (widget.message.downloadState == DownloadState.downloaded && - widget.message.openedAt == null) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return MediaViewerView( - widget.contact, - initialMessage: widget.message, - ); - }, - ), - ); - await checkIfTutorialCanBeShown(); - } else if (widget.message.downloadState == DownloadState.pending) { - await received.startDownloadMedia(widget.message, true); - } - } - - @override - Widget build(BuildContext context) { - final color = getMessageColorFromType( - widget.content, - context, - ); - - return GestureDetector( - key: reopenMediaFile, - onDoubleTap: onDoubleTap, - onTap: widget.message.kind == MessageKind.media ? onTap : null, - child: SizedBox( - width: 150, - height: widget.message.mediaStored ? 271 : null, - child: Align( - alignment: Alignment.centerRight, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: InChatMediaViewer( - message: widget.message, - contact: widget.contact, - color: color, - galleryItems: widget.galleryItems, - canBeReopened: canBeReopened, - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart index 113400d..1a46d10 100644 --- a/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart +++ b/lib/src/views/chats/chat_messages_components/chat_reaction_row.dart @@ -1,89 +1,135 @@ -import 'dart:convert'; +import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; -class ReactionRow extends StatefulWidget { +class ReactionRow extends StatelessWidget { const ReactionRow({ - required this.otherReactions, + required this.reactions, required this.message, super.key, }); - final List otherReactions; + final List reactions; final Message message; - @override - State createState() => _ReactionRowState(); -} + Future _showReactionMenu(BuildContext context) async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return AllReactionsView( + message: message, + ); + }, + ); + } -class _ReactionRowState extends State { @override Widget build(BuildContext context) { - final children = []; - var hasOneTextReaction = false; - var hasOneReopened = false; - for (final reaction in widget.otherReactions.reversed) { - final content = MessageContent.fromJson( - reaction.kind, - jsonDecode(reaction.contentJson!) as Map, - ); - - if (content is ReopenedMediaFileContent) { - if (hasOneReopened) continue; - hasOneReopened = true; - children.add( - Expanded( - child: Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.only(right: 3), - child: FaIcon( - FontAwesomeIcons.repeat, - size: 12, - color: isDarkMode(context) ? Colors.white : Colors.black, - ), + final emojis = {}; + for (final reaction in reactions) { + late Widget child; + if (EmojiAnimation.animatedIcons.containsKey(reaction.emoji)) { + child = SizedBox( + height: 18, + child: EmojiAnimation(emoji: reaction.emoji), + ); + } else { + child = SizedBox( + height: 18, + child: Center( + child: Text( + reaction.emoji, + style: TextStyle(fontSize: Platform.isIOS ? 18 : 15), + strutStyle: StrutStyle( + forceStrutHeight: true, + height: Platform.isIOS ? 1.6 : 1.3, ), ), ), ); } - // only show one reaction - if (hasOneTextReaction) continue; - - if (content is TextMessageContent) { - hasOneTextReaction = true; - if (!isEmoji(content.text)) continue; - late Widget child; - if (EmojiAnimation.animatedIcons.containsKey(content.text)) { - child = SizedBox( - height: 18, - child: EmojiAnimation(emoji: content.text), - ); - } else { - child = Text(content.text, style: const TextStyle(fontSize: 14)); - } - children.insert( - 0, - Padding( - padding: const EdgeInsets.only(left: 3), - child: child, - ), - ); + if (emojis.containsKey(reaction.emoji)) { + emojis[reaction.emoji] = + (emojis[reaction.emoji]!.$1, emojis[reaction.emoji]!.$2 + 1); + } else { + emojis[reaction.emoji] = (child, 1); } } - if (children.isEmpty) return Container(); + if (emojis.isEmpty) return Container(); - return Row( - mainAxisAlignment: widget.message.messageOtherId == null - ? MainAxisAlignment.end - : MainAxisAlignment.end, - children: children, + var emojisToShow = emojis.values.toList() + ..sort((a, b) => b.$2.compareTo(a.$2)); + + if (emojisToShow.length > 4) { + emojisToShow = emojisToShow.slice(0, 3).toList() + ..add( + ( + SizedBox( + height: 18, + child: Transform.translate( + offset: const Offset(0, -3), + child: const FaIcon(FontAwesomeIcons.ellipsis), + ), + ), + 1 + ), + ); + } + + return GestureDetector( + onTap: () => _showReactionMenu(context), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.only(bottom: 20, top: 5), + child: Row( + mainAxisAlignment: message.senderId == null + ? MainAxisAlignment.start + : MainAxisAlignment.end, + children: emojisToShow.map((entry) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), + margin: const EdgeInsets.only(left: 4), + decoration: BoxDecoration( + border: Border.all(), + borderRadius: BorderRadius.circular(12), + color: isDarkMode(context) + ? const Color.fromARGB(255, 74, 74, 74) + : const Color.fromARGB(255, 197, 197, 197), + ), + child: Row( + children: [ + entry.$1, + if (entry.$2 > 1) + SizedBox( + height: 19, + width: 13, + child: Text( + entry.$2.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: + isDarkMode(context) ? Colors.white : Colors.black, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), ); } } diff --git a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/chat_text_entry.dart deleted file mode 100644 index f161c52..0000000 --- a/lib/src/views/chats/chat_messages_components/chat_text_entry.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.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.text, - required this.hasReaction, - super.key, - }); - - final String text; - final ChatMessage message; - final bool hasReaction; - - @override - Widget build(BuildContext context) { - if (EmojiAnimation.supported(text)) { - return Container( - constraints: const BoxConstraints( - maxWidth: 100, - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 10, - ), - child: EmojiAnimation(emoji: text), - ); - } - return Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.8, - ), - padding: EdgeInsets.only( - left: 10, - top: 4, - bottom: 4, - right: hasReaction ? 30 : 10, - ), - decoration: BoxDecoration( - color: message.responseTo == null - ? getMessageColor(message.message) - : null, - borderRadius: BorderRadius.circular(12), - ), - child: BetterText(text: text), - ); - } -} diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart new file mode 100644 index 0000000..24267fb --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart @@ -0,0 +1,232 @@ +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatAudioEntry extends StatelessWidget { + const ChatAudioEntry({ + required this.message, + required this.nextMessage, + required this.mediaService, + required this.prevMessage, + required this.borderRadius, + required this.userIdToContact, + required this.minWidth, + super.key, + }); + + final Message message; + final MediaFileService mediaService; + final Message? nextMessage; + final Message? prevMessage; + final Map? userIdToContact; + final BorderRadius borderRadius; + final double minWidth; + + @override + Widget build(BuildContext context) { + if (!mediaService.tempPath.existsSync() && + !mediaService.originalPath.existsSync()) { + return Container(); // media file was purged + } + final info = getBubbleInfo( + context, + message, + nextMessage, + prevMessage, + userIdToContact, + minWidth, + ); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + minWidth: 250, + ), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: info.color, + borderRadius: borderRadius, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info.displayUserName != '') + Text( + info.displayUserName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (info.text != '') + Expanded( + child: BetterText(text: info.text, textColor: info.textColor), + ) + else ...[ + if (mediaService.mediaFile.downloadState == + DownloadState.ready || + mediaService.mediaFile.downloadState == null) + mediaService.tempPath.existsSync() + ? InChatAudioPlayer( + path: mediaService.tempPath.path, + message: message, + ) + : (mediaService.originalPath.existsSync()) + ? InChatAudioPlayer( + path: mediaService.originalPath.path, + message: message, + ) + : Container() + else + MessageSendStateIcon([message], [mediaService.mediaFile]), + ], + if (info.displayTime || message.modifiedAt != null) + FriendlyMessageTime(message: message), + ], + ), + ], + ), + ); + } +} + +class InChatAudioPlayer extends StatefulWidget { + const InChatAudioPlayer({ + required this.path, + required this.message, + super.key, + }); + + final String path; + final Message message; + + @override + State createState() => _InChatAudioPlayerState(); +} + +class _InChatAudioPlayerState extends State { + final PlayerController _playerController = PlayerController(); + int _displayDuration = 0; + int _maxDuration = 0; + + @override + void initState() { + super.initState(); + _playerController + ..preparePlayer(path: widget.path) + ..setFinishMode(finishMode: FinishMode.pause); + + _playerController.onCompletion.listen((_) { + if (mounted) { + setState(() { + _isPlaying = false; + _playerController.seekTo(0); + }); + } + }); + + _playerController.onCurrentDurationChanged.listen((duration) { + if (mounted) { + setState(() { + _displayDuration = _maxDuration - duration; + }); + } + }); + initAsync(); + } + + @override + void dispose() { + _playerController.dispose(); + super.dispose(); + } + + Future initAsync() async { + _displayDuration = await _playerController.getDuration(DurationType.max); + _maxDuration = _displayDuration; + if (!mounted) return; + setState(() {}); + } + + bool _isPlaying = false; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 4), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () { + if (_isPlaying) { + _playerController.pausePlayer(); + } else { + _playerController.startPlayer(); + if (widget.message.senderId != null && + widget.message.openedAt == null) { + notifyContactAboutOpeningMessage( + widget.message.senderId!, + [widget.message.messageId], + ); + } + } + setState(() { + _isPlaying = !_isPlaying; + }); + }, + child: Container( + padding: EdgeInsets.only( + left: _isPlaying ? 2 : 0, + top: 4, + bottom: 4, + ), + color: Colors.transparent, + child: FaIcon( + _isPlaying ? FontAwesomeIcons.pause : FontAwesomeIcons.play, + size: 20, + color: Colors.white, + ), + ), + ), + Text( + formatMsToMinSec(_displayDuration), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + AudioFileWaveforms( + playerController: _playerController, + size: const Size(150, 40), + ), + ], + ); + } +} + +String formatMsToMinSec(int milliseconds) { + final d = Duration(milliseconds: milliseconds); + final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; +} diff --git a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart b/lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart similarity index 74% rename from lib/src/views/chats/chat_messages_components/chat_date_chip.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart index bfaeaa4..4df7be7 100644 --- a/lib/src/views/chats/chat_messages_components/chat_date_chip.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_date_chip.dart @@ -9,10 +9,8 @@ class ChatDateChip extends StatelessWidget { @override Widget build(BuildContext context) { - final formattedDate = item.isTime - ? DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()) - .format(item.time!) - : '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}'; + final formattedDate = + '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}'; return Center( child: Container( @@ -23,6 +21,7 @@ class ChatDateChip extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + margin: const EdgeInsets.only(bottom: 20), child: Text( formattedDate, textAlign: TextAlign.center, diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart new file mode 100644 index 0000000..1f89085 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + hide Message; +import 'package:twonly/src/services/api/mediafiles/download.service.dart' + as received; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart'; +import 'package:twonly/src/views/chats/media_viewer.view.dart'; + +class ChatMediaEntry extends StatefulWidget { + const ChatMediaEntry({ + required this.message, + required this.group, + required this.galleryItems, + required this.mediaService, + required this.minWidth, + super.key, + }); + + final Message message; + final double minWidth; + final Group group; + final List galleryItems; + final MediaFileService mediaService; + + @override + State createState() => _ChatMediaEntryState(); +} + +class _ChatMediaEntryState extends State { + GlobalKey reopenMediaFile = GlobalKey(); + bool _canBeReopened = false; + + @override + void initState() { + super.initState(); + unawaited(initAsync()); + } + + Future initAsync() async { + if (widget.message.senderId == null || widget.message.mediaStored) { + return; + } + if (widget.mediaService.mediaFile.requiresAuthentication || + widget.mediaService.mediaFile.displayLimitInMilliseconds != null) { + return; + } + if (widget.mediaService.tempPath.existsSync()) { + if (mounted) { + setState(() { + _canBeReopened = true; + }); + } + } + } + + Future onDoubleTap() async { + if (widget.message.openedAt == null || widget.message.mediaStored) { + return; + } + if (widget.mediaService.tempPath.existsSync() && + widget.message.senderId != null) { + await sendCipherText( + widget.message.senderId!, + EncryptedContent( + mediaUpdate: EncryptedContent_MediaUpdate( + type: EncryptedContent_MediaUpdate_Type.REOPENED, + targetMessageId: widget.message.messageId, + ), + ), + ); + await twonlyDB.messagesDao.updateMessageId( + widget.message.messageId, + const MessagesCompanion(openedAt: Value(null)), + ); + } + } + + Future onTap() async { + if (widget.mediaService.mediaFile.downloadState == DownloadState.ready && + widget.message.openedAt == null) { + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MediaViewerView( + widget.group, + initialMessage: widget.message, + ); + }, + ), + ); + } else if (widget.mediaService.mediaFile.downloadState == + DownloadState.pending) { + await received.startDownloadMedia(widget.mediaService.mediaFile, true); + } + } + + @override + Widget build(BuildContext context) { + final color = getMessageColorFromType( + widget.message, + widget.mediaService.mediaFile, + context, + ); + + return GestureDetector( + key: reopenMediaFile, + onDoubleTap: onDoubleTap, + onTap: (widget.message.type == MessageType.media) ? onTap : null, + child: SizedBox( + width: (widget.minWidth > 150) ? widget.minWidth : 150, + height: (widget.message.mediaStored && + widget.mediaService.imagePreviewAvailable) + ? 271 + : null, + child: Align( + alignment: Alignment.centerRight, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: InChatMediaViewer( + message: widget.message, + group: widget.group, + mediaService: widget.mediaService, + color: color, + galleryItems: widget.galleryItems, + canBeReopened: _canBeReopened, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart new file mode 100644 index 0000000..0d9f365 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/better_text.dart'; + +class ChatTextEntry extends StatelessWidget { + const ChatTextEntry({ + required this.message, + required this.nextMessage, + required this.prevMessage, + required this.borderRadius, + required this.userIdToContact, + required this.minWidth, + super.key, + }); + + final Message message; + final Message? nextMessage; + final Message? prevMessage; + final Map? userIdToContact; + final BorderRadius borderRadius; + final double minWidth; + + @override + Widget build(BuildContext context) { + final text = message.content ?? ''; + + if (EmojiAnimation.supported(text)) { + return Container( + constraints: const BoxConstraints( + maxWidth: 100, + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 10, + ), + child: EmojiAnimation(emoji: text), + ); + } + + final info = getBubbleInfo( + context, + message, + nextMessage, + prevMessage, + userIdToContact, + minWidth, + ); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + minWidth: minWidth, + ), + padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), + decoration: BoxDecoration( + color: info.color, + borderRadius: borderRadius, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (info.displayUserName != '') + Text( + info.displayUserName, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (info.expanded) + Expanded( + child: BetterText(text: info.text, textColor: info.textColor), + ) + else ...[ + BetterText(text: info.text, textColor: info.textColor), + SizedBox( + width: info.spacerWidth, + ), + ], + if (info.displayTime || message.modifiedAt != null) + FriendlyMessageTime(message: message), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/entries/common.dart b/lib/src/views/chats/chat_messages_components/entries/common.dart new file mode 100644 index 0000000..da56997 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/common.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +class BubbleInfo { + late String text; + late Color textColor; + late bool displayTime; + late String displayUserName; + late Color color; + late bool expanded; + late double spacerWidth; +} + +BubbleInfo getBubbleInfo( + BuildContext context, + Message message, + Message? nextMessage, + Message? prevMessage, + Map? userIdToContact, + double minWidth, +) { + final info = BubbleInfo() + ..text = message.content ?? '' + ..textColor = Colors.white + ..color = getMessageColor(message) + ..displayTime = !combineTextMessageWithNext(message, nextMessage) + ..displayUserName = ''; + + if (message.senderId != null && + userIdToContact != null && + userIdToContact[message.senderId] != null) { + if (prevMessage == null) { + info.displayUserName = + getContactDisplayName(userIdToContact[message.senderId]!); + } else { + if (!combineTextMessageWithNext(prevMessage, message)) { + info.displayUserName = + getContactDisplayName(userIdToContact[message.senderId]!); + } + } + } + + info.spacerWidth = minWidth - measureTextWidth(info.text) - 53; + if (info.spacerWidth < 0) info.spacerWidth = 0; + + info.expanded = false; + if (message.quotesMessageId == null) { + info.color = getMessageColor(message); + } + if (message.isDeletedFromSender) { + info + ..color = context.color.surfaceBright + ..displayTime = false; + } else if (measureTextWidth(info.text) > 270) { + info.expanded = true; + } + + if (message.isDeletedFromSender) { + info + ..text = context.lang.messageWasDeleted + ..color = isDarkMode(context) ? Colors.black : Colors.grey; + if (isDarkMode(context)) { + info.textColor = const Color.fromARGB(255, 99, 99, 99); + } + } + return info; +} + +double measureTextWidth( + String text, +) { + final tp = TextPainter( + text: TextSpan(text: text, style: const TextStyle(fontSize: 17)), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return tp.size.width; +} + +bool combineTextMessageWithNext(Message message, Message? nextMessage) { + if (nextMessage != null && nextMessage.content != null) { + if (nextMessage.senderId == message.senderId) { + if (nextMessage.type == MessageType.text && + message.type == MessageType.text) { + if (!EmojiAnimation.supported(nextMessage.content!)) { + final diff = + nextMessage.createdAt.difference(message.createdAt).inMinutes; + if (diff <= 1) { + return true; + } + } + } + } + } + return false; +} diff --git a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart new file mode 100644 index 0000000..1d793e0 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class FriendlyMessageTime extends StatelessWidget { + const FriendlyMessageTime({required this.message, super.key}); + + final Message message; + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentGeometry.centerRight, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Row( + children: [ + if (message.modifiedAt != null) + Padding( + padding: const EdgeInsets.only(right: 5), + child: SizedBox( + height: 10, + child: FaIcon( + FontAwesomeIcons.pencil, + color: Colors.white.withAlpha(150), + size: 10, + ), + ), + ), + Text( + friendlyTime( + context, + (message.modifiedAt != null) + ? message.modifiedAt! + : message.createdAt, + ), + style: TextStyle( + fontSize: 10, + color: Colors.white.withAlpha(150), + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} + +String friendlyTime(BuildContext context, DateTime dt) { + final now = DateTime.now(); + final diff = now.difference(dt); + + if (diff.inMinutes >= 0 && diff.inMinutes < 60) { + final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes; + if (minutes <= 1) { + return context.lang.now; + } + return '$minutes ${context.lang.minutesShort}'; + } + + // Determine 24h vs 12h from system/local settings + final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; + + if (!use24Hour) { + // 12-hour format with locale-aware AM/PM + final format = DateFormat.jm(Localizations.localeOf(context).toString()); + return format.format(dt); + } else { + // 24-hour HH:mm, locale-aware + final format = DateFormat.Hm(Localizations.localeOf(context).toString()); + return format.format(dt); + } +} diff --git a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart index 26774e0..d3a01d7 100644 --- a/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart +++ b/lib/src/views/chats/chat_messages_components/in_chat_media_viewer.dart @@ -1,9 +1,9 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -11,7 +11,8 @@ import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; class InChatMediaViewer extends StatefulWidget { const InChatMediaViewer({ required this.message, - required this.contact, + required this.group, + required this.mediaService, required this.color, required this.galleryItems, required this.canBeReopened, @@ -19,7 +20,8 @@ class InChatMediaViewer extends StatefulWidget { }); final Message message; - final Contact contact; + final Group group; + final MediaFileService mediaService; final List galleryItems; final Color color; final bool canBeReopened; @@ -56,8 +58,7 @@ class _InChatMediaViewerState extends State { bool loadIndex() { if (widget.message.mediaStored) { final index = widget.galleryItems.indexWhere( - (x) => - x.id == (widget.message.mediaUploadId ?? widget.message.messageId), + (x) => x.mediaService.mediaFile.mediaId == (widget.message.mediaId), ); if (index != -1) { galleryItemIndex = index; @@ -83,7 +84,7 @@ class _InChatMediaViewerState extends State { if (widget.message.mediaStored) return; final stream = twonlyDB.messagesDao - .getMessageByMessageId(widget.message.messageId) + .getMessageById(widget.message.messageId) .watchSingleOrNull(); messageStream = stream.listen((updated) async { if (updated != null) { @@ -111,7 +112,8 @@ class _InChatMediaViewerState extends State { @override Widget build(BuildContext context) { - if (!widget.message.mediaStored) { + if (!widget.message.mediaStored || + !widget.mediaService.imagePreviewAvailable) { return Container( constraints: const BoxConstraints( minHeight: 39, @@ -129,6 +131,7 @@ class _InChatMediaViewerState extends State { ), child: MessageSendStateIcon( [widget.message], + [widget.mediaService.mediaFile], mainAxisAlignment: MainAxisAlignment.center, canBeReopened: widget.canBeReopened, ), diff --git a/lib/src/views/chats/chat_messages_components/message_actions.dart b/lib/src/views/chats/chat_messages_components/message_actions.dart index 7ee69ea..e691a92 100644 --- a/lib/src/views/chats/chat_messages_components/message_actions.dart +++ b/lib/src/views/chats/chat_messages_components/message_actions.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; class MessageActions extends StatefulWidget { const MessageActions({ diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 26fe88e..88903f8 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,112 +1,237 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' + as pb; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; class MessageContextMenu extends StatelessWidget { const MessageContextMenu({ required this.message, + required this.group, required this.child, required this.onResponseTriggered, + required this.galleryItems, super.key, }); + final Group group; final Widget child; final Message message; + final List galleryItems; final VoidCallback onResponseTriggered; @override Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ - PieAction( - tooltip: Text(context.lang.react), - onSelect: () async { - final layer = await showModalBottomSheet( - context: context, - backgroundColor: Colors.black, - builder: (BuildContext context) { - return const Emojis(); - }, - ) as EmojiLayerData?; - if (layer == null) return; + return ContextMenu( + items: [ + if (!message.isDeletedFromSender) + ContextMenuItem( + title: context.lang.react, + onTap: () async { + final layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return const EmojiPickerBottom(); + }, + ) as EmojiLayerData?; + if (layer == null) return; - await sendTextMessage( - message.contactId, - TextMessageContent( - text: layer.text, - responseToMessageId: message.messageOtherId, - responseToOtherMessageId: - (message.messageOtherId == null) ? message.messageId : null, - ), - (message.messageOtherId != null) - ? PushNotification( - kind: (message.kind == MessageKind.textMessage) - ? PushKind.reactionToText - : (getMediaContent(message)!.isVideo) - ? PushKind.reactionToVideo - : PushKind.reactionToImage, - reactionContent: layer.text, - ) - : null, - ); - }, - child: const FaIcon(FontAwesomeIcons.faceLaugh), - ), - PieAction( - tooltip: Text(context.lang.reply), - onSelect: onResponseTriggered, - child: const FaIcon(FontAwesomeIcons.reply), - ), - PieAction( - tooltip: Text(context.lang.copy), - onSelect: () async { - final text = getMessageText(message); - await Clipboard.setData(ClipboardData(text: text)); - await HapticFeedback.heavyImpact(); - }, - child: const FaIcon(FontAwesomeIcons.solidCopy), - ), - PieAction( - tooltip: Text(context.lang.delete), - onSelect: () async { + await twonlyDB.reactionsDao.updateMyReaction( + message.messageId, + layer.text, + false, + ); + + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + reaction: pb.EncryptedContent_Reaction( + targetMessageId: message.messageId, + emoji: layer.text, + remove: false, + ), + ), + ); + }, + icon: FontAwesomeIcons.faceLaugh, + ), + if (!message.isDeletedFromSender) + ContextMenuItem( + title: context.lang.reply, + onTap: () async { + onResponseTriggered(); + }, + icon: FontAwesomeIcons.reply, + ), + if (!message.isDeletedFromSender && + message.senderId == null && + message.type == MessageType.text) + ContextMenuItem( + title: context.lang.edit, + onTap: () async { + await editTextMessage(context, message); + }, + icon: FontAwesomeIcons.pencil, + ), + if (message.content != null) + ContextMenuItem( + title: context.lang.copy, + onTap: () async { + await Clipboard.setData(ClipboardData(text: message.content!)); + await HapticFeedback.heavyImpact(); + }, + icon: FontAwesomeIcons.solidCopy, + ), + ContextMenuItem( + title: context.lang.delete, + onTap: () async { final delete = await showAlertDialog( context, context.lang.deleteTitle, null, - customOk: context.lang.deleteOkBtn, + customOk: + (message.senderId == null && !message.isDeletedFromSender) + ? context.lang.deleteOkBtnForAll + : context.lang.deleteOkBtnForMe, ); if (delete) { - await twonlyDB.messagesDao - .deleteMessagesByMessageId(message.messageId); + if (message.senderId == null && !message.isDeletedFromSender) { + await twonlyDB.messagesDao.handleMessageDeletion( + null, + message.messageId, + DateTime.now(), + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.DELETE, + senderMessageId: message.messageId, + ), + ), + ); + } else { + await twonlyDB.messagesDao + .deleteMessagesById(message.messageId); + } } }, - child: const FaIcon(FontAwesomeIcons.trash), + icon: FontAwesomeIcons.trash, ), - // PieAction( - // tooltip: Text(context.lang.info), - // onSelect: () {}, - // child: const FaIcon(FontAwesomeIcons.circleInfo), - // ), + if (!message.isDeletedFromSender) + ContextMenuItem( + title: context.lang.info, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return MessageInfoView( + message: message, + group: group, + galleryItems: galleryItems, + ); + }, + ), + ); + }, + icon: FontAwesomeIcons.circleInfo, + ), ], child: child, ); } } + +Future editTextMessage(BuildContext context, Message message) async { + var newText = message.content; + final controller = TextEditingController(text: message.content); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: TextField( + controller: controller, + autofocus: true, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) => setState(() { + newText = value; + }), + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + ], + ), + ); + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.lang.cancel), + ), + TextButton( + onPressed: () async { + if (newText != null && + newText != message.content && + newText != '') { + final timestamp = DateTime.now(); + + await twonlyDB.messagesDao.handleTextEdit( + null, + message.messageId, + newText!, + timestamp, + ); + await sendCipherTextToGroup( + message.groupId, + pb.EncryptedContent( + messageUpdate: pb.EncryptedContent_MessageUpdate( + type: pb.EncryptedContent_MessageUpdate_Type.EDIT_TEXT, + senderMessageId: message.messageId, + text: newText, + timestamp: Int64( + timestamp.millisecondsSinceEpoch, + ), + ), + ), + ); + } + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + child: Text(context.lang.ok), + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart new file mode 100644 index 0000000..ec24342 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -0,0 +1,373 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_send_to_view.dart'; + +class MessageInput extends StatefulWidget { + const MessageInput({ + required this.group, + required this.quotesMessage, + required this.textFieldFocus, + required this.onMessageSend, + super.key, + }); + + final Group group; + final FocusNode textFieldFocus; + final Message? quotesMessage; + final VoidCallback onMessageSend; + + @override + State createState() => _MessageInputState(); +} + +enum RecordingState { none, recording, finished } + +class _MessageInputState extends State { + late final TextEditingController _textFieldController; + late final RecorderController recorderController; + final bool isApple = Platform.isIOS; + bool _emojiShowing = false; + RecordingState _recordingState = RecordingState.none; + + Future _sendMessage() async { + if (_textFieldController.text == '') return; + + await insertAndSendTextMessage( + widget.group.groupId, + _textFieldController.text, + widget.quotesMessage?.messageId, + ); + + _textFieldController.clear(); + widget.onMessageSend(); + setState(() {}); + } + + @override + void initState() { + _textFieldController = TextEditingController(); + widget.textFieldFocus.addListener(_handleTextFocusChange); + _initializeControllers(); + super.initState(); + } + + @override + void dispose() { + widget.textFieldFocus.removeListener(_handleTextFocusChange); + widget.textFieldFocus.dispose(); + super.dispose(); + } + + void _initializeControllers() { + recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 44100; + } + + void _handleTextFocusChange() { + if (widget.textFieldFocus.hasFocus) { + setState(() { + _emojiShowing = false; + }); + } + } + + Future _stopAudioRecording() async { + await HapticFeedback.heavyImpact(); + setState(() { + _recordingState = RecordingState.none; + }); + + final audioTmpPath = await recorderController.stop(); + + if (audioTmpPath == null) return; + + final mediaFileService = await initializeMediaUpload( + MediaType.audio, + null, + ); + + if (mediaFileService == null) return; + + File(audioTmpPath) + ..copySync(mediaFileService.originalPath.path) + ..deleteSync(); + + await insertMediaFileInMessagesTable( + mediaFileService, + [widget.group.groupId], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 10, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + if (_recordingState != RecordingState.recording) + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, + ), + ), + ), + ), + Expanded( + child: (_recordingState == RecordingState.recording) + ? AudioWaveforms( + enableGesture: true, + size: Size( + MediaQuery.of(context).size.width / 2, + 50, + ), + recorderController: recorderController, + waveStyle: WaveStyle( + waveColor: isDarkMode(context) + ? Colors.white + : Colors.black, + extendWaveform: true, + showMiddleLine: false, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.color.surfaceContainer, + ), + padding: const EdgeInsets.only(left: 18), + margin: + const EdgeInsets.symmetric(horizontal: 15), + ) + : TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + if (_textFieldController.text == '') + GestureDetector( + onLongPressStart: (a) async { + if (!await Permission.microphone.isGranted) { + final statuses = await [ + Permission.microphone, + ].request(); + if (statuses[Permission.microphone]! + .isPermanentlyDenied) { + await openAppSettings(); + return; + } + if (!await Permission.microphone.isGranted) { + return; + } + } + setState(() { + _recordingState = RecordingState.recording; + }); + await HapticFeedback.heavyImpact(); + final audioTmpPath = + '${(await getApplicationCacheDirectory()).path}/recording.m4a'; + unawaited( + recorderController.record( + path: audioTmpPath, + ), + ); + }, + 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, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (_textFieldController.text != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), + ), + Offstage( + offstage: !_emojiShowing, + child: EmojiPicker( + textEditingController: _textFieldController, + onEmojiSelected: (category, emoji) { + setState(() {}); + }, + onBackspacePressed: () { + setState(() {}); + }, + config: Config( + height: 300, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index f6c2cda..9f2aefc 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -1,13 +1,13 @@ import 'dart:collection'; -import 'dart:convert'; - +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/tables/messages_table.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; enum MessageSendState { received, @@ -19,42 +19,38 @@ enum MessageSendState { } MessageSendState messageSendStateFromMessage(Message msg) { - MessageSendState state; - - if (!msg.acknowledgeByServer) { - if (msg.messageOtherId == null) { - state = MessageSendState.sending; - } else { - state = MessageSendState.receiving; + if (msg.senderId == null) { + /// messages was send by me, look up if every messages was received by the server... + if (msg.ackByServer == null) { + return MessageSendState.sending; } - } else { - if (msg.messageOtherId == null) { - // message send - if (msg.openedAt == null) { - state = MessageSendState.send; - } else { - state = MessageSendState.sendOpened; - } + if (msg.openedAt != null) { + return MessageSendState.sendOpened; } else { - // message received - if (msg.openedAt == null) { - state = MessageSendState.received; - } else { - state = MessageSendState.receivedOpened; - } + return MessageSendState.send; } } - return state; + + // message received + if (msg.openedAt == null) { + return MessageSendState.received; + } else { + return MessageSendState.receivedOpened; + } } class MessageSendStateIcon extends StatefulWidget { const MessageSendStateIcon( - this.messages, { + this.messages, + this.mediaFiles, { super.key, this.canBeReopened = false, + this.lastReaction, this.mainAxisAlignment = MainAxisAlignment.end, }); final List messages; + final List mediaFiles; + final Reaction? lastReaction; final MainAxisAlignment mainAxisAlignment; final bool canBeReopened; @@ -83,31 +79,28 @@ class _MessageSendStateIconState extends State { @override Widget build(BuildContext context) { - final icons = []; + var icons = []; var text = ''; - - final kindsAlreadyShown = HashSet(); Widget? textWidget; + textWidget = null; + final kindsAlreadyShown = HashSet(); + + var hasLoader = false; + GestureTapCallback? onTap; for (final message in widget.messages) { if (icons.length == 2) break; - if (kindsAlreadyShown.contains(message.kind)) continue; - kindsAlreadyShown.add(message.kind); + if (kindsAlreadyShown.contains(message.type)) continue; + kindsAlreadyShown.add(message.type); final state = messageSendStateFromMessage(message); - late Color color; - MessageContent? content; - if (message.contentJson == null) { - color = getMessageColorFromType(TextMessageContent(text: ''), context); - } else { - content = MessageContent.fromJson( - message.kind, - jsonDecode(message.contentJson!) as Map, - ); - if (content == null) continue; - color = getMessageColorFromType(content, context); - } + final mediaFile = message.mediaId == null + ? null + : widget.mediaFiles + .firstWhereOrNull((t) => t.mediaId == message.mediaId); + + final color = getMessageColorFromType(message, mediaFile, context); Widget icon = const Placeholder(); textWidget = null; @@ -115,9 +108,12 @@ class _MessageSendStateIconState extends State { switch (state) { case MessageSendState.receivedOpened: icon = Icon(Icons.crop_square, size: 14, color: color); - if (content is TextMessageContent) { - if (isEmoji(content.text)) { - icon = Text(content.text, style: const TextStyle(fontSize: 12)); + if (message.content != null) { + if (isEmoji(message.content!)) { + icon = Text( + message.content!, + style: const TextStyle(fontSize: 12), + ); } } text = context.lang.messageSendState_Received; @@ -133,13 +129,14 @@ class _MessageSendStateIconState extends State { case MessageSendState.received: icon = Icon(Icons.square_rounded, size: 14, color: color); text = context.lang.messageSendState_Received; - if (message.kind == MessageKind.media) { - if (message.downloadState == DownloadState.pending) { + if (message.type == MessageType.media && mediaFile != null) { + if (mediaFile.downloadState == DownloadState.pending) { text = context.lang.messageSendState_TapToLoad; } - if (message.downloadState == DownloadState.downloading) { + if (mediaFile.downloadState == DownloadState.downloading) { text = context.lang.messageSendState_Loading; icon = getLoaderIcon(color); + hasLoader = true; } } case MessageSendState.send: @@ -149,42 +146,116 @@ class _MessageSendStateIconState extends State { case MessageSendState.sending: icon = getLoaderIcon(color); text = context.lang.messageSendState_Sending; + + if (mediaFile != null) { + if (mediaFile.uploadState == UploadState.uploadLimitReached) { + icon = FaIcon( + FontAwesomeIcons.triangleExclamation, + size: 12, + color: color, + ); + + textWidget = Text( + context.lang.uploadLimitReached, + style: const TextStyle(fontSize: 9), + ); + + onTap = () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + }; + } + if (mediaFile.uploadState == UploadState.preprocessing) { + text = 'Wird verarbeitet'; + } + } + + hasLoader = true; case MessageSendState.receiving: icon = getLoaderIcon(color); text = context.lang.messageSendState_Received; + hasLoader = true; } - if (message.kind == MessageKind.storedMediaFile) { + if (message.mediaStored) { icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); text = context.lang.messageStoredInGallery; } - if (message.kind == MessageKind.reopenedMedia) { - icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); - text = context.lang.messageReopened; + if (mediaFile != null) { + if (mediaFile.reopenByContact) { + icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); + text = context.lang.messageReopened; + } + + if (mediaFile.downloadState == DownloadState.reuploadRequested) { + icon = + FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); + textWidget = Text( + context.lang.retransmissionRequested, + style: const TextStyle(fontSize: 9), + ); + } } - if (message.errorWhileSending) { - icon = - FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color); - text = 'Error'; + if (message.isDeletedFromSender) { + icon = FaIcon(FontAwesomeIcons.trash, size: 10, color: color); + text = context.lang.messageWasDeletedShort; } - if (message.mediaRetransmissionState == MediaRetransmitting.requested) { - icon = FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); - textWidget = Text( - context.lang.retransmissionRequested, - style: const TextStyle(fontSize: 9), - ); + if (hasLoader) { + icons = [icon]; + break; } - if (message.kind == MessageKind.media) { + if (message.type == MessageType.media) { icons.insert(0, icon); } else { icons.add(icon); } } + if (widget.lastReaction != null && + !widget.messages.any((t) => t.openedAt == null)) { + /// No messages are still open, so check if the reaction is the last message received. + + if (!widget.messages + .any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) { + if (EmojiAnimation.animatedIcons + .containsKey(widget.lastReaction!.emoji)) { + icons = [ + SizedBox( + height: 18, + child: EmojiAnimation(emoji: widget.lastReaction!.emoji), + ), + ]; + } else { + icons = [ + SizedBox( + height: 18, + child: Center( + child: Text( + widget.lastReaction!.emoji, + style: const TextStyle(fontSize: 15), + strutStyle: const StrutStyle( + forceStrutHeight: true, + height: 1.4, + ), + ), + ), + ), + ]; + } + // Log.info("DISPLAY REACTION"); + } + } + if (icons.isEmpty) return Container(); var icon = icons[0]; @@ -193,36 +264,32 @@ class _MessageSendStateIconState extends State { icon = Stack( alignment: Alignment.center, children: [ - // First icon (bottom icon) - icons[0], - - Transform( - transform: Matrix4.identity() - ..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half - ..translateByDouble(3, 5, 0, 1), - // Move down by 10 pixels (adjust as needed) - alignment: Alignment.center, + Transform.scale( + scale: 1.3, child: icons[1], ), - // Second icon (top icon, slightly offset) + icons[0], ], ); } - return Row( - mainAxisAlignment: widget.mainAxisAlignment, - children: [ - icon, - const SizedBox(width: 3), - if (textWidget != null) - textWidget - else - Text( - text, - style: const TextStyle(fontSize: 12), - ), - const SizedBox(width: 5), - ], + return GestureDetector( + onTap: onTap, + child: Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: [ + icon, + const SizedBox(width: 3), + if (textWidget != null) + textWidget + else + Text( + text, + style: const TextStyle(fontSize: 12), + ), + const SizedBox(width: 5), + ], + ), ); } } diff --git a/lib/src/views/chats/chat_messages_components/response_container.dart b/lib/src/views/chats/chat_messages_components/response_container.dart index 14bdf7a..1c27f90 100644 --- a/lib/src/views/chats/chat_messages_components/response_container.dart +++ b/lib/src/views/chats/chat_messages_components/response_container.dart @@ -1,29 +1,30 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - 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_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; class ResponseContainer extends StatefulWidget { const ResponseContainer({ required this.msg, - required this.contact, + required this.group, required this.child, - required this.scrollToMessage, + required this.mediaService, + required this.borderRadius, + this.scrollToMessage, super.key, }); - final ChatMessage msg; - final Widget child; - final Contact contact; - final void Function(int) scrollToMessage; + final Message msg; + final Widget? child; + final Group group; + final MediaFileService? mediaService; + final BorderRadius borderRadius; + final void Function(String)? scrollToMessage; @override State createState() => _ResponseContainerState(); @@ -57,26 +58,31 @@ class _ResponseContainerState extends State { @override Widget build(BuildContext context) { - if (widget.msg.responseTo == null) { - return widget.child; + if (widget.msg.quotesMessageId == null) { + if (widget.child == null) { + return Container(); + } + return widget.child!; } return GestureDetector( - onTap: () => widget.scrollToMessage(widget.msg.responseTo!.messageId), + onTap: widget.scrollToMessage == null + ? null + : () => widget.scrollToMessage!(widget.msg.quotesMessageId!), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, ), decoration: BoxDecoration( - color: getMessageColor(widget.msg.message), - borderRadius: BorderRadius.circular(12), + color: getMessageColor(widget.msg), + borderRadius: widget.borderRadius, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( + key: _preview, padding: const EdgeInsets.only(top: 4, right: 4, left: 4), child: Container( - key: _preview, width: minWidth, decoration: BoxDecoration( color: context.color.surface.withAlpha(150), @@ -88,8 +94,8 @@ class _ResponseContainerState extends State { ), ), child: ResponsePreview( - contact: widget.contact, - message: widget.msg.responseTo!, + group: widget.group, + messageId: widget.msg.quotesMessageId, showBorder: false, ), ), @@ -108,14 +114,16 @@ class _ResponseContainerState extends State { class ResponsePreview extends StatefulWidget { const ResponsePreview({ - required this.message, - required this.contact, + required this.group, required this.showBorder, + this.message, + this.messageId, super.key, }); - final Message message; - final Contact contact; + final Message? message; + final String? messageId; + final Group group; final bool showBorder; @override @@ -123,81 +131,96 @@ class ResponsePreview extends StatefulWidget { } class _ResponsePreviewState extends State { - File? thumbnailPath; + Message? _message; + MediaFileService? _mediaService; + String _username = ''; @override void initState() { + _message = widget.message; + initAsync(); super.initState(); - unawaited(initAsync()); } Future initAsync() async { - final items = await MemoryItem.convertFromMessages([widget.message]); - if (items.length == 1 && mounted) { - setState(() { - thumbnailPath = items.values.first.thumbnailPath; - }); + _message ??= await twonlyDB.messagesDao + .getMessageById(widget.messageId!) + .getSingleOrNull(); + if (_message?.mediaId != null) { + _mediaService = await MediaFileService.fromMediaId(_message!.mediaId!); } + if (_message?.senderId != null) { + final contact = await twonlyDB.contactsDao + .getContactByUserId(_message!.senderId!) + .getSingleOrNull(); + if (contact != null) { + _username = getContactDisplayName(contact); + } + } + if (_message == null && mounted) { + _username = context.lang.quotedMessageWasDeleted; + } + if (mounted) setState(() {}); } @override Widget build(BuildContext context) { String? subtitle; + var color = const Color.fromARGB(233, 68, 137, 255); - if (widget.message.kind == MessageKind.textMessage) { - if (widget.message.contentJson != null) { - final content = MessageContent.fromJson( - MessageKind.textMessage, - jsonDecode(widget.message.contentJson!) as Map, - ); - if (content is TextMessageContent) { - subtitle = truncateString(content.text); + if (_message != null) { + if (_message!.type == MessageType.text) { + if (_message!.content != null) { + subtitle = truncateString(_message!.content!); } } - } - if (widget.message.kind == MessageKind.media) { - final content = MessageContent.fromJson( - MessageKind.media, - jsonDecode(widget.message.contentJson!) as Map, - ); - if (content is MediaMessageContent) { - subtitle = content.isVideo ? 'Video' : 'Image'; + if (_message!.type == MessageType.media && _mediaService != null) { + switch (_mediaService!.mediaFile.type) { + case MediaType.image: + subtitle = context.lang.image; + case MediaType.video: + subtitle = context.lang.video; + case MediaType.gif: + subtitle = 'Gif'; + case MediaType.audio: + subtitle = 'Audio'; + } } - } - var username = 'You'; - if (widget.message.messageOtherId != null) { - username = getContactDisplayName(widget.contact); - } + if (_message!.senderId == null) { + _username = context.lang.you; + // _username = _message!.senderId.toString(); + } - final color = getMessageColor(widget.message); + color = getMessageColor(_message!); - if (!widget.message.mediaStored) { - return Container( - padding: widget.showBorder - ? const EdgeInsets.only(left: 10, right: 10) - : const EdgeInsets.symmetric(horizontal: 5), - decoration: (widget.showBorder) - ? BoxDecoration( - border: Border( - left: BorderSide( - color: color, - width: 2, + if (!_message!.mediaStored) { + return Container( + padding: widget.showBorder + ? const EdgeInsets.only(left: 10, right: 10) + : const EdgeInsets.symmetric(horizontal: 5), + decoration: (widget.showBorder) + ? BoxDecoration( + border: Border( + left: BorderSide( + color: color, + width: 2, + ), ), - ), - ) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - username, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - if (subtitle != null) Text(subtitle), - ], - ), - ); + ) + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _username, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (subtitle != null) Text(subtitle), + ], + ), + ); + } } return Container( @@ -218,17 +241,22 @@ class _ResponsePreviewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - username, + _username, style: const TextStyle(fontWeight: FontWeight.bold), ), if (subtitle != null) Text(subtitle), ], ), ), - if (thumbnailPath != null) + if (_mediaService != null && + _mediaService!.mediaFile.type != MediaType.audio) SizedBox( height: widget.showBorder ? 100 : 210, - child: Image.file(thumbnailPath!), + child: Image.file( + _mediaService!.mediaFile.type == MediaType.video + ? _mediaService!.thumbnailPath + : _mediaService!.storedPath, + ), ), ], ), diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index f58449c..471cc42 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -1,29 +1,23 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:twonly/globals.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_database.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart' + show DownloadState, MediaType; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:video_player/video_player.dart'; @@ -31,8 +25,8 @@ import 'package:video_player/video_player.dart'; final NoScreenshot _noScreenshot = NoScreenshot.instance; class MediaViewerView extends StatefulWidget { - const MediaViewerView(this.contact, {super.key, this.initialMessage}); - final Contact contact; + const MediaViewerView(this.group, {super.key, this.initialMessage}); + final Group group; final Message? initialMessage; @@ -48,23 +42,22 @@ class _MediaViewerViewState extends State { double mediaViewerDistanceFromBottom = 0; // current image related - Uint8List? imageBytes; - String? videoPath; VideoPlayerController? videoController; + MediaFileService? currentMedia; + Message? currentMessage; + DateTime? canBeSeenUntil; - int maxShowTime = gMediaShowInfinite; double progress = 0; - bool isRealTwonly = false; - bool mirrorVideo = false; - bool isDownloading = false; bool showSendTextMessageInput = false; final GlobalKey mediaWidgetKey = GlobalKey(); bool imageSaved = false; bool imageSaving = false; + bool displayTwonlyPresent = true; + final emojiKey = GlobalKey(); - StreamSubscription? downloadStateListener; + StreamSubscription? downloadStateListener; List allMediaFiles = []; late StreamSubscription> _subscription; @@ -89,25 +82,27 @@ class _MediaViewerViewState extends State { _subscription.cancel(); downloadStateListener?.cancel(); videoController?.dispose(); + videoController = null; super.dispose(); } Future asyncLoadNextMedia(bool firstRun) async { final messages = - twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId); + twonlyDB.messagesDao.watchMediaNotOpened(widget.group.groupId); - _subscription = messages.listen((messages) { + _subscription = messages.listen((messages) async { for (final msg in messages) { - // if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { - // allMediaFiles.add(msg); - // } - // Find the index of the existing message with the same messageId + if (msg.mediaId == currentMedia?.mediaFile.mediaId) { + // The update of the current Media in case of a download is done in loadCurrentMediaFile + continue; + } + + /// If the messages was already there just replace it and go to the next... + final index = allMediaFiles.indexWhere((m) => m.messageId == msg.messageId); if (index >= 1) { - // to not modify the first message - // If the message exists, replace it allMediaFiles[index] = msg; } else if (index == -1) { // If the message does not exist, add it @@ -116,101 +111,96 @@ class _MediaViewerViewState extends State { } setState(() {}); if (firstRun) { - loadCurrentMediaFile(); // ignore: parameter_assignments firstRun = false; + await loadCurrentMediaFile(); } }); } Future nextMediaOrExit() async { - if (!mounted) return; - await videoController?.dispose(); - nextMediaTimer?.cancel(); - progressTimer?.cancel(); - if (allMediaFiles.isNotEmpty) { - try { - if (!imageSaved && maxShowTime != gMediaShowInfinite) { - await deleteMediaFile(allMediaFiles.first.messageId, 'mp4'); - await deleteMediaFile(allMediaFiles.first.messageId, 'png'); - } - } catch (e) { - Log.error('$e'); + /// Remove the current media file in case it is not set to unlimited + if (currentMedia != null) { + if (!imageSaved && + currentMedia!.mediaFile.displayLimitInMilliseconds != null) { + currentMedia!.fullMediaRemoval(); } } - if (allMediaFiles.isEmpty || allMediaFiles.length == 1) { - if (mounted) { - Navigator.pop(context); - } + + await videoController?.dispose(); + if (!mounted) return; + + nextMediaTimer?.cancel(); + progressTimer?.cancel(); + + if (allMediaFiles.isEmpty) { + Navigator.pop(context); } else { - allMediaFiles.removeAt(0); await loadCurrentMediaFile(); } } Future loadCurrentMediaFile({bool showTwonly = false}) async { - if (!mounted) return; - if (!context.mounted || allMediaFiles.isEmpty) return nextMediaOrExit(); + if (!mounted || !context.mounted) return; + if (allMediaFiles.isEmpty || allMediaFiles.first.mediaId == null) { + return nextMediaOrExit(); + } await _noScreenshot.screenshotOff(); setState(() { videoController = null; - imageBytes = null; + currentMedia = null; + currentMessage = null; canBeSeenUntil = null; - maxShowTime = gMediaShowInfinite; imageSaving = false; imageSaved = false; - mirrorVideo = false; progress = 0; - videoPath = null; - isDownloading = false; - isRealTwonly = false; showSendTextMessageInput = false; }); - if (Platform.isAndroid) { - await flutterLocalNotificationsPlugin - .cancel(allMediaFiles.first.contactId); - } else { - await flutterLocalNotificationsPlugin.cancelAll(); - } + // if (Platform.isAndroid) { + // await flutterLocalNotificationsPlugin + // .cancel(allMediaFiles.first.contactId); + // } else { + await flutterLocalNotificationsPlugin.cancelAll(); + // } - if (allMediaFiles.first.downloadState != DownloadState.downloaded) { - setState(() { - isDownloading = true; - }); - await startDownloadMedia(allMediaFiles.first, true); + final stream = + twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!); - final stream = twonlyDB.messagesDao - .getMessageByMessageId(allMediaFiles.first.messageId) - .watchSingleOrNull(); - await downloadStateListener?.cancel(); - downloadStateListener = stream.listen((updated) async { - if (updated != null) { - if (updated.downloadState == DownloadState.downloaded) { - await downloadStateListener?.cancel(); - await handleNextDownloadedMedia(updated, showTwonly); - // start downloading all the other possible missing media files. - await tryDownloadAllMediaFiles(force: true); - } + var downloadTriggered = false; + + await downloadStateListener?.cancel(); + downloadStateListener = stream.listen((updated) async { + if (updated == null) return; + if (updated.downloadState != DownloadState.ready) { + if (!downloadTriggered) { + downloadTriggered = true; + final mediaFile = await twonlyDB.mediaFilesDao + .getMediaFileById(allMediaFiles.first.mediaId!); + if (mediaFile == null) return; + await startDownloadMedia(mediaFile, true); + unawaited(tryDownloadAllMediaFiles(force: true)); } - }); - } else { - await handleNextDownloadedMedia(allMediaFiles.first, showTwonly); - } + return; + } + + await downloadStateListener?.cancel(); + await handleNextDownloadedMedia(showTwonly); + // start downloading all the other possible missing media files. + }); } Future handleNextDownloadedMedia( - Message current, bool showTwonly, ) async { - final content = - MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map); + if (allMediaFiles.isEmpty) return; + currentMessage = allMediaFiles.removeAt(0); + final currentMediaLocal = + await MediaFileService.fromMediaId(currentMessage!.mediaId!); + if (currentMediaLocal == null || !mounted) return; - if (content.isRealTwonly) { - setState(() { - isRealTwonly = true; - }); + if (currentMediaLocal.mediaFile.requiresAuthentication) { if (!showTwonly) return; final isAuth = await authenticateUser( @@ -224,119 +214,103 @@ class _MediaViewerViewState extends State { } await notifyContactAboutOpeningMessage( - current.contactId, - [current.messageOtherId!], + currentMessage!.senderId!, + [currentMessage!.messageId], ); - await twonlyDB.messagesDao.updateMessageByMessageId( - current.messageId, - MessagesCompanion(openedAt: Value(DateTime.now())), - ); - - if (content.isVideo) { - final videoPathTmp = await getVideoPath(current.messageId); - if (videoPathTmp != null) { - videoController = VideoPlayerController.file(File(videoPathTmp.path)); - await videoController - ?.setLooping(content.maxShowTime == gMediaShowInfinite); - await videoController?.initialize().then((_) { - videoController!.play(); - videoController?.addListener(() { - setState(() { - progress = 1 - - videoController!.value.position.inSeconds / - videoController!.value.duration.inSeconds; - }); - if (content.maxShowTime != gMediaShowInfinite) { - if (videoController?.value.position == - videoController?.value.duration) { - nextMediaOrExit(); - } - } - }); - setState(() { - videoPath = videoPathTmp.path; - }); - // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler - }).catchError(Log.error); - } - } - - imageBytes = await getImageBytes(current.messageId); - - if ((imageBytes == null && !content.isVideo) || - (content.isVideo && videoController == null)) { - Log.error('media files are not found...'); - // When the message should be downloaded but imageBytes are null then a error happened - await handleMediaError(current); + if (!currentMediaLocal.tempPath.existsSync()) { + Log.error('Temp media file not found...'); + await handleMediaError(currentMediaLocal.mediaFile); return nextMediaOrExit(); } - if (!content.isVideo) { - if (content.maxShowTime != gMediaShowInfinite) { + if (currentMediaLocal.mediaFile.type == MediaType.video) { + videoController = VideoPlayerController.file(currentMediaLocal.tempPath); + await videoController?.setLooping( + currentMediaLocal.mediaFile.displayLimitInMilliseconds == null, + ); + await videoController?.initialize().then((_) { + if (videoController == null) return; + videoController?.play(); + videoController?.addListener(() { + setState(() { + progress = 1 - + videoController!.value.position.inSeconds / + videoController!.value.duration.inSeconds; + }); + if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { + if (videoController?.value.position == + videoController?.value.duration) { + nextMediaOrExit(); + } + } + }); + // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler + }).catchError(Log.error); + } else { + if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { canBeSeenUntil = DateTime.now().add( - Duration(seconds: content.maxShowTime), + Duration( + milliseconds: + currentMediaLocal.mediaFile.displayLimitInMilliseconds!, + ), ); startTimer(); } } setState(() { - maxShowTime = content.maxShowTime; - isDownloading = false; - mirrorVideo = content.mirrorVideo; + currentMedia = currentMediaLocal; }); } void startTimer() { nextMediaTimer?.cancel(); progressTimer?.cancel(); - nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { - if (context.mounted) { - nextMediaOrExit(); - } - }); - progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { - if (canBeSeenUntil != null) { + if (canBeSeenUntil != null) { + nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { + if (context.mounted) { + nextMediaOrExit(); + } + }); + progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { + if (currentMedia!.mediaFile.displayLimitInMilliseconds == null || + canBeSeenUntil == null) { + return; + } final difference = canBeSeenUntil!.difference(DateTime.now()); // Calculate the progress as a value between 0.0 and 1.0 - progress = difference.inMilliseconds / (maxShowTime * 1000); + progress = difference.inMilliseconds / + (currentMedia!.mediaFile.displayLimitInMilliseconds!); setState(() {}); - } - }); + }); + } } Future onPressedSaveToGallery() async { - if (allMediaFiles.first.messageOtherId == null) { - return; // should not be possible - } setState(() { imageSaving = true; }); - await twonlyDB.messagesDao.updateMessageByMessageId( - allMediaFiles.first.messageId, - const MessagesCompanion(mediaStored: Value(true)), - ); - await encryptAndSendMessageAsync( - null, - widget.contact.userId, - MessageJson( - kind: MessageKind.storedMediaFile, - messageSenderId: allMediaFiles.first.messageId, - messageReceiverId: allMediaFiles.first.messageOtherId, - content: MessageContent(), - timestamp: DateTime.now(), + await currentMedia!.storeMediaFile(); + await sendCipherTextToGroup( + widget.group.groupId, + pb.EncryptedContent( + mediaUpdate: pb.EncryptedContent_MediaUpdate( + type: pb.EncryptedContent_MediaUpdate_Type.STORED, + targetMessageId: currentMessage!.messageId, + ), ), - pushNotification: PushNotification(kind: PushKind.storedMediaFile), ); setState(() { imageSaved = true; }); - final user = await getUser(); - if (user != null && (user.storeMediaFilesInGallery)) { - if (videoPath != null) { - await saveVideoToGallery(videoPath!); - } else { - await saveImageToGallery(imageBytes!); + + if (gUser.storeMediaFilesInGallery) { + if (currentMedia!.mediaFile.type == MediaType.video) { + await saveVideoToGallery(currentMedia!.storedPath.path); + } else if (currentMedia!.mediaFile.type == MediaType.image || + currentMedia!.mediaFile.type == MediaType.gif) { + final imageBytes = await currentMedia!.storedPath.readAsBytes(); + await saveImageToGallery(imageBytes); } } setState(() { @@ -346,10 +320,12 @@ class _MediaViewerViewState extends State { void displayShortReactions() { final renderBox = - mediaWidgetKey.currentContext!.findRenderObject()! as RenderBox; + mediaWidgetKey.currentContext!.findRenderObject() as RenderBox?; setState(() { showShortReactions = true; - mediaViewerDistanceFromBottom = renderBox.size.height; + if (renderBox != null) { + mediaViewerDistanceFromBottom = renderBox.size.height; + } }); } @@ -358,7 +334,8 @@ class _MediaViewerViewState extends State { key: mediaWidgetKey, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (maxShowTime == gMediaShowInfinite) + if (currentMedia != null && + currentMedia!.mediaFile.displayLimitInMilliseconds == null) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: imageSaved @@ -368,7 +345,7 @@ class _MediaViewerViewState extends State { ? Theme.of(context).colorScheme.outline : Theme.of(context).colorScheme.primary, ), - onPressed: onPressedSaveToGallery, + onPressed: (currentMedia == null) ? null : onPressedSaveToGallery, child: Row( children: [ if (imageSaving) @@ -450,11 +427,12 @@ class _MediaViewerViewState extends State { context, MaterialPageRoute( builder: (context) { - return CameraSendToView(widget.contact); + return CameraSendToView(widget.group); }, ), ); - if (mounted && maxShowTime != gMediaShowInfinite) { + if (mounted && + currentMedia!.mediaFile.displayLimitInMilliseconds != null) { await nextMediaOrExit(); } else { await videoController?.play(); @@ -477,7 +455,7 @@ class _MediaViewerViewState extends State { child: Stack( fit: StackFit.expand, children: [ - if ((imageBytes != null || videoController != null) && + if ((currentMedia != null || videoController != null) && (canBeSeenUntil == null || progress >= 0)) GestureDetector( onTap: () { @@ -497,44 +475,24 @@ class _MediaViewerViewState extends State { children: [ if (videoController != null) Positioned.fill( - child: Transform.flip( - flipX: mirrorVideo, - child: VideoPlayer(videoController!), - ), - ), - if (imageBytes != null) + child: VideoPlayer(videoController!), + ) + else if (currentMedia != null && + currentMedia!.mediaFile.type == MediaType.image || + currentMedia!.mediaFile.type == MediaType.gif) Positioned.fill( - child: Image.memory( - imageBytes!, + child: Image.file( + currentMedia!.tempPath, fit: BoxFit.contain, - frameBuilder: ( - context, - child, - frame, - wasSynchronouslyLoaded, - ) { - if (wasSynchronouslyLoaded) return child; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: frame != null - ? child - : Container( - height: 60, - color: Colors.transparent, - width: 60, - child: const CircularProgressIndicator( - strokeWidth: 2, - ), - ), - ); - }, ), ), ], ), ), ), - if (isRealTwonly && imageBytes == null) + if (currentMedia != null && + currentMedia!.mediaFile.requiresAuthentication && + displayTwonlyPresent) Positioned.fill( child: GestureDetector( onTap: () { @@ -570,13 +528,16 @@ class _MediaViewerViewState extends State { ], ), ), - if (isDownloading) + if (currentMedia != null && + currentMedia?.mediaFile.downloadState != DownloadState.ready) const Positioned.fill( child: Center( child: SizedBox( height: 60, width: 60, - child: CircularProgressIndicator(strokeWidth: 6), + child: CircularProgressIndicator( + strokeWidth: 6, + ), ), ), ), @@ -602,7 +563,7 @@ class _MediaViewerViewState extends State { left: showSendTextMessageInput ? 0 : null, right: showSendTextMessageInput ? 0 : 15, child: Text( - getContactDisplayName(widget.contact), + widget.group.groupName, textAlign: TextAlign.center, style: TextStyle( fontSize: showSendTextMessageInput ? 24 : 14, @@ -658,18 +619,12 @@ class _MediaViewerViewState extends State { ), IconButton( icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () { + onPressed: () async { if (textMessageController.text.isNotEmpty) { - sendTextMessage( - widget.contact.userId, - TextMessageContent( - text: textMessageController.text, - responseToMessageId: - allMediaFiles.first.messageOtherId, - ), - PushNotification( - kind: PushKind.response, - ), + await insertAndSendTextMessage( + widget.group.groupId, + textMessageController.text, + currentMessage!.messageId, ); textMessageController.clear(); } @@ -683,14 +638,14 @@ class _MediaViewerViewState extends State { ), ), ), - if (allMediaFiles.isNotEmpty) + if (currentMedia != null) ReactionButtons( show: showShortReactions, textInputFocused: showSendTextMessageInput, mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, - userId: widget.contact.userId, - responseToMessageId: allMediaFiles.first.messageOtherId!, - isVideo: videoController != null, + groupId: widget.group.groupId, + messageId: currentMessage!.messageId, + emojiKey: emojiKey, hide: () { setState(() { showShortReactions = false; @@ -698,201 +653,12 @@ class _MediaViewerViewState extends State { }); }, ), + Positioned.fill( + child: EmojiFloatWidget(key: emojiKey), + ), ], ), ), ); } } - -class ReactionButtons extends StatefulWidget { - const ReactionButtons({ - required this.show, - required this.textInputFocused, - required this.userId, - required this.mediaViewerDistanceFromBottom, - required this.responseToMessageId, - required this.isVideo, - required this.hide, - super.key, - }); - - final double mediaViewerDistanceFromBottom; - final bool show; - final bool isVideo; - final bool textInputFocused; - final int userId; - final int responseToMessageId; - final void Function() hide; - - @override - State createState() => _ReactionButtonsState(); -} - -class _ReactionButtonsState extends State { - int selectedShortReaction = -1; - - List selectedEmojis = - EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); - - @override - void initState() { - super.initState(); - initAsync(); - } - - Future initAsync() async { - final user = await getUser(); - if (user != null && user.preSelectedEmojies != null) { - selectedEmojis = user.preSelectedEmojies!; - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final firstRowEmojis = selectedEmojis.take(6).toList(); - final secondRowEmojis = - selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; - - return AnimatedPositioned( - duration: const Duration(milliseconds: 200), // Animation duration - bottom: widget.show - ? (widget.textInputFocused - ? 50 - : widget.mediaViewerDistanceFromBottom) - : widget.mediaViewerDistanceFromBottom - 20, - left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, - right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, - curve: Curves.linearToEaseOut, - child: AnimatedOpacity( - opacity: widget.show ? 1.0 : 0.0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: Container( - color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, - padding: - widget.show ? const EdgeInsets.symmetric(vertical: 32) : null, - child: Column( - children: [ - if (secondRowEmojis.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: secondRowEmojis - .map( - (emoji) => EmojiReactionWidget( - userId: widget.userId, - responseToMessageId: widget.responseToMessageId, - hide: widget.hide, - show: widget.show, - isVideo: widget.isVideo, - emoji: emoji as String, - ), - ) - .toList(), - ), - if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: firstRowEmojis - .map( - (emoji) => EmojiReactionWidget( - userId: widget.userId, - responseToMessageId: widget.responseToMessageId, - hide: widget.hide, - show: widget.show, - isVideo: widget.isVideo, - emoji: emoji, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ); - } -} - -class EmojiReactionWidget extends StatefulWidget { - const EmojiReactionWidget({ - required this.userId, - required this.responseToMessageId, - required this.hide, - required this.isVideo, - required this.show, - required this.emoji, - super.key, - }); - final int userId; - final int responseToMessageId; - final Function hide; - final bool show; - final bool isVideo; - final String emoji; - - @override - State createState() => _EmojiReactionWidgetState(); -} - -class _EmojiReactionWidgetState extends State { - int selectedShortReaction = -1; - - @override - Widget build(BuildContext context) { - return AnimatedSize( - duration: const Duration(milliseconds: 200), - curve: Curves.linearToEaseOut, - child: GestureDetector( - onTap: () { - sendTextMessage( - widget.userId, - TextMessageContent( - text: widget.emoji, - responseToMessageId: widget.responseToMessageId, - ), - PushNotification( - kind: widget.isVideo - ? PushKind.reactionToVideo - : PushKind.reactionToImage, - reactionContent: widget.emoji, - ), - ); - setState(() { - selectedShortReaction = 0; // Assuming index is 0 for this example - }); - Future.delayed(const Duration(milliseconds: 300), () { - if (mounted) { - setState(() { - widget.hide(); - selectedShortReaction = -1; - }); - } - }); - }, - child: (selectedShortReaction == - 0) // Assuming index is 0 for this example - ? EmojiAnimationFlying( - emoji: widget.emoji, - duration: const Duration(milliseconds: 300), - startPosition: 0, - size: (widget.show) ? 40 : 10, - ) - : AnimatedOpacity( - opacity: (selectedShortReaction == -1) ? 1 : 0, // Fade in/out - duration: const Duration(milliseconds: 150), - child: SizedBox( - width: widget.show ? 40 : 10, - child: Center( - child: EmojiAnimation( - emoji: widget.emoji, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart new file mode 100644 index 0000000..2f3a871 --- /dev/null +++ b/lib/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +Offset getGlobalOffset(GlobalKey targetKey) { + final ctx = targetKey.currentContext; + if (ctx == null) { + return Offset.zero; + } + final renderObject = ctx.findRenderObject(); + if (renderObject is RenderBox) { + return renderObject.localToGlobal( + Offset(renderObject.size.width / 2, renderObject.size.height / 2), + ); + } + return Offset.zero; +} + +Future sendReaction( + String groupId, + String messageId, + String emoji, +) async { + await twonlyDB.reactionsDao.updateMyReaction( + messageId, + emoji, + false, + ); + await sendCipherTextToGroup( + groupId, + EncryptedContent( + reaction: EncryptedContent_Reaction( + targetMessageId: messageId, + emoji: emoji, + remove: false, + ), + ), + ); +} + +class EmojiReactionWidget extends StatefulWidget { + const EmojiReactionWidget({ + required this.messageId, + required this.groupId, + required this.hide, + required this.show, + required this.emoji, + required this.emojiKey, + super.key, + }); + final String messageId; + final String groupId; + final void Function() hide; + final bool show; + final String emoji; + final GlobalKey emojiKey; + + @override + State createState() => _EmojiReactionWidgetState(); +} + +class _EmojiReactionWidgetState extends State { + final GlobalKey _targetKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return AnimatedSize( + key: _targetKey, + duration: const Duration(milliseconds: 200), + curve: Curves.linearToEaseOut, + child: GestureDetector( + onTap: () async { + await sendReaction(widget.groupId, widget.messageId, widget.emoji); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_targetKey), + widget.emoji, + ); + widget.hide(); + }, + child: SizedBox( + width: widget.show ? 40 : 10, + child: Center( + child: EmojiAnimation( + emoji: widget.emoji, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart new file mode 100644 index 0000000..5da9f3d --- /dev/null +++ b/lib/src/views/chats/media_viewer_components/reaction_buttons.component.dart @@ -0,0 +1,318 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; +import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; +import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; + +class ReactionButtons extends StatefulWidget { + const ReactionButtons({ + required this.show, + required this.textInputFocused, + required this.mediaViewerDistanceFromBottom, + required this.messageId, + required this.groupId, + required this.emojiKey, + required this.hide, + super.key, + }); + + final double mediaViewerDistanceFromBottom; + final bool show; + final bool textInputFocused; + final GlobalKey emojiKey; + final String messageId; + final String groupId; + final void Function() hide; + + @override + State createState() => _ReactionButtonsState(); +} + +class _ReactionButtonsState extends State { + int selectedShortReaction = -1; + final GlobalKey _keyEmojiPicker = GlobalKey(); + + List selectedEmojis = + EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + if (gUser.preSelectedEmojies != null) { + selectedEmojis = gUser.preSelectedEmojies!; + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final firstRowEmojis = selectedEmojis.take(6).toList(); + final secondRowEmojis = + selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; + + return AnimatedPositioned( + duration: const Duration(milliseconds: 200), // Animation duration + bottom: widget.show + ? (widget.textInputFocused + ? 50 + : widget.mediaViewerDistanceFromBottom) + : widget.mediaViewerDistanceFromBottom - 20, + left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, + right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2, + curve: Curves.linearToEaseOut, + child: AnimatedOpacity( + opacity: widget.show ? 1.0 : 0.0, // Fade in/out + duration: const Duration(milliseconds: 150), + child: Container( + color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, + padding: + widget.show ? const EdgeInsets.symmetric(vertical: 32) : null, + child: Column( + children: [ + if (secondRowEmojis.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: secondRowEmojis + .map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: widget.show, + emoji: emoji as String, + emojiKey: widget.emojiKey, + ), + ) + .toList(), + ), + if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ...firstRowEmojis.map( + (emoji) => EmojiReactionWidget( + messageId: widget.messageId, + groupId: widget.groupId, + hide: widget.hide, + show: widget.show, + emoji: emoji, + emojiKey: widget.emojiKey, + ), + ), + GestureDetector( + key: _keyEmojiPicker, + onTap: () async { + // ignore: inference_failure_on_function_invocation + final layer = await showModalBottomSheet( + context: context, + backgroundColor: context.color.surface, + builder: (BuildContext context) { + return const EmojiPickerBottom(); + }, + ) as EmojiLayerData?; + if (layer == null) return; + await sendReaction( + widget.groupId, + widget.messageId, + layer.text, + ); + widget.emojiKey.currentState?.spawn( + getGlobalOffset(_keyEmojiPicker), + layer.text, + ); + widget.hide(); + }, + child: Container( + decoration: BoxDecoration( + color: context.color.surfaceContainer.withAlpha(100), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(8), + child: const FaIcon( + FontAwesomeIcons.ellipsisVertical, + size: 24, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class EmojiFloatWidget extends StatefulWidget { + const EmojiFloatWidget({ + super.key, + }); + @override + EmojiFloatWidgetState createState() => EmojiFloatWidgetState(); +} + +class EmojiFloatWidgetState extends State + with SingleTickerProviderStateMixin { + final List<_Particle> _particles = []; + late final Ticker _ticker; + final Random _rnd = Random(); + Duration _lastTick = Duration.zero; + + @override + void initState() { + super.initState(); + _ticker = createTicker(_tick)..start(); + } + + void _tick(Duration elapsed) { + final dt = (_lastTick == Duration.zero) + ? 0.016 + : (elapsed - _lastTick).inMicroseconds / 1e6; + _lastTick = elapsed; + + for (final p in List<_Particle>.from(_particles)) { + p.update(dt); + if (p.isDead) _particles.remove(p); + } + if (mounted) setState(() {}); + } + + @override + void dispose() { + _ticker.dispose(); + super.dispose(); + } + + /// Call this to spawn the emoji animation from a global screen position. + void spawn(Offset globalPosition, String emoji) { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return; + final local = box.globalToLocal(globalPosition); + const spawnCount = 10; + final life = const Duration(milliseconds: 2000).inMilliseconds / 1000.0; + + for (var i = 0; i < spawnCount; i++) { + final dx = (_rnd.nextDouble() - 0.5) * 220; + final vx = dx; + final vy = -(100 + _rnd.nextDouble() * 80); + final rot = (_rnd.nextDouble() - 0.5) * 2; + final scale = 0.9 + _rnd.nextDouble() * 0.6; + + _particles.add( + _Particle( + emoji: emoji, + x: local.dx, + y: local.dy, + vx: vx, + vy: vy, + rotation: rot, + lifetime: life, + scale: scale, + ), + ); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: CustomPaint( + painter: _ParticlePainter(List<_Particle>.from(_particles)), + size: Size.infinite, + ), + ); + } +} + +class _Particle { + _Particle({ + required this.emoji, + required this.x, + required this.y, + required this.vx, + required this.vy, + required this.rotation, + required this.lifetime, + required this.scale, + }); + final String emoji; + double x; + double y; + double vx; + double vy; + double rotation; + double age = 0; + final double lifetime; + final double scale; + bool get isDead => age >= lifetime; + + void update(double dt) { + age += dt; + // vertical-only motion emphasis: mild gravity slows ascent then gently pulls down + vy += 100 * dt; // gravity (positive = down) + // slight horizontal drag to reduce sideways drift + vx *= 1 - 3.0 * dt; + // integrate position + x += vx * dt; + y += vy * dt; + // slow rotation decay + rotation *= 1 - 1.5 * dt; + } + + double get progress => (age / lifetime).clamp(0.0, 1.0); + + // opacity falls from 1 -> 0 as particle ages + double get opacity => (1.0 - progress).clamp(0.0, 1.0); + + // scale can gently grow then shrink; here we slightly increase early + double get currentScale { + final p = progress; + if (p < 0.5) return scale * (1.0 + 0.3 * (p / 0.5)); + return scale * (1.3 - 0.3 * ((p - 0.5) / 0.5)); + } +} + +class _ParticlePainter extends CustomPainter { + _ParticlePainter(this.particles); + final List<_Particle> particles; + + @override + void paint(Canvas canvas, Size size) { + final textPainter = TextPainter(textDirection: TextDirection.ltr); + for (final p in particles) { + final tp = TextSpan( + text: p.emoji, + style: TextStyle( + fontSize: 24 * p.currentScale, + color: Colors.black.withValues(alpha: p.opacity), + ), + ); + textPainter + ..text = tp + ..layout(); + canvas + ..save() + ..translate(p.x - textPainter.width / 2, p.y - textPainter.height / 2) + ..rotate(p.rotation); + textPainter.paint(canvas, Offset.zero); + canvas.restore(); + } + } + + @override + bool shouldRepaint(covariant _ParticlePainter old) => true; +} diff --git a/lib/src/views/chats/message_info.view.dart b/lib/src/views/chats/message_info.view.dart new file mode 100644 index 0000000..bcf15c8 --- /dev/null +++ b/lib/src/views/chats/message_info.view.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.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/model/memory_item.model.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; + +class MessageInfoView extends StatefulWidget { + const MessageInfoView({ + required this.message, + required this.group, + required this.galleryItems, + super.key, + }); + + final Message message; + final Group group; + final List galleryItems; + + @override + State createState() => _MessageInfoViewState(); +} + +class _MessageInfoViewState extends State { + StreamSubscription>? actionsStream; + StreamSubscription>? historyStream; + StreamSubscription>? groupMemberStream; + + List<(MessageAction, Contact)> messageActions = []; + List messageHistory = []; + List<(GroupMember, Contact)> groupMembers = []; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + actionsStream?.cancel(); + historyStream?.cancel(); + groupMemberStream?.cancel(); + super.dispose(); + } + + Future initAsync() async { + final streamActions = + twonlyDB.messagesDao.watchMessageActions(widget.message.messageId); + actionsStream = streamActions.listen((update) { + setState(() { + messageActions = update; + }); + }); + + final streamGroup = + twonlyDB.messagesDao.watchMembersByGroupId(widget.message.groupId); + groupMemberStream = streamGroup.listen((update) { + setState(() { + groupMembers = update; + }); + }); + + final streamHistory = + twonlyDB.messagesDao.watchMessageHistory(widget.message.messageId); + historyStream = streamHistory.listen((update) { + setState(() { + messageHistory = update; + }); + }); + } + + List getReceivedColumns(BuildContext context) { + if (widget.message.senderId != null) return []; + + final columns = [ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Text(context.lang.sentTo), + const SizedBox(height: 10), + ]; + + for (final groupMember in groupMembers) { + final ackByServer = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByServerAt && + t.$2.userId == groupMember.$2.userId, + ); + final ackByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.ackByUserAt && + t.$2.userId == groupMember.$2.userId, + ); + final openedByUser = messageActions.firstWhereOrNull( + (t) => + t.$1.type == MessageActionType.openedAt && + t.$2.userId == groupMember.$2.userId, + ); + + var actionTypeText = context.lang.waitingForInternet; + var actionAt = widget.message.createdAt; + if (ackByServer != null) { + actionTypeText = context.lang.sent; + actionAt = ackByServer.$1.actionAt; + } + if (ackByUser != null) { + actionTypeText = context.lang.received; + actionAt = ackByUser.$1.actionAt; + } + if (openedByUser != null) { + actionTypeText = context.lang.opened; + actionAt = openedByUser.$1.actionAt; + } + + columns.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + AvatarIcon( + contactId: groupMember.$2.userId, + fontSize: 15, + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getContactDisplayName(groupMember.$2), + style: const TextStyle(fontSize: 17), + ), + ], + ), + ), + Column( + children: [ + Text( + friendlyDateTime(context, actionAt), + style: const TextStyle(fontSize: 12), + ), + Text(actionTypeText), + ], + ), + ], + ), + ), + ); + } + return columns; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: ListView( + children: [ + const SizedBox(height: 20), + Stack( + children: [ + ChatListEntry( + group: widget.group, + message: widget.message, + galleryItems: widget.galleryItems, + ), + Positioned.fill( + child: GestureDetector( + onTap: () { + // In case in ChatListEntry is a image, this prevents to open the image preview. + }, + child: Container( + color: Colors.transparent, + ), + ), + ), + ], + ), + Text( + '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}', + ), + if (widget.message.senderId != null && + widget.message.ackByServer != null) + Text( + '${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}', + ), + if (messageHistory.isNotEmpty) ...[ + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + BetterListTile( + icon: FontAwesomeIcons.pencil, + padding: EdgeInsets.zero, + text: context.lang.editHistory, + onTap: () async { + // ignore: inference_failure_on_function_invocation + await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) { + return MessageHistoryView( + message: widget.message, + changes: messageHistory, + group: widget.group, + ); + }, + ); + }, + ), + ], + ...getReceivedColumns(context), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 85c0773..a014939 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -1,18 +1,18 @@ import 'dart:async'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/group_context_menu.component.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; +import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; class StartNewChatView extends StatefulWidget { const StartNewChatView({super.key}); @@ -21,18 +21,20 @@ class StartNewChatView extends StatefulWidget { } class _StartNewChatView extends State { - List contacts = []; + List filteredContacts = []; + List filteredGroups = []; List allContacts = []; + List allNonDirectGroups = []; final TextEditingController searchUserName = TextEditingController(); late StreamSubscription> contactSub; + late StreamSubscription> allNonDirectGroupsSub; @override void initState() { super.initState(); - final stream = twonlyDB.contactsDao.watchContactsForStartNewChat(); - - contactSub = stream.listen((update) async { + contactSub = + twonlyDB.contactsDao.watchAllAcceptedContacts().listen((update) async { update.sort( (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)), ); @@ -41,18 +43,28 @@ class _StartNewChatView extends State { }); await filterUsers(); }); + + allNonDirectGroupsSub = + twonlyDB.groupsDao.watchGroupsForStartNewChat().listen((update) async { + setState(() { + allNonDirectGroups = update; + }); + await filterUsers(); + }); } @override void dispose() { - unawaited(contactSub.cancel()); + allNonDirectGroupsSub.cancel(); + contactSub.cancel(); super.dispose(); } Future filterUsers() async { if (searchUserName.value.text.isEmpty) { setState(() { - contacts = allContacts; + filteredContacts = allContacts; + filteredGroups = []; }); return; } @@ -63,11 +75,54 @@ class _StartNewChatView extends State { .contains(searchUserName.value.text.toLowerCase()), ) .toList(); + final groupsFiltered = allNonDirectGroups + .where( + (g) => g.groupName + .toLowerCase() + .contains(searchUserName.value.text.toLowerCase()), + ) + .toList(); setState(() { - contacts = usersFiltered; + filteredContacts = usersFiltered; + filteredGroups = groupsFiltered; }); } + Future _onTapUser(Contact user) async { + var directChat = await twonlyDB.groupsDao.getDirectChat(user.userId); + if (directChat == null) { + await twonlyDB.groupsDao.createNewDirectChat( + user.userId, + GroupsCompanion( + groupName: Value( + getContactDisplayName(user), + ), + ), + ); + directChat = await twonlyDB.groupsDao.getDirectChat(user.userId); + } + if (!mounted) return; + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(directChat!); + }, + ), + ); + } + + Future _onTapGroup(Group group) async { + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(group); + }, + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -75,127 +130,157 @@ class _StartNewChatView extends State { title: Text(context.lang.startNewChatTitle), ), body: SafeArea( - child: PieCanvas( - theme: getPieCanvasTheme(context), - child: Padding( - padding: - const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: (_) async { - await filterUsers(); - }, - controller: searchUserName, - decoration: getInputDecoration( - context, - context.lang.shareImageSearchAllContacts, - ), + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + await filterUsers(); + }, + controller: searchUserName, + decoration: getInputDecoration( + context, + context.lang.startNewChatSearchHint, ), ), - const SizedBox(height: 10), - Expanded( - child: UserList( - contacts, - ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: + filteredContacts.length + 3 + filteredGroups.length, + itemBuilder: (BuildContext context, int i) { + if (searchUserName.text.isEmpty) { + if (i == 0) { + return ListTile( + title: Text(context.lang.newGroup), + leading: const CircleAvatar( + child: FaIcon( + FontAwesomeIcons.userGroup, + size: 13, + ), + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const GroupCreateSelectMembersView(), + ), + ); + }, + ); + } + if (i == 1) { + return ListTile( + title: Text(context.lang.startNewChatNewContact), + leading: const CircleAvatar( + child: FaIcon( + FontAwesomeIcons.userPlus, + size: 13, + ), + ), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AddNewUserView(), + ), + ); + }, + ); + } + if (i == 2) { + return const Divider(); + } + i = i - 3; + } else { + if (i == 0) { + return filteredContacts.isNotEmpty + ? ListTile( + title: Text( + context.lang.contacts, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ) + : Container(); + } else { + i -= 1; + } + } + + if (i < filteredContacts.length) { + return UserContextMenu( + key: Key(filteredContacts[i].userId.toString()), + contact: filteredContacts[i], + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(filteredContacts[i])), + FlameCounterWidget( + contactId: filteredContacts[i].userId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + contactId: filteredContacts[i].userId, + fontSize: 13, + ), + onTap: () => _onTapUser(filteredContacts[i]), + ), + ); + } + i -= filteredContacts.length; + + if (i == 0) { + return filteredGroups.isNotEmpty + ? ListTile( + title: Text( + context.lang.groups, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ) + : Container(); + } + + i -= 1; + + if (i < filteredGroups.length) { + return GroupContextMenu( + key: Key(filteredGroups[i].groupId), + group: filteredGroups[i], + child: ListTile( + title: Text( + filteredGroups[i].groupName, + ), + leading: AvatarIcon( + group: filteredGroups[i], + fontSize: 13, + ), + onTap: () => _onTapGroup(filteredGroups[i]), + ), + ); + } + return Container(); + }, ), - ], - ), + ), + ], ), ), ), ); } } - -class UserList extends StatelessWidget { - const UserList( - this.users, { - super.key, - }); - final List users; - - @override - Widget build(BuildContext context) { - return ListView.builder( - restorationId: 'new_message_users_list', - itemCount: users.length + 2, - itemBuilder: (BuildContext context, int i) { - if (i == 0) { - return ListTile( - key: const Key('add_new_contact'), - title: Text(context.lang.startNewChatNewContact), - leading: const CircleAvatar( - child: FaIcon( - FontAwesomeIcons.userPlus, - size: 13, - ), - ), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AddNewUserView(), - ), - ); - }, - ); - } - if (i == 1) { - return const Divider(); - } - final user = users[i - 2]; - final flameCounter = getFlameCounterFromContact(user); - return UserContextMenu( - key: Key(user.userId.toString()), - contact: user, - child: ListTile( - title: Row( - children: [ - Text(getContactDisplayName(user)), - if (flameCounter >= 1) - FlameCounterWidget( - user, - flameCounter, - prefix: true, - ), - const Spacer(), - IconButton( - icon: FaIcon( - FontAwesomeIcons.boxOpen, - size: 13, - color: user.archived ? null : Colors.transparent, - ), - onPressed: user.archived - ? () async { - const update = - ContactsCompanion(archived: Value(false)); - await twonlyDB.contactsDao - .updateContact(user.userId, update); - } - : null, - ), - ], - ), - leading: ContactAvatar( - contact: user, - fontSize: 13, - ), - onTap: () async { - await Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(user); - }, - ), - ); - }, - ), - ); - }, - ); - } -} diff --git a/lib/src/views/components/animate_icon.dart b/lib/src/views/components/animate_icon.dart index 267a400..981c315 100644 --- a/lib/src/views/components/animate_icon.dart +++ b/lib/src/views/components/animate_icon.dart @@ -33,7 +33,7 @@ class EmojiAnimation extends StatelessWidget { '😭': 'loudly-crying.json', '🤯': 'mind-blown.json', '❤️‍🔥': 'red_heart_fire.json', - '😁': 'grinning.json', + //'😁': 'grinning.json', '😆': 'laughing.json', '😅': 'grin-sweat.json', '🤣': 'rofl.json', diff --git a/lib/src/views/components/avatar_icon.component.dart b/lib/src/views/components/avatar_icon.component.dart new file mode 100644 index 0000000..0eca6dd --- /dev/null +++ b/lib/src/views/components/avatar_icon.component.dart @@ -0,0 +1,178 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class AvatarIcon extends StatefulWidget { + const AvatarIcon({ + super.key, + this.group, + this.contactId, + this.myAvatar = false, + this.fontSize = 20, + this.color, + }); + final Group? group; + final int? contactId; + final bool myAvatar; + final double? fontSize; + final Color? color; + + @override + State createState() => _AvatarIconState(); +} + +class _AvatarIconState extends State { + List _avatarSVGs = []; + String? _globalUserDataCallBackId; + + StreamSubscription>? groupStream; + StreamSubscription>? contactsStream; + StreamSubscription? contactStream; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + groupStream?.cancel(); + contactStream?.cancel(); + contactsStream?.cancel(); + if (_globalUserDataCallBackId != null) { + globalUserDataChangedCallBack.remove(_globalUserDataCallBackId); + } + super.dispose(); + } + + Future initAsync() async { + if (widget.group != null) { + groupStream = twonlyDB.groupsDao + .watchGroupContact(widget.group!.groupId) + .listen((contacts) { + _avatarSVGs = []; + if (contacts.length == 1) { + if (contacts.first.avatarSvgCompressed != null) { + _avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); + } + } else { + for (final contact in contacts) { + if (contact.avatarSvgCompressed != null) { + _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); + } + } + } + setState(() {}); + }); + } else if (widget.myAvatar) { + _globalUserDataCallBackId = 'avatar_${getRandomString(10)}'; + globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () { + setState(() { + if (gUser.avatarSvg != null) { + _avatarSVGs = [gUser.avatarSvg!]; + } else { + _avatarSVGs = []; + } + }); + }; + if (gUser.avatarSvg != null) { + _avatarSVGs = [gUser.avatarSvg!]; + } + } else if (widget.contactId != null) { + contactStream = twonlyDB.contactsDao + .watchContact(widget.contactId!) + .listen((contact) { + if (contact != null && contact.avatarSvgCompressed != null) { + _avatarSVGs = [getAvatarSvg(contact.avatarSvgCompressed!)]; + setState(() {}); + } + }); + } + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2); + + Widget avatars = SvgPicture.asset('assets/images/default_avatar.svg'); + + if (_avatarSVGs.length == 1) { + avatars = SvgPicture.string( + _avatarSVGs.first, + errorBuilder: (a, b, c) => avatars, + ); + } else if (_avatarSVGs.length >= 2) { + final a = SvgPicture.string( + _avatarSVGs.first, + errorBuilder: (a, b, c) => avatars, + ); + final b = SvgPicture.string( + _avatarSVGs[1], + errorBuilder: (a, b, c) => avatars, + ); + if (_avatarSVGs.length >= 3) { + final c = SvgPicture.string( + _avatarSVGs[2], + errorBuilder: (a, b, c) => avatars, + ); + avatars = Stack( + children: [ + Transform.translate( + offset: const Offset(-15, 5), + child: Transform.scale( + scale: 0.8, + child: c, + ), + ), + Transform.translate( + offset: const Offset(15, 5), + child: Transform.scale( + scale: 0.8, + child: b, + ), + ), + a, + ], + ); + } else { + avatars = Stack( + children: [ + Transform.translate( + offset: const Offset(-10, 5), + child: Transform.scale( + scale: 0.8, + child: b, + ), + ), + Transform.translate(offset: const Offset(10, 0), child: a), + ], + ); + } + } + + return Container( + constraints: BoxConstraints( + minHeight: 2 * (widget.fontSize ?? 20), + minWidth: 2 * (widget.fontSize ?? 20), + maxWidth: 2 * (widget.fontSize ?? 20), + maxHeight: 2 * (widget.fontSize ?? 20), + ), + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + height: proSize as double, + width: proSize, + color: widget.color, + child: Center(child: avatars), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index b7ad83c..fe408a5 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -3,35 +3,46 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class BetterListTile extends StatelessWidget { const BetterListTile({ - required this.icon, required this.text, - required this.onTap, + this.onTap, + this.icon, + this.leading, super.key, this.color, this.subtitle, + this.trailing, this.iconSize = 20, + this.padding, }); - final IconData icon; + final IconData? icon; + final Widget? leading; + final Widget? trailing; final String text; final Widget? subtitle; final Color? color; - final VoidCallback onTap; + final VoidCallback? onTap; final double iconSize; + final EdgeInsets? padding; @override Widget build(BuildContext context) { return ListTile( leading: Padding( - padding: const EdgeInsets.only( - right: 10, - left: 19, - ), - child: FaIcon( - icon, - size: iconSize, - color: color, - ), + padding: (padding == null) + ? const EdgeInsets.only( + right: 10, + left: 19, + ) + : padding!, + child: (leading != null) + ? leading + : FaIcon( + icon, + size: iconSize, + color: color, + ), ), + trailing: trailing, title: Text( text, style: TextStyle(color: color), diff --git a/lib/src/views/components/better_text.dart b/lib/src/views/components/better_text.dart index 2373f9f..7b4608f 100644 --- a/lib/src/views/components/better_text.dart +++ b/lib/src/views/components/better_text.dart @@ -5,8 +5,9 @@ import 'package:twonly/src/utils/log.dart'; import 'package:url_launcher/url_launcher.dart'; class BetterText extends StatelessWidget { - const BetterText({required this.text, super.key}); + const BetterText({required this.text, required this.textColor, super.key}); final String text; + final Color textColor; @override Widget build(BuildContext context) { @@ -51,16 +52,25 @@ class BetterText extends StatelessWidget { } if (lastMatchEnd < text.length) { - spans.add(TextSpan(text: text.substring(lastMatchEnd))); + spans.add( + TextSpan( + text: text.substring(lastMatchEnd), + ), + ); } return Text.rich( TextSpan( children: spans, ), - style: const TextStyle( - color: Colors.white, + softWrap: true, + textAlign: TextAlign.start, + overflow: TextOverflow.visible, + style: TextStyle( + color: textColor, fontSize: 17, + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, ), ); } diff --git a/lib/src/views/components/context_menu.component.dart b/lib/src/views/components/context_menu.component.dart new file mode 100644 index 0000000..5e6fd62 --- /dev/null +++ b/lib/src/views/components/context_menu.component.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class ContextMenu extends StatefulWidget { + const ContextMenu({ + required this.child, + required this.items, + super.key, + }); + + final List items; + final Widget child; + + @override + State createState() => _ContextMenuState(); +} + +class _ContextMenuState extends State { + Offset? _tapPosition; + + Widget _getIcon(IconData icon) { + return Padding( + padding: const EdgeInsets.only(left: 12), + child: FaIcon( + icon, + size: 20, + ), + ); + } + + Future _showCustomMenu() async { + if (_tapPosition == null) { + return; + } + final overlay = Overlay.of(context).context.findRenderObject(); + if (overlay == null) { + return; + } + unawaited(HapticFeedback.heavyImpact()); + + await showMenu( + context: context, + menuPadding: EdgeInsetsGeometry.zero, + elevation: 1, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + popUpAnimationStyle: const AnimationStyle( + duration: Duration.zero, + curve: Curves.fastOutSlowIn, + ), + items: >[ + ...widget.items.map( + (item) => PopupMenuItem( + padding: const EdgeInsets.only(right: 4), + child: ListTile( + title: Text(item.title), + onTap: () async { + if (mounted) Navigator.pop(context); + await item.onTap(); + }, + leading: _getIcon(item.icon), + ), + ), + ), + ], + position: RelativeRect.fromRect( + _tapPosition! & const Size(40, 40), + Offset.zero & overlay.semanticBounds.size, + ), + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onLongPress: _showCustomMenu, + onTapDown: (TapDownDetails details) { + _tapPosition = details.globalPosition; + }, + child: widget.child, + ); + } +} + +class ContextMenuItem { + ContextMenuItem({ + required this.title, + required this.onTap, + required this.icon, + }); + final String title; + final Future Function() onTap; + final IconData icon; +} diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 2f3c80a..dc83b12 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -1,26 +1,74 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; -class FlameCounterWidget extends StatelessWidget { - const FlameCounterWidget( - this.user, - this.flameCounter, { +class FlameCounterWidget extends StatefulWidget { + const FlameCounterWidget({ + this.groupId, + this.contactId, this.prefix = false, super.key, }); - final Contact user; - final int flameCounter; + final String? groupId; + final int? contactId; final bool prefix; + @override + State createState() => _FlameCounterWidgetState(); +} + +class _FlameCounterWidgetState extends State { + int flameCounter = 0; + bool isBestFriend = false; + + StreamSubscription? flameCounterSub; + + @override + void initState() { + initAsync(); + super.initState(); + } + + @override + void dispose() { + flameCounterSub?.cancel(); + super.dispose(); + } + + Future initAsync() async { + var groupId = widget.groupId; + if (widget.groupId == null && widget.contactId != null) { + final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!); + groupId = group?.groupId; + } else if (groupId != null) { + // do not display the flame counter for groups + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (!(group?.isDirectChat ?? false)) { + return; + } + } + if (groupId != null) { + isBestFriend = gUser.myBestFriendGroupId == groupId; + final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); + flameCounterSub = stream.listen((counter) { + if (mounted) { + setState(() { + flameCounter = counter; + }); + } + }); + } + } + @override Widget build(BuildContext context) { + if (flameCounter < 1) return Container(); return Row( children: [ - if (prefix) const SizedBox(width: 5), - if (prefix) const Text('•'), - if (prefix) const SizedBox(width: 5), + if (widget.prefix) const SizedBox(width: 5), + if (widget.prefix) const Text('•'), + if (widget.prefix) const SizedBox(width: 5), Text( flameCounter.toString(), style: const TextStyle(fontSize: 13), @@ -28,7 +76,7 @@ class FlameCounterWidget extends StatelessWidget { SizedBox( height: 15, child: EmojiAnimation( - emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥', + emoji: isBestFriend ? '❤️‍🔥' : '🔥', ), ), ], diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart new file mode 100644 index 0000000..28808a1 --- /dev/null +++ b/lib/src/views/components/group_context_menu.component.dart @@ -0,0 +1,100 @@ +import 'package:drift/drift.dart' hide Column; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.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/alert_dialog.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; + +class GroupContextMenu extends StatelessWidget { + const GroupContextMenu({ + required this.group, + required this.child, + super.key, + }); + final Widget child; + final Group group; + + @override + Widget build(BuildContext context) { + return ContextMenu( + items: [ + if (!group.archived) + ContextMenuItem( + title: context.lang.contextMenuArchiveUser, + onTap: () async { + const update = GroupsCompanion(archived: Value(true)); + if (context.mounted) { + await twonlyDB.groupsDao.updateGroup(group.groupId, update); + } + }, + icon: Icons.archive_outlined, + ), + if (group.archived) + ContextMenuItem( + title: context.lang.contextMenuUndoArchiveUser, + onTap: () async { + const update = GroupsCompanion(archived: Value(false)); + if (context.mounted) { + await twonlyDB.groupsDao.updateGroup(group.groupId, update); + } + }, + icon: Icons.unarchive_outlined, + ), + ContextMenuItem( + title: context.lang.contextMenuOpenChat, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ChatMessagesView(group); + }, + ), + ); + }, + icon: FontAwesomeIcons.comments, + ), + if (!group.archived) + ContextMenuItem( + title: group.pinned + ? context.lang.contextMenuUnpin + : context.lang.contextMenuPin, + onTap: () async { + final update = GroupsCompanion(pinned: Value(!group.pinned)); + if (context.mounted) { + await twonlyDB.groupsDao.updateGroup(group.groupId, update); + } + }, + icon: group.pinned + ? FontAwesomeIcons.thumbtackSlash + : FontAwesomeIcons.thumbtack, + ), + ContextMenuItem( + title: context.lang.delete, + icon: FontAwesomeIcons.trashCan, + onTap: () async { + final ok = await showAlertDialog( + context, + context.lang.deleteTitle, + context.lang.groupContextMenuDeleteGroup, + ); + if (ok) { + await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + // await twonlyDB.groupsDao.deleteGroup(group.groupId); + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + deletedContent: Value(true), + ), + ); + } + }, + ), + ], + child: child, + ); + } +} diff --git a/lib/src/views/components/initialsavatar.dart b/lib/src/views/components/initialsavatar.dart deleted file mode 100644 index 59068db..0000000 --- a/lib/src/views/components/initialsavatar.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/utils/log.dart'; - -class ContactAvatar extends StatelessWidget { - const ContactAvatar({ - super.key, - this.contact, - this.userData, - this.fontSize = 20, - this.color, - }); - final Contact? contact; - final UserData? userData; - final double? fontSize; - final Color? color; - - @override - Widget build(BuildContext context) { - String? avatarSvg; - - if (contact != null) { - avatarSvg = contact!.avatarSvg; - } else if (userData != null) { - avatarSvg = userData!.avatarSvg; - } else { - return Container(); - } - - final proSize = (fontSize == null) ? 40 : (fontSize! * 2); - - return Container( - constraints: BoxConstraints( - minHeight: 2 * (fontSize ?? 20), - minWidth: 2 * (fontSize ?? 20), - maxWidth: 2 * (fontSize ?? 20), - maxHeight: 2 * (fontSize ?? 20), - ), - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - height: proSize as double, - width: proSize, - color: color, - child: Center( - child: avatarSvg == null - ? SvgPicture.asset('assets/images/default_avatar.svg') - : SvgPicture.string( - avatarSvg, - errorBuilder: (context, error, stackTrace) { - Log.error('$error'); - return Container(); - }, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart new file mode 100644 index 0000000..ba490e8 --- /dev/null +++ b/lib/src/views/components/max_flame_list_title.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; + +class MaxFlameListTitle extends StatefulWidget { + const MaxFlameListTitle({ + required this.contactId, + super.key, + }); + final int contactId; + + @override + State createState() => _MaxFlameListTitleState(); +} + +class _MaxFlameListTitleState extends State { + int _flameCounter = 0; + Group? _directChat; + late String _groupId; + + late StreamSubscription _flameCounterSub; + late StreamSubscription _groupSub; + + @override + void initState() { + _groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); + final stream = twonlyDB.groupsDao.watchFlameCounter(_groupId); + _flameCounterSub = stream.listen((counter) { + if (mounted) { + setState(() { + _flameCounter = counter - + 1; // in the watchFlameCounter a one is added, so remove this here + }); + } + }); + final stream2 = twonlyDB.groupsDao.watchGroup(_groupId); + _groupSub = stream2.listen((update) { + if (mounted) { + setState(() { + _directChat = update; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _flameCounterSub.cancel(); + _groupSub.cancel(); + super.dispose(); + } + + Future _restoreFlames() async { + if (!isPayingUser(getCurrentPlan())) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + return; + } + await twonlyDB.groupsDao.updateGroup( + _groupId, + GroupsCompanion( + flameCounter: Value(_directChat!.maxFlameCounter), + lastFlameCounterChange: Value(DateTime.now()), + ), + ); + await syncFlameCounters(forceForGroup: _groupId); + } + + @override + Widget build(BuildContext context) { + if (_directChat == null || + _directChat!.maxFlameCounter == 0 || + _flameCounter >= _directChat!.maxFlameCounter || + _directChat!.maxFlameCounterFrom! + .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { + return Container(); + } + return BetterListTile( + onTap: _restoreFlames, + leading: const SizedBox( + width: 24, + child: EmojiAnimation( + emoji: '🔥', + ), + ), + text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames', + ); + } +} diff --git a/lib/src/views/components/select_chat_deletion_time.comp.dart b/lib/src/views/components/select_chat_deletion_time.comp.dart new file mode 100644 index 0000000..279a4c7 --- /dev/null +++ b/lib/src/views/components/select_chat_deletion_time.comp.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:drift/drift.dart' show Value; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; + +class SelectChatDeletionTimeListTitle extends StatefulWidget { + const SelectChatDeletionTimeListTitle({ + required this.groupId, + this.disabled = false, + super.key, + }); + + final String groupId; + final bool disabled; + + @override + State createState() => + _SelectChatDeletionTimeListTitleState(); +} + +class _SelectChatDeletionTimeListTitleState + extends State { + Group? group; + + late StreamSubscription groupSub; + int _selectedDeletionTime = 0; + + @override + void initState() { + groupSub = twonlyDB.groupsDao.watchGroup(widget.groupId).listen((update) { + if (update == null) return; + group = update; + + final selected = _getOptions().indexWhere( + (t) => t.$1 == update.deleteMessagesAfterMilliseconds, + ); + setState(() { + _selectedDeletionTime = selected % _getOptions().length; + }); + }); + super.initState(); + } + + @override + void dispose() { + groupSub.cancel(); + super.dispose(); + } + + Future _showDialog(Widget child) async { + await showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6), + // The Bottom margin is provided to align the popup above the system navigation bar. + margin: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + // Provide a background color for the popup. + color: CupertinoColors.systemBackground.resolveFrom(context), + // Use a SafeArea widget to avoid system overlaps. + child: SafeArea(top: false, child: child), + ), + ); + + if (group == null) return; + + final selected = _getOptions()[_selectedDeletionTime].$1; + + if (group!.deleteMessagesAfterMilliseconds != selected) { + if (group!.isDirectChat) { + await twonlyDB.groupsDao.updateGroup( + group!.groupId, + GroupsCompanion( + deleteMessagesAfterMilliseconds: Value(selected), + ), + ); + await sendCipherTextToGroup( + group!.groupId, + EncryptedContent( + groupUpdate: EncryptedContent_GroupUpdate( + groupActionType: GroupActionType.changeDisplayMaxTime.name, + newDeleteMessagesAfterMilliseconds: Int64(selected), + ), + ), + ); + } else { + if (!await updateChatDeletionTime(group!, selected)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + } + } + + List<(int, String)> _getOptions() { + return getOptions(context); + } + + static List<(int, String)> getOptions(BuildContext context) { + return <(int, String)>[ + (1000 * 60 * 60, context.lang.deleteChatAfterAnHour), + (1000 * 60 * 60 * 24, context.lang.deleteChatAfterADay), + (1000 * 60 * 60 * 24 * 7, context.lang.deleteChatAfterAWeek), + (1000 * 60 * 60 * 24 * 30, context.lang.deleteChatAfterAMonth), + (1000 * 60 * 60 * 24 * 365, context.lang.deleteChatAfterAYear), + ]; + } + + @override + Widget build(BuildContext context) { + return BetterListTile( + icon: FontAwesomeIcons.stopwatch20, + text: context.lang.deleteChatAfter, + trailing: Text(_getOptions()[_selectedDeletionTime].$2), + onTap: widget.disabled + ? null + : () => _showDialog( + CupertinoPicker( + magnification: 1.22, + squeeze: 1.2, + useMagnifier: true, + itemExtent: 32, + // This sets the initial item. + scrollController: FixedExtentScrollController( + initialItem: _selectedDeletionTime, + ), + // This is called when selected item is changed. + onSelectedItemChanged: (int selectedItem) { + setState(() { + _selectedDeletionTime = selectedItem; + }); + }, + children: + List.generate(_getOptions().length, (int index) { + return Center( + child: Text(_getOptions()[index].$2), + ); + }), + ), + ), + ); + } +} diff --git a/lib/src/views/components/user_context_menu.component.dart b/lib/src/views/components/user_context_menu.component.dart new file mode 100644 index 0000000..7151848 --- /dev/null +++ b/lib/src/views/components/user_context_menu.component.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; + +class UserContextMenu extends StatelessWidget { + const UserContextMenu({ + required this.contact, + required this.child, + super.key, + }); + final Widget child; + final Contact contact; + + @override + Widget build(BuildContext context) { + return ContextMenu( + items: [ + ContextMenuItem( + title: context.lang.contextMenuUserProfile, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactView(contact.userId); + }, + ), + ); + }, + icon: FontAwesomeIcons.user, + ), + ], + child: child, + ); + } +} diff --git a/lib/src/views/components/user_context_menu.dart b/lib/src/views/components/user_context_menu.dart deleted file mode 100644 index 2b30fce..0000000 --- a/lib/src/views/components/user_context_menu.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/chats/chat_messages.view.dart'; -import 'package:twonly/src/views/contact/contact.view.dart'; - -class UserContextMenu extends StatefulWidget { - const UserContextMenu({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuState(); -} - -class _UserContextMenuState extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - onToggle: (menuOpen) async { - if (menuOpen) { - await HapticFeedback.heavyImpact(); - } - }, - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuOpenChat), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ChatMessagesView(widget.contact); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.solidComments), - ), - PieAction( - tooltip: Text( - widget.contact.pinned - ? context.lang.contextMenuUnpin - : context.lang.contextMenuPin, - ), - onSelect: () async { - final update = - ContactsCompanion(pinned: Value(!widget.contact.pinned)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: FaIcon( - widget.contact.pinned - ? FontAwesomeIcons.thumbtackSlash - : FontAwesomeIcons.thumbtack, - ), - ), - ], - child: widget.child, - ); - } -} - -class UserContextMenuBlocked extends StatefulWidget { - const UserContextMenuBlocked({ - required this.contact, - required this.child, - super.key, - }); - final Widget child; - final Contact contact; - - @override - State createState() => _UserContextMenuBlocked(); -} - -class _UserContextMenuBlocked extends State { - @override - Widget build(BuildContext context) { - return PieMenu( - onPressed: () => (), - actions: [ - if (!widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(true)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxArchive), - ), - if (widget.contact.archived) - PieAction( - tooltip: Text(context.lang.contextMenuUndoArchiveUser), - onSelect: () async { - const update = ContactsCompanion(archived: Value(false)); - if (context.mounted) { - await twonlyDB.contactsDao - .updateContact(widget.contact.userId, update); - } - }, - child: const FaIcon(FontAwesomeIcons.boxOpen), - ), - PieAction( - tooltip: Text(context.lang.contextMenuUserProfile), - onSelect: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactView(widget.contact.userId); - }, - ), - ); - }, - child: const FaIcon(FontAwesomeIcons.user), - ), - ], - child: widget.child, - ); - } -} - -PieTheme getPieCanvasTheme(BuildContext context) { - return PieTheme( - brightness: Theme.of(context).brightness, - rightClickShowsMenu: true, - radius: 70, - buttonTheme: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.tertiary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - buttonThemeHovered: PieButtonTheme( - backgroundColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.surfaceBright, - ), - tooltipPadding: const EdgeInsets.all(20), - overlayColor: isDarkMode(context) - ? const Color.fromARGB(69, 0, 0, 0) - : const Color.fromARGB(40, 0, 0, 0), - // spacing: 0, - tooltipTextStyle: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.w600, - ), - ); -} diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 27c1c45..6c8b2d8 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -1,38 +1,82 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; -class VerifiedShield extends StatelessWidget { - const VerifiedShield(this.contact, {super.key, this.size = 18}); - final Contact contact; +class VerifiedShield extends StatefulWidget { + const VerifiedShield({ + this.contact, + this.group, + super.key, + this.size = 18, + }); + final Group? group; + final Contact? contact; final double size; + @override + State createState() => _VerifiedShieldState(); +} + +class _VerifiedShieldState extends State { + bool isVerified = false; + Contact? contact; + + StreamSubscription>? stream; + + @override + void initState() { + if (widget.group != null) { + stream = twonlyDB.groupsDao + .watchGroupContact(widget.group!.groupId) + .listen((contacts) { + if (contacts.length == 1) { + contact = contacts.first; + } + setState(() { + isVerified = contacts.any((t) => t.verified); + }); + }); + } else if (widget.contact != null) { + isVerified = widget.contact!.verified; + contact = widget.contact; + } + + super.initState(); + } + + @override + void dispose() { + stream?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactVerifyView(contact); + onTap: (contact == null) + ? null + : () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return ContactVerifyView(contact!); + }, + ), + ); }, - ), - ); - }, child: Tooltip( - message: contact.verified + message: isVerified ? 'You verified this contact' : 'You have not verifies this contact.', child: FaIcon( - contact.verified - ? FontAwesomeIcons.shieldHeart - : Icons.gpp_maybe_rounded, - color: contact.verified - ? Theme.of(context).colorScheme.primary - : Colors.red, - size: size, + isVerified ? FontAwesomeIcons.shieldHeart : Icons.gpp_maybe_rounded, + color: + isVerified ? Theme.of(context).colorScheme.primary : Colors.red, + size: widget.size, ), ), ); diff --git a/lib/src/views/components/video_player_wrapper.dart b/lib/src/views/components/video_player_wrapper.dart index b35edc8..80833d0 100644 --- a/lib/src/views/components/video_player_wrapper.dart +++ b/lib/src/views/components/video_player_wrapper.dart @@ -7,11 +7,9 @@ import 'package:video_player/video_player.dart'; class VideoPlayerWrapper extends StatefulWidget { const VideoPlayerWrapper({ required this.videoPath, - required this.mirrorVideo, super.key, }); final File videoPath; - final bool mirrorVideo; @override State createState() => _VideoPlayerWrapperState(); @@ -48,10 +46,7 @@ class _VideoPlayerWrapperState extends State { child: _controller.value.isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(_controller), - ), + child: VideoPlayer(_controller), ) : const CircularProgressIndicator(), // Show loading indicator while initializing ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 31fcfde..d911a1b 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -1,17 +1,20 @@ +import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/flame.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; +import 'package:twonly/src/views/components/max_flame_list_title.dart'; +import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -26,13 +29,19 @@ class _ContactViewState extends State { Future handleUserRemoveRequest(Contact contact) async { final remove = await showAlertDialog( context, - context.lang.contactRemoveTitle(getContactDisplayName(contact)), + context.lang + .contactRemoveTitle(getContactDisplayName(contact, maxLength: 20)), context.lang.contactRemoveBody, ); if (remove) { - // trigger deletion for the other user... - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion( + accepted: Value(false), + requested: Value(false), + deletedByUser: Value(true), + ), + ); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } @@ -63,30 +72,14 @@ class _ContactViewState extends State { if (!mounted) return; if (res.isSuccess) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Benutzer wurde gemeldet.'), - duration: Duration(seconds: 3), + SnackBar( + content: Text(context.lang.userGotReported), + duration: const Duration(seconds: 3), ), ); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.', - ), - duration: Duration(seconds: 3), - ), - ); + showNetworkIssue(context); } - // if (block) { - // const update = ContactsCompanion(blocked: Value(true)); - // if (context.mounted) { - // await twonlyDB.contactsDao.updateContact(contact.userId, update); - // } - // if (mounted) { - // Navigator.popUntil(context, (route) => route.isFirst); - // } - // } } @override @@ -106,30 +99,30 @@ class _ContactViewState extends State { return Container(); } final contact = snapshot.data!; - final flameCounter = getFlameCounterFromContact(contact); return ListView( children: [ Padding( padding: const EdgeInsets.all(10), - child: ContactAvatar(contact: contact, fontSize: 30), + child: AvatarIcon(contactId: contact.userId, fontSize: 30), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(right: 10), - child: VerifiedShield(contact), + child: VerifiedShield( + key: GlobalKey(), + contact: contact, + ), ), Text( - getContactDisplayName(contact), + getContactDisplayName(contact, maxLength: 20), style: const TextStyle(fontSize: 20), ), - if (flameCounter > 0) - FlameCounterWidget( - contact, - flameCounter, - prefix: true, - ), + FlameCounterWidget( + contactId: contact.userId, + prefix: true, + ), ], ), if (getContactDisplayName(contact) != contact.username) @@ -150,6 +143,13 @@ class _ContactViewState extends State { }, ), const Divider(), + SelectChatDeletionTimeListTitle( + groupId: getUUIDforDirectChat(widget.userId, gUser.userId), + ), + const Divider(), + MaxFlameListTitle( + contactId: widget.userId, + ), BetterListTile( icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, @@ -162,28 +162,29 @@ class _ContactViewState extends State { }, ), ); + setState(() {}); }, ), - BetterListTile( - icon: FontAwesomeIcons.eraser, - iconSize: 16, - text: context.lang.deleteAllContactMessages, - onTap: () async { - final block = await showAlertDialog( - context, - context.lang.deleteAllContactMessages, - context.lang.deleteAllContactMessagesBody( - getContactDisplayName(contact), - ), - ); - if (block) { - if (context.mounted) { - await twonlyDB.messagesDao - .deleteMessagesByContactId(contact.userId); - } - } - }, - ), + // BetterListTile( + // icon: FontAwesomeIcons.eraser, + // iconSize: 16, + // text: context.lang.deleteAllContactMessages, + // onTap: () async { + // final block = await showAlertDialog( + // context, + // context.lang.deleteAllContactMessages, + // context.lang.deleteAllContactMessagesBody( + // getContactDisplayName(contact), + // ), + // ); + // if (block) { + // if (context.mounted) { + // await twonlyDB.messagesDao + // .deleteMessagesByContactId(contact.userId); + // } + // } + // }, + // ), BetterListTile( icon: FontAwesomeIcons.flag, text: context.lang.reportUser, @@ -194,13 +195,13 @@ class _ContactViewState extends State { text: context.lang.contactBlock, onTap: () => handleUserBlockRequest(contact), ), - BetterListTile( - icon: FontAwesomeIcons.userMinus, - iconSize: 16, - color: Colors.red, - text: context.lang.contactRemove, - onTap: () => handleUserRemoveRequest(contact), - ), + // BetterListTile( + // icon: FontAwesomeIcons.userMinus, + // iconSize: 16, + // color: Colors.red, + // text: context.lang.contactRemove, + // onTap: () => handleUserRemoveRequest(contact), + // ), ], ); }, diff --git a/lib/src/views/contact/contact_verify.view.dart b/lib/src/views/contact/contact_verify.view.dart index 05b05c3..2b37dd7 100644 --- a/lib/src/views/contact/contact_verify.view.dart +++ b/lib/src/views/contact/contact_verify.view.dart @@ -9,8 +9,8 @@ import 'package:image/image.dart' as imglib; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:lottie/lottie.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/components/fingerprint_text.dart'; diff --git a/lib/src/views/contact/contact_verify_qr_scan.view.dart b/lib/src/views/contact/contact_verify_qr_scan.view.dart index 566a833..5328b42 100644 --- a/lib/src/views/contact/contact_verify_qr_scan.view.dart +++ b/lib/src/views/contact/contact_verify_qr_scan.view.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; class ContactVerifyQrScanView extends StatefulWidget { diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart new file mode 100644 index 0000000..4d495ac --- /dev/null +++ b/lib/src/views/groups/group.view.dart @@ -0,0 +1,325 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; +import 'package:twonly/src/views/components/verified_shield.dart'; +import 'package:twonly/src/views/contact/contact.view.dart'; +import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; +import 'package:twonly/src/views/groups/group_member.context.dart'; +import 'package:twonly/src/views/settings/profile/profile.view.dart'; + +class GroupView extends StatefulWidget { + const GroupView(this.group, {super.key}); + + final Group group; + + @override + State createState() => _GroupViewState(); +} + +class _GroupViewState extends State { + late Group group; + + List<(Contact, GroupMember)> members = []; + + late StreamSubscription groupSub; + late StreamSubscription> membersSub; + + @override + void initState() { + group = widget.group; + initAsync(); + super.initState(); + } + + @override + void dispose() { + groupSub.cancel(); + membersSub.cancel(); + super.dispose(); + } + + Future initAsync() async { + final groupStream = twonlyDB.groupsDao.watchGroup(widget.group.groupId); + groupSub = groupStream.listen((update) { + if (update != null) { + setState(() { + group = update; + }); + } + }); + final membersStream = + twonlyDB.groupsDao.watchGroupMembers(widget.group.groupId); + membersSub = membersStream.listen((update) { + setState(() { + members = update; + members.sort( + (b, a) => a.$2.memberState!.index.compareTo(b.$2.memberState!.index), + ); + }); + }); + } + + Future _updateGroupName() async { + final newGroupName = await showGroupNameChangeDialog(context, group); + + if (context.mounted && + newGroupName != null && + newGroupName != '' && + newGroupName != group.groupName) { + if (!await updateGroupName(group, newGroupName)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _addNewGroupMembers() async { + final selectedUserIds = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectMembersView(group: group), + ), + ) as List?; + if (selectedUserIds == null) return; + if (!await addNewGroupMembers(group, selectedUserIds)) { + if (mounted) { + showNetworkIssue(context); + } + } + } + + Future _leaveGroup() async { + final ok = await showAlertDialog( + context, + context.lang.leaveGroupSureTitle, + context.lang.leaveGroupSureBody, + customOk: context.lang.leaveGroupSureOkBtn, + ); + if (!ok) return; + + // 1. Check if I am the only admin, while there are still normal members + // -> ERROR first select new admin + + if (members.isNotEmpty) { + // In case there are other members, check that there is at least one other admin before I leave the group. + + if (group.isGroupAdmin) { + if (!members.any((m) => m.$2.memberState == MemberState.admin)) { + if (!mounted) return; + await showAlertDialog( + context, + context.lang.leaveGroupSelectOtherAdminTitle, + context.lang.leaveGroupSelectOtherAdminBody, + customCancel: '', + ); + return; + } + } + } + + late bool success; + + if (group.isGroupAdmin) { + // Current user is a admin, to the state can be updated by the user him self. + final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); + success = await removeMemberFromGroup( + group, + keyPair.getPublicKey().serialize(), + gUser.userId, + ); + } else { + success = await leaveAsNonAdminFromGroup(group); + } + + if (!success) { + if (mounted) { + showNetworkIssue(context); + return; + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(''), + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: AvatarIcon( + group: group, + fontSize: 30, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: VerifiedShield(key: Key(group.groupId), group: group), + ), + Text( + substringBy(group.groupName, 25), + style: const TextStyle(fontSize: 20), + ), + ], + ), + const SizedBox(height: 50), + if (group.isGroupAdmin && !group.leftGroup) + BetterListTile( + icon: FontAwesomeIcons.pencil, + text: context.lang.groupNameInput, + onTap: _updateGroupName, + ), + SelectChatDeletionTimeListTitle( + groupId: widget.group.groupId, + disabled: !group.isGroupAdmin, + ), + const Divider(), + ListTile( + title: Padding( + padding: const EdgeInsets.only(left: 17), + child: Text( + '${members.length + 1} ${context.lang.groupMembers}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + if (group.isGroupAdmin && !group.leftGroup) + BetterListTile( + icon: FontAwesomeIcons.plus, + text: context.lang.addMember, + onTap: _addNewGroupMembers, + ), + BetterListTile( + padding: const EdgeInsets.only(left: 13), + leading: const AvatarIcon( + myAvatar: true, + fontSize: 16, + ), + text: context.lang.you, + trailing: (group.isGroupAdmin) ? Text(context.lang.admin) : null, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProfileView(), + ), + ); + }, + ), + ...members.map((member) { + return GroupMemberContextMenu( + group: group, + contact: member.$1, + member: member.$2, + child: BetterListTile( + padding: const EdgeInsets.only(left: 13), + leading: AvatarIcon( + contactId: member.$1.userId, + fontSize: 16, + ), + text: getContactDisplayName(member.$1, maxLength: 25), + trailing: (member.$2.memberState == MemberState.admin) + ? Text(context.lang.admin) + : null, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ContactView(member.$1.userId), + ), + ); + }, + ), + ); + }), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 10), + if (!group.leftGroup) + BetterListTile( + icon: FontAwesomeIcons.rightFromBracket, + color: Colors.red, + text: context.lang.leaveGroup, + onTap: _leaveGroup, + ) + else + ListTile( + title: Padding( + padding: const EdgeInsets.only(left: 17), + child: Text( + context.lang.groupYouAreNowLongerAMember, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ), + ], + ), + ); + } +} + +Future showGroupNameChangeDialog( + BuildContext context, + Group group, +) { + final controller = TextEditingController(text: group.groupName); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(context.lang.groupNameInput), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration(hintText: context.lang.groupNameInput), + ), + actions: [ + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(context.lang.ok), + onPressed: () { + Navigator.of(context).pop(controller.text); + }, + ), + ], + ); + }, + ); +} + +void showNetworkIssue(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.groupNetworkIssue), + duration: const Duration(seconds: 3), + ), + ); +} diff --git a/lib/src/views/groups/group_create_select_group_name.view.dart b/lib/src/views/groups/group_create_select_group_name.view.dart new file mode 100644 index 0000000..3af27e5 --- /dev/null +++ b/lib/src/views/groups/group_create_select_group_name.view.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; + +class GroupCreateSelectGroupNameView extends StatefulWidget { + const GroupCreateSelectGroupNameView({ + required this.selectedUsers, + super.key, + }); + + final List selectedUsers; + + @override + State createState() => + _GroupCreateSelectGroupNameViewState(); +} + +class _GroupCreateSelectGroupNameViewState + extends State { + final TextEditingController textFieldGroupName = TextEditingController(); + + bool _isLoading = false; + + Future _createNewGroup() async { + setState(() { + _isLoading = true; + }); + + final wasSuccess = + await createNewGroup(textFieldGroupName.text, widget.selectedUsers); + if (wasSuccess) { + // POP + if (mounted) { + Navigator.popUntil(context, (route) => route.isFirst); + } + return; + } + + setState(() { + _isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: Text(context.lang.selectGroupName), + ), + floatingActionButton: FilledButton.icon( + onPressed: (textFieldGroupName.text.isEmpty || _isLoading) + ? null + : _createNewGroup, + label: Text(context.lang.createGroup), + icon: _isLoading + ? const SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ) + : const FaIcon(FontAwesomeIcons.penToSquare), + ), + body: SafeArea( + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + setState(() {}); + }, + autofocus: true, + controller: textFieldGroupName, + decoration: getInputDecoration( + context, + context.lang.groupNameInput, + ), + ), + ), + const SizedBox(height: 10), + ListTile( + title: Text(context.lang.groupMembers), + ), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: widget.selectedUsers.length, + itemBuilder: (BuildContext context, int i) { + final user = widget.selectedUsers[i]; + return UserContextMenu( + contact: user, + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(user)), + FlameCounterWidget( + contactId: user.userId, + prefix: true, + ), + ], + ), + leading: AvatarIcon( + contactId: user.userId, + fontSize: 13, + ), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart new file mode 100644 index 0000000..0b2ca38 --- /dev/null +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -0,0 +1,281 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; +import 'package:twonly/src/views/groups/group_create_select_group_name.view.dart'; + +class GroupCreateSelectMembersView extends StatefulWidget { + const GroupCreateSelectMembersView({this.group, super.key}); + final Group? group; + @override + State createState() => _StartNewChatView(); +} + +class _StartNewChatView extends State { + List contacts = []; + List allContacts = []; + final TextEditingController searchUserName = TextEditingController(); + late StreamSubscription> contactSub; + + final HashSet selectedUsers = HashSet(); + final HashSet alreadyInGroup = HashSet(); + + @override + void initState() { + super.initState(); + + final stream = twonlyDB.contactsDao.watchAllAcceptedContacts(); + + contactSub = stream.listen((update) async { + update.sort( + (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)), + ); + setState(() { + allContacts = update; + }); + await filterUsers(); + }); + initAsync(); + } + + Future initAsync() async { + if (widget.group != null) { + final members = + await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); + for (final member in members) { + alreadyInGroup.add(member.userId); + } + if (mounted) setState(() {}); + } + } + + @override + void dispose() { + unawaited(contactSub.cancel()); + super.dispose(); + } + + Future filterUsers() async { + if (searchUserName.value.text.isEmpty) { + setState(() { + contacts = allContacts; + }); + return; + } + final usersFiltered = allContacts + .where( + (user) => getContactDisplayName(user) + .toLowerCase() + .contains(searchUserName.value.text.toLowerCase()), + ) + .toList(); + setState(() { + contacts = usersFiltered; + }); + } + + void toggleSelectedUser(int userId) { + if (alreadyInGroup.contains(userId)) return; + if (!selectedUsers.contains(userId)) { + selectedUsers.add(userId); + } else { + selectedUsers.remove(userId); + } + setState(() {}); + } + + Future submitChanges() async { + if (widget.group != null) { + Navigator.pop(context, selectedUsers.toList()); + return; + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupCreateSelectGroupNameView( + selectedUsers: allContacts + .where((t) => selectedUsers.contains(t.userId)) + .toList(), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: Text( + widget.group == null + ? context.lang.selectMembers + : context.lang.addMember, + ), + ), + floatingActionButton: FilledButton.icon( + onPressed: selectedUsers.isEmpty ? null : submitChanges, + label: Text( + widget.group == null ? context.lang.next : context.lang.updateGroup, + ), + icon: const FaIcon(FontAwesomeIcons.penToSquare), + ), + body: SafeArea( + child: Padding( + padding: + const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (_) async { + await filterUsers(); + }, + controller: searchUserName, + decoration: getInputDecoration( + context, + context.lang.shareImageSearchAllContacts, + ), + ), + ), + const SizedBox(height: 10), + Expanded( + child: ListView.builder( + restorationId: 'new_message_users_list', + itemCount: + contacts.length + (selectedUsers.isEmpty ? 0 : 2), + itemBuilder: (BuildContext context, int i) { + if (selectedUsers.isNotEmpty) { + final selected = selectedUsers.toList(); + if (i == 0) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 18), + constraints: const BoxConstraints( + maxHeight: 150, + ), + child: SingleChildScrollView( + child: LayoutBuilder( + builder: (context, constraints) { + // Wrap will use the available width from constraints.maxWidth + return Wrap( + spacing: 8, + children: selected.map((w) { + return _Chip( + contact: allContacts + .firstWhere((t) => t.userId == w), + onTap: toggleSelectedUser, + ); + }).toList(), + ); + }, + ), + ), + ); + } + if (i == 1) { + return const Divider(); + } + i -= 2; + } + final user = contacts[i]; + return UserContextMenu( + // when this is not set, then the avatar is not updated in the list when searching :/ + key: Key(user.userId.toString()), + contact: user, + child: ListTile( + title: Row( + children: [ + Text(getContactDisplayName(user)), + FlameCounterWidget( + contactId: user.userId, + prefix: true, + ), + ], + ), + subtitle: (alreadyInGroup.contains(user.userId)) + ? Text(context.lang.alreadyInGroup) + : null, + leading: AvatarIcon( + contactId: user.userId, + fontSize: 13, + ), + trailing: Checkbox( + value: selectedUsers.contains(user.userId) | + alreadyInGroup.contains(user.userId), + side: WidgetStateBorderSide.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return const BorderSide(width: 0); + } + return BorderSide( + color: Theme.of(context).colorScheme.outline, + ); + }, + ), + onChanged: (bool? value) { + toggleSelectedUser(user.userId); + }, + ), + onTap: () { + toggleSelectedUser(user.userId); + }, + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _Chip extends StatelessWidget { + const _Chip({ + required this.contact, + required this.onTap, + }); + final Contact contact; + final void Function(int) onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onTap(contact.userId), + child: Chip( + avatar: AvatarIcon( + contactId: contact.userId, + fontSize: 10, + ), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getContactDisplayName(contact), + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(width: 15), + const FaIcon( + FontAwesomeIcons.xmark, + color: Colors.grey, + size: 12, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/groups/group_member.context.dart b/lib/src/views/groups/group_member.context.dart new file mode 100644 index 0000000..687401f --- /dev/null +++ b/lib/src/views/groups/group_member.context.dart @@ -0,0 +1,172 @@ +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/group.services.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/components/context_menu.component.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; + +class GroupMemberContextMenu extends StatelessWidget { + const GroupMemberContextMenu({ + required this.contact, + required this.member, + required this.child, + required this.group, + super.key, + }); + final Contact contact; + final GroupMember member; + final Group group; + final Widget child; + + Future _makeContactAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.makeAdminRightsTitle(getContactDisplayName(contact)), + context.lang.makeAdminRightsBody(getContactDisplayName(contact)), + customOk: context.lang.makeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member.groupPublicKey!, + contact.userId, + false, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactAsAdmin(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.revokeAdminRightsTitle(getContactDisplayName(contact)), + '', + customOk: context.lang.revokeAdminRightsOkBtn, + ); + if (ok) { + if (!await manageAdminState( + group, + member.groupPublicKey!, + contact.userId, + true, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _removeContactFromGroup(BuildContext context) async { + final ok = await showAlertDialog( + context, + context.lang.removeContactFromGroupTitle(getContactDisplayName(contact)), + '', + ); + if (ok) { + if (!await removeMemberFromGroup( + group, + member.groupPublicKey!, + contact.userId, + )) { + if (context.mounted) { + showNetworkIssue(context); + } + } + } + } + + Future _makeContactRequest(BuildContext context) async { + await twonlyDB.contactsDao.updateContact( + member.contactId, + const ContactsCompanion( + requested: Value(true), + ), + ); + await sendCipherText( + member.contactId, + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), + ), + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.contactRequestSend), + duration: const Duration(seconds: 3), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return ContextMenu( + items: [ + if (contact.accepted) + ContextMenuItem( + title: context.lang.contextMenuOpenChat, + onTap: () async { + final directChat = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + if (directChat == null) { + // create + return; + } + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatMessagesView(directChat), + ), + ); + }, + icon: FontAwesomeIcons.message, + ), + if (!contact.accepted) + ContextMenuItem( + title: context.lang.createContactRequest, + onTap: () => _makeContactRequest(context), + icon: FontAwesomeIcons.userPlus, + ), + if (member.groupPublicKey != null && + group.isGroupAdmin && + member.memberState == MemberState.normal) + ContextMenuItem( + title: context.lang.makeAdmin, + onTap: () => _makeContactAdmin(context), + icon: FontAwesomeIcons.key, + ), + if (member.groupPublicKey != null && + group.isGroupAdmin && + member.memberState == MemberState.admin) + ContextMenuItem( + title: context.lang.removeAdmin, + onTap: () => _removeContactAsAdmin(context), + icon: FontAwesomeIcons.key, + ), + if (group.isGroupAdmin && member.groupPublicKey != null) + ContextMenuItem( + title: context.lang.removeFromGroup, + onTap: () => _removeContactFromGroup(context), + icon: FontAwesomeIcons.rightFromBracket, + ), + ], + child: child, + ); + } +} diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 3463ea0..53ae88b 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -3,14 +3,15 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; @@ -72,7 +73,7 @@ class HomeViewState extends State { } if (cameraController == null && !initCameraStarted && offsetRatio < 1) { initCameraStarted = true; - unawaited(selectCamera(selectedCameraDetails.cameraId, false, false)); + unawaited(selectCamera(selectedCameraDetails.cameraId, false)); } if (offsetRatio == 1) { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { @@ -99,7 +100,7 @@ class HomeViewState extends State { .listen((NotificationResponse? response) async { globalUpdateOfHomeViewPageIndex(0); }); - unawaited(selectCamera(0, true, false)); + unawaited(selectCamera(0, true)); unawaited(initAsync()); } @@ -111,16 +112,11 @@ class HomeViewState extends State { super.dispose(); } - Future selectCamera( - int sCameraId, - bool init, - bool enableAudio, - ) async { + Future selectCamera(int sCameraId, bool init) async { final opts = await initializeCameraController( selectedCameraDetails, sCameraId, init, - enableAudio, ); if (opts != null) { selectedCameraDetails = opts.$1; @@ -140,7 +136,7 @@ class HomeViewState extends State { } await cameraController!.dispose(); cameraController = null; - await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); } Future initAsync() async { @@ -152,97 +148,111 @@ class HomeViewState extends State { globalUpdateOfHomeViewPageIndex(0); } } + + final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); + if (draftMedia != null) { + final service = await MediaFileService.fromMedia(draftMedia); + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageEditorView( + mediaFileService: service, + sharedFromGallery: true, + ), + ), + ); + } } @override Widget build(BuildContext context) { - return PieCanvas( - theme: getPieCanvasTheme(context), - child: Scaffold( - body: GestureDetector( - onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, - child: Stack( - children: [ - HomeViewCameraPreview( - controller: cameraController, - screenshotController: screenshotController, - ), - Shade( - opacity: offsetRatio, - ), - NotificationListener( - onNotification: onPageView, - child: Positioned.fill( - child: PageView( - controller: homeViewPageController, - onPageChanged: (index) { - setState(() { - activePageIdx = index; - }); - }, - children: [ - const ChatListView(), - Container(), - const MemoriesView(), - ], - ), + return Scaffold( + body: GestureDetector( + onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, + child: Stack( + children: [ + HomeViewCameraPreview( + controller: cameraController, + screenshotController: screenshotController, + ), + Shade( + opacity: offsetRatio, + ), + NotificationListener( + onNotification: onPageView, + child: Positioned.fill( + child: PageView( + controller: homeViewPageController, + onPageChanged: (index) { + setState(() { + activePageIdx = index; + }); + }, + children: [ + const ChatListView(), + Container(), + const MemoriesView(), + ], ), ), - Positioned( - left: 0, - top: 0, - right: 0, - bottom: (offsetRatio > 0.25) - ? MediaQuery.sizeOf(context).height * 2 - : 0, - child: Opacity( - opacity: 1 - (offsetRatio * 4) % 1, - child: CameraPreviewControllerView( - cameraController: cameraController, - screenshotController: screenshotController, - selectedCameraDetails: selectedCameraDetails, - selectCamera: selectCamera, - ), + ), + Positioned( + left: 0, + top: 0, + right: 0, + bottom: (offsetRatio > 0.25) + ? MediaQuery.sizeOf(context).height * 2 + : 0, + child: Opacity( + opacity: 1 - (offsetRatio * 4) % 1, + child: CameraPreviewControllerView( + cameraController: cameraController, + screenshotController: screenshotController, + selectedCameraDetails: selectedCameraDetails, + selectCamera: selectCamera, + isVisible: + ((1 - (offsetRatio * 4) % 1) == 1) && activePageIdx == 1, ), ), - ], - ), - ), - bottomNavigationBar: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - unselectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), - ), - selectedIconTheme: IconThemeData( - color: Theme.of(context).colorScheme.inverseSurface, - ), - items: const [ - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.solidComments), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.camera), - label: '', - ), - BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.photoFilm), - label: '', ), ], - onTap: (int index) async { - activePageIdx = index; - await homeViewPageController.animateToPage( - index, - duration: const Duration(milliseconds: 100), - curve: Curves.bounceIn, - ); - if (mounted) setState(() {}); - }, - currentIndex: activePageIdx, ), ), + bottomNavigationBar: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + unselectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), + ), + selectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.inverseSurface, + ), + items: const [ + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.solidComments), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.camera), + label: '', + ), + BottomNavigationBarItem( + icon: FaIcon(FontAwesomeIcons.photoFilm), + label: '', + ), + ], + onTap: (int index) async { + activePageIdx = index; + await homeViewPageController.animateToPage( + index, + duration: const Duration(milliseconds: 100), + curve: Curves.bounceIn, + ); + if (mounted) setState(() {}); + }, + currentIndex: activePageIdx, + ), ); } } diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 91702c6..e44f48d 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; @@ -24,7 +23,7 @@ class MemoriesViewState extends State { List galleryItems = []; Map> orderedByMonth = {}; List months = []; - StreamSubscription>? messageSub; + StreamSubscription>? messageSub; @override void initState() { @@ -38,76 +37,44 @@ class MemoriesViewState extends State { super.dispose(); } - Future> loadMemoriesDirectory() async { - final directoryPath = await send.getMediaBaseFilePath('memories'); - final directory = Directory(directoryPath); - - final items = []; - if (directory.existsSync()) { - final files = directory.listSync(); - - for (final file in files) { - if (file is File) { - final fileName = file.uri.pathSegments.last; - File? imagePath; - File? videoPath; - late File thumbnailFile; - if (fileName.contains('.thumbnail.')) { - continue; - } - if (fileName.contains('.png')) { - imagePath = file; - thumbnailFile = file; - // if (!await thumbnailFile.exists()) { - // await createThumbnailsForImage(imagePath); - // } - } else if (fileName.contains('.mp4')) { - videoPath = file; - thumbnailFile = getThumbnailPath(videoPath); - if (!thumbnailFile.existsSync()) { - await createThumbnailsForVideo(videoPath); - } - } else { - break; - } - final creationDate = file.lastModifiedSync(); - items.add( - MemoryItem( - id: int.parse(fileName.split('.')[0]), - messages: [], - date: creationDate, - mirrorVideo: false, - thumbnailPath: thumbnailFile, - imagePath: imagePath, - videoPath: videoPath, - ), - ); - } - } - } - return items; - } - Future initAsync() async { await messageSub?.cancel(); - final msgStream = twonlyDB.messagesDao.getAllStoredMediaFiles(); + final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles(); - messageSub = msgStream.listen((msgs) async { - final items = await MemoryItem.convertFromMessages(msgs); + messageSub = msgStream.listen((mediaFiles) async { // Group items by month orderedByMonth = {}; months = []; var lastMonth = ''; - galleryItems = await loadMemoriesDirectory(); - for (final item in galleryItems) { - items.remove( - item.id, - ); // prefer the stored one and not the saved on in the chat.... + galleryItems = []; + final applicationSupportDirectory = + await getApplicationSupportDirectory(); + for (final mediaFile in mediaFiles) { + final mediaService = MediaFileService( + mediaFile, + applicationSupportDirectory: applicationSupportDirectory, + ); + if (!mediaService.imagePreviewAvailable) continue; + if (mediaService.mediaFile.type == MediaType.video) { + if (!mediaService.thumbnailPath.existsSync()) { + await mediaService.createThumbnail(); + } + } + galleryItems.add( + MemoryItem( + mediaService: mediaService, + messages: [], + ), + ); } - galleryItems += items.values.toList(); - galleryItems.sort((a, b) => b.date.compareTo(a.date)); + galleryItems.sort( + (a, b) => b.mediaService.mediaFile.createdAt.compareTo( + a.mediaService.mediaFile.createdAt, + ), + ); for (var i = 0; i < galleryItems.length; i++) { - final month = DateFormat('MMMM yyyy').format(galleryItems[i].date); + final month = DateFormat('MMMM yyyy') + .format(galleryItems[i].mediaService.mediaFile.createdAt); if (lastMonth != month) { lastMonth = month; months.add(month); @@ -185,7 +152,5 @@ class MemoriesViewState extends State { ), ) as bool?; setState(() {}); - - await initAsync(); } } diff --git a/lib/src/views/memories/memories_item_thumbnail.dart b/lib/src/views/memories/memories_item_thumbnail.dart index efcd0f6..c1486bf 100644 --- a/lib/src/views/memories/memories_item_thumbnail.dart +++ b/lib/src/views/memories/memories_item_thumbnail.dart @@ -1,5 +1,6 @@ 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/model/memory_item.model.dart'; class MemoriesItemThumbnail extends StatefulWidget { @@ -19,9 +20,19 @@ class MemoriesItemThumbnail extends StatefulWidget { class _MemoriesItemThumbnailState extends State { @override void initState() { + initAsync(); super.initState(); } + Future initAsync() async { + if (!widget.galleryItem.mediaService.thumbnailPath.existsSync()) { + if (widget.galleryItem.mediaService.storedPath.existsSync()) { + await widget.galleryItem.mediaService.createThumbnail(); + if (mounted) setState(() {}); + } + } + } + @override void dispose() { super.dispose(); @@ -36,14 +47,23 @@ class _MemoriesItemThumbnailState extends State { @override Widget build(BuildContext context) { + final media = widget.galleryItem.mediaService; return GestureDetector( onTap: widget.onTap, child: Hero( - tag: widget.galleryItem.id.toString(), + tag: media.mediaFile.mediaId, child: Stack( children: [ - Image.file(widget.galleryItem.thumbnailPath), - if (widget.galleryItem.videoPath != null) + if (media.thumbnailPath.existsSync()) + Image.file(media.thumbnailPath) + else if (media.storedPath.existsSync() && + media.mediaFile.type == MediaType.image || + media.mediaFile.type == MediaType.gif) + Image.file(media.storedPath) + else + const Text('Media file removed.'), + if (widget.galleryItem.mediaService.mediaFile.type == + MediaType.video) const Positioned.fill( child: Center( child: FaIcon(FontAwesomeIcons.circlePlay), diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index a1c0103..b2016d5 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -1,13 +1,12 @@ -import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; -import 'package:twonly/src/services/api/media_upload.dart' as send; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; @@ -50,7 +49,6 @@ class _MemoriesPhotoSliderViewState extends State { } Future deleteFile() async { - final messages = widget.galleryItems[currentIndex].messages; final confirmed = await showAlertDialog( context, context.lang.deleteImageTitle, @@ -59,32 +57,27 @@ class _MemoriesPhotoSliderViewState extends State { if (!confirmed) return; - widget.galleryItems[currentIndex].imagePath?.deleteSync(); - widget.galleryItems[currentIndex].videoPath?.deleteSync(); - for (final message in messages) { - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion(mediaStored: Value(false)), - ); - } + widget.galleryItems[currentIndex].mediaService.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile( + widget.galleryItems[currentIndex].mediaService.mediaFile.mediaId, + ); widget.galleryItems.removeAt(currentIndex); setState(() {}); - await send.purgeSendMediaFiles(); - await received.purgeReceivedMediaFiles(); if (mounted) { Navigator.pop(context, true); } } Future exportFile() async { - final item = widget.galleryItems[currentIndex]; + final item = widget.galleryItems[currentIndex].mediaService; try { - if (item.videoPath != null) { - await saveVideoToGallery(item.videoPath!.path); - } else if (item.imagePath != null) { - final imageBytes = await item.imagePath!.readAsBytes(); + if (item.mediaFile.type == MediaType.video) { + await saveVideoToGallery(item.storedPath.path); + } else if (item.mediaFile.type == MediaType.image || + item.mediaFile.type == MediaType.gif) { + final imageBytes = await item.storedPath.readAsBytes(); await saveImageToGallery(imageBytes); } if (!mounted) return; @@ -127,18 +120,29 @@ class _MemoriesPhotoSliderViewState extends State { FilledButton.icon( icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { + final orgMediaService = + widget.galleryItems[currentIndex].mediaService; + + final newMediaService = await initializeMediaUpload( + orgMediaService.mediaFile.type, + gUser.defaultShowTime, + ); + if (newMediaService == null) { + Log.error('Could not create new mediaFIle'); + return; + } + + orgMediaService.storedPath + .copySync(newMediaService.tempPath.path); + + if (!context.mounted) return; + await Navigator.push( context, MaterialPageRoute( builder: (context) => ShareImageEditorView( - videoFilePath: - widget.galleryItems[currentIndex].videoPath, - imageBytes: widget - .galleryItems[currentIndex].imagePath - ?.readAsBytes(), - mirrorVideo: false, + mediaFileService: newMediaService, sharedFromGallery: true, - useHighQuality: true, ), ), ); @@ -213,24 +217,27 @@ class _MemoriesPhotoSliderViewState extends State { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { final item = widget.galleryItems[index]; - return item.videoPath != null + return item.mediaService.mediaFile.type == MediaType.video ? PhotoViewGalleryPageOptions.customChild( child: VideoPlayerWrapper( - videoPath: item.videoPath!, - mirrorVideo: item.mirrorVideo, + videoPath: item.mediaService.storedPath, ), // childSize: const Size(300, 300), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId, + ), ) : PhotoViewGalleryPageOptions( - imageProvider: FileImage(item.imagePath!), + imageProvider: FileImage(item.mediaService.storedPath), initialScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 4.1, - heroAttributes: PhotoViewHeroAttributes(tag: item.id), + heroAttributes: PhotoViewHeroAttributes( + tag: item.mediaService.mediaFile.mediaId, + ), ); } } diff --git a/lib/src/views/onboarding/recover.view.dart b/lib/src/views/onboarding/recover.view.dart index 4590aef..e9b061c 100644 --- a/lib/src/views/onboarding/recover.view.dart +++ b/lib/src/views/onboarding/recover.view.dart @@ -60,13 +60,13 @@ class _BackupRecoveryViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('twonly Safe ${context.lang.twonlySafeRecoverTitle}'), + title: Text('twonly Backup ${context.lang.twonlySafeRecoverTitle}'), actions: [ IconButton( onPressed: () async { await showAlertDialog( context, - 'twonly Safe', + 'twonly Backup', context.lang.backupTwonlySafeLongDesc, ); }, diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 84edc97..67b6f1c 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -13,14 +13,21 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/onboarding/recover.view.dart'; class RegisterView extends StatefulWidget { - const RegisterView({required this.callbackOnSuccess, super.key}); + const RegisterView({ + required this.callbackOnSuccess, + required this.proofOfWork, + super.key, + }); final Function callbackOnSuccess; + final (Future?, bool) proofOfWork; @override State createState() => _RegisterViewState(); } @@ -29,10 +36,20 @@ class _RegisterViewState extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController inviteCodeController = TextEditingController(); + bool _registrationDisabled = false; bool _isTryingToRegister = false; bool _isValidUserName = false; bool _showUserNameError = false; + late Future? proofOfWork; + + @override + void initState() { + proofOfWork = widget.proofOfWork.$1; + _registrationDisabled = widget.proofOfWork.$2; + super.initState(); + } + Future createNewUser() async { if (!_isValidUserName) { setState(() { @@ -48,20 +65,48 @@ class _RegisterViewState extends State { _showUserNameError = false; }); + late int proof; + + if (proofOfWork != null) { + proof = await proofOfWork!; + } else { + final (pow, registrationDisabled) = await apiService.getProofOfWork(); + if (pow == null) { + _registrationDisabled = registrationDisabled; + if (mounted) { + showNetworkIssue(context); + } + return; + // Starting with the proof of work. + } + proof = await calculatePoW(pow.prefix, pow.difficulty.toInt()); + } + + Log.info('The result of the POW is $proof'); + await createIfNotExistsSignalIdentity(); var userId = 0; - final res = await apiService.register(username, inviteCode); + final res = await apiService.register(username, inviteCode, proof); if (res.isSuccess) { Log.info('Got user_id ${res.value} from server'); userId = res.value.userid.toInt() as int; } else { + if (res.error == ErrorCode.RegistrationDisabled) { + _registrationDisabled = true; + return; + } if (res.error == ErrorCode.UserIdAlreadyTaken) { Log.error('User ID already token. Tying again.'); await deleteLocalUserData(); return createNewUser(); } + if (res.error == ErrorCode.InvalidProofOfWork) { + Log.error('Proof of Work is invalid. Try again.'); + await deleteLocalUserData(); + return createNewUser(); + } if (mounted) { setState(() { _isTryingToRegister = false; @@ -84,18 +129,56 @@ class _RegisterViewState extends State { username: username, displayName: username, subscriptionPlan: 'Preview', - isDemoUser: false, - ); + )..appVersion = 62; await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); + gUser = userData; + await apiService.authenticate(); widget.callbackOnSuccess(); } @override Widget build(BuildContext context) { + if (_registrationDisabled) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView( + children: [ + const SizedBox(height: 50), + Text( + context.lang.registerTitle, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 30), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Text( + context.lang.registerSlogan, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(height: 130), + Text( + context.lang.registrationClosed, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.red, + ), + ), + ], + ), + ), + ), + ); + } + InputDecoration getInputDecoration(String hintText) { return InputDecoration(hintText: hintText, fillColor: Colors.grey[400]); } @@ -165,30 +248,6 @@ class _RegisterViewState extends State { ), textAlign: TextAlign.center, ), - // const SizedBox(height: 5), - // Center( - // child: Padding( - // padding: EdgeInsets.only(left: 10, right: 10), - // child: Text( - // context.lang.registerUsernameLimits, - // textAlign: TextAlign.center, - // style: const TextStyle(fontSize: 9), - // ), - // ), - // ), - // const SizedBox(height: 30), - // Center( - // child: Text( - // context.lang.registerTwonlyCodeText, - // textAlign: TextAlign.center, - // ), - // ), - // const SizedBox(height: 10), - // TextField( - // controller: inviteCodeController, - // decoration: - // getInputDecoration(context.lang.registerTwonlyCodeLabel), - // ), const SizedBox(height: 30), Column( children: [ diff --git a/lib/src/views/settings/account.view.dart b/lib/src/views/settings/account.view.dart index 37af702..d56e734 100644 --- a/lib/src/views/settings/account.view.dart +++ b/lib/src/views/settings/account.view.dart @@ -37,7 +37,7 @@ class _AccountViewState extends State { .where( (x) => x.transactionType != Response_TransactionTypes.ThanksForTesting || - kDebugMode, + !kReleaseMode, ) .map((a) => a.depositCents.toInt()) .sum; @@ -101,7 +101,7 @@ class _AccountViewState extends State { ), ) : Text(context.lang.settingsAccountDeleteAccountNoBallance), - onLongPress: kDebugMode + onLongPress: !kReleaseMode ? () async { await deleteLocalUserData(); await Restart.restartApp( diff --git a/lib/src/views/settings/backup/backup.view.dart b/lib/src/views/settings/backup/backup.view.dart index 87daaab..1720925 100644 --- a/lib/src/views/settings/backup/backup.view.dart +++ b/lib/src/views/settings/backup/backup.view.dart @@ -1,13 +1,10 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; -import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; void Function() gUpdateBackupView = () {}; @@ -26,8 +23,6 @@ BackupServer defaultBackupServer = BackupServer( ); class _BackupViewState extends State { - TwonlySafeBackup? twonlySafeBackup; - BackupServer backupServer = defaultBackupServer; bool isLoading = false; int activePageIdx = 0; @@ -48,12 +43,6 @@ class _BackupViewState extends State { } Future initAsync() async { - final user = await getUser(); - twonlySafeBackup = user?.twonlySafeBackup; - backupServer = defaultBackupServer; - if (user?.backupServer != null) { - backupServer = user!.backupServer!; - } setState(() {}); } @@ -70,8 +59,25 @@ class _BackupViewState extends State { } } + Future changeTwonlySafePassword() async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const TwonlyIdentityBackupView( + isPasswordChangeOnly: true, + ); + }, + ), + ); + setState(() { + // gUser was updated + }); + } + @override Widget build(BuildContext context) { + final backupServer = gUser.backupServer ?? defaultBackupServer; return Scaffold( appBar: AppBar( title: Text(context.lang.settingsBackup), @@ -85,10 +91,13 @@ class _BackupViewState extends State { }, children: [ BackupOption( - title: 'twonly Safe', + title: 'twonly Backup', description: context.lang.backupTwonlySafeDesc, - autoBackupEnabled: twonlySafeBackup != null, - child: (twonlySafeBackup == null) + bottomButton: FilledButton( + onPressed: changeTwonlySafePassword, + child: Text(context.lang.backupChangePassword), + ), + child: (gUser.twonlySafeBackup == null) ? null : Column( children: [ @@ -116,16 +125,20 @@ class _BackupViewState extends State { context.lang.backupLastBackupDate, formatDateTime( context, - twonlySafeBackup!.lastBackupDone, + gUser.twonlySafeBackup!.lastBackupDone, ) ), ( context.lang.backupLastBackupSize, - formatBytes(twonlySafeBackup!.lastBackupSize) + formatBytes( + gUser.twonlySafeBackup!.lastBackupSize, + ) ), ( context.lang.backupLastBackupResult, - backupStatus(twonlySafeBackup!.backupUploadState) + backupStatus( + gUser.twonlySafeBackup!.backupUploadState, + ) ), ].map((pair) { return TableRow( @@ -136,8 +149,9 @@ class _BackupViewState extends State { ), TableCell( child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: 4, + ), child: Text( pair.$2, textAlign: TextAlign.right, @@ -150,7 +164,7 @@ class _BackupViewState extends State { ], ), const SizedBox(height: 10), - FilledButton( + OutlinedButton( onPressed: isLoading ? null : () async { @@ -166,37 +180,10 @@ class _BackupViewState extends State { ), ], ), - onTap: () async { - if (twonlySafeBackup != null) { - final disable = await showAlertDialog( - context, - context.lang.deleteBackupTitle, - context.lang.deleteBackupBody, - ); - if (disable) { - await disableTwonlySafe(); - } - } else { - setState(() { - isLoading = true; - }); - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const TwonlyIdentityBackupView(); - }, - ), - ); - } - await initAsync(); - }, ), BackupOption( title: '${context.lang.backupData} (Coming Soon)', description: context.lang.backupDataDesc, - autoBackupEnabled: false, - onTap: null, ), ], ), @@ -211,10 +198,10 @@ class _BackupViewState extends State { items: [ const BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.vault, size: 17), - label: 'twonly Safe', + label: 'twonly Backup', ), BottomNavigationBarItem( - icon: const FaIcon(FontAwesomeIcons.boxArchive, size: 17), + icon: const FaIcon(Icons.archive_outlined, size: 17), label: context.lang.backupData, ), ], @@ -238,51 +225,35 @@ class BackupOption extends StatelessWidget { const BackupOption({ required this.title, required this.description, - required this.autoBackupEnabled, - required this.onTap, + this.bottomButton, super.key, this.child, }); final String title; final String description; final Widget? child; - final bool autoBackupEnabled; - final void Function()? onTap; + final Widget? bottomButton; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: autoBackupEnabled ? null : onTap, - child: Card( - margin: const EdgeInsets.all(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: - const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text(description), - const SizedBox(height: 8), - if (child != null) child! else Container(), - Expanded(child: Container()), - Center( - child: autoBackupEnabled - ? OutlinedButton( - onPressed: onTap, - child: Text(context.lang.disable), - ) - : FilledButton( - onPressed: onTap, - child: Text(context.lang.enable), - ), - ), - ], - ), + return Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text(description), + const SizedBox(height: 8), + if (child != null) child! else Container(), + Expanded(child: Container()), + if (bottomButton != null) Center(child: bottomButton), + ], ), ), ); diff --git a/lib/src/views/settings/backup/twonly_safe_backup.view.dart b/lib/src/views/settings/backup/twonly_safe_backup.view.dart index 7d7915f..c5255d1 100644 --- a/lib/src/views/settings/backup/twonly_safe_backup.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_backup.view.dart @@ -8,7 +8,16 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_server.view.dart'; class TwonlyIdentityBackupView extends StatefulWidget { - const TwonlyIdentityBackupView({super.key}); + const TwonlyIdentityBackupView({ + this.isPasswordChangeOnly = false, + this.callBack, + super.key, + }); + + // in case a callback is defined the callback + // is called instead of the Navigator.pop() + final VoidCallback? callBack; + final bool isPasswordChangeOnly; @override State createState() => @@ -56,148 +65,165 @@ class _TwonlyIdentityBackupViewState extends State { isLoading = false; }); - Navigator.pop(context); + if (widget.callBack != null) { + widget.callBack!(); + } else { + Navigator.pop(context); + } } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('twonly Safe'), - actions: [ - IconButton( - onPressed: () async { - await showAlertDialog( - context, - 'twonly Safe', - context.lang.backupTwonlySafeLongDesc, - ); - }, - icon: const FaIcon(FontAwesomeIcons.circleInfo), - iconSize: 18, - ), - ], - ), - body: Padding( - padding: - const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), - child: ListView( - children: [ - Text( - context.lang.backupSelectStrongPassword, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Stack( - children: [ - TextField( - controller: passwordCtrl, - onChanged: (value) { - setState(() {}); - }, - style: const TextStyle(fontSize: 17), - obscureText: obscureText, - decoration: getInputDecoration( - context, - context.lang.password, - ), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: IconButton( - onPressed: () { - setState(() { - obscureText = !obscureText; - }); - }, - icon: FaIcon( - obscureText - ? FontAwesomeIcons.eye - : FontAwesomeIcons.eyeSlash, - size: 16, - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsetsGeometry.all(5), - child: Text( - context.lang.backupPasswordRequirement, - style: TextStyle( - fontSize: 13, - color: (passwordCtrl.text.length < 8 && - passwordCtrl.text.isNotEmpty) - ? Colors.red - : Colors.transparent, - ), - ), - ), - const SizedBox(height: 5), - TextField( - controller: repeatedPasswordCtrl, - onChanged: (value) { - setState(() {}); + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + appBar: AppBar( + title: const Text('twonly Backup'), + actions: [ + IconButton( + onPressed: () async { + await showAlertDialog( + context, + 'twonly Backup', + context.lang.backupTwonlySafeLongDesc, + ); }, - style: const TextStyle(fontSize: 17), - obscureText: true, - decoration: getInputDecoration( - context, - context.lang.passwordRepeated, - ), - ), - Padding( - padding: const EdgeInsetsGeometry.all(5), - child: Text( - context.lang.passwordRepeatedNotEqual, - style: TextStyle( - fontSize: 13, - color: (passwordCtrl.text != repeatedPasswordCtrl.text && - repeatedPasswordCtrl.text.isNotEmpty) - ? Colors.red - : Colors.transparent, - ), - ), - ), - const SizedBox(height: 10), - Center( - child: OutlinedButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const TwonlySafeServerView(); - }, - ), - ); - }, - child: Text(context.lang.backupExpertSettings), - ), - ), - const SizedBox(height: 10), - Center( - child: FilledButton.icon( - onPressed: (!isLoading && - (passwordCtrl.text == repeatedPasswordCtrl.text && - passwordCtrl.text.length >= 8 || - kDebugMode)) - ? onPressedEnableTwonlySafe - : null, - icon: isLoading - ? const SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator(strokeWidth: 1), - ) - : const Icon(Icons.lock_clock_rounded), - label: Text(context.lang.backupEnableBackup), - ), + icon: const FaIcon(FontAwesomeIcons.circleInfo), + iconSize: 18, ), ], ), + body: Padding( + padding: + const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), + child: ListView( + children: [ + Text( + context.lang.backupSelectStrongPassword, + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Stack( + children: [ + TextField( + controller: passwordCtrl, + onChanged: (value) { + setState(() {}); + }, + style: const TextStyle(fontSize: 17), + obscureText: obscureText, + decoration: getInputDecoration( + context, + context.lang.password, + ), + ), + Positioned( + right: 0, + top: 0, + bottom: 0, + child: IconButton( + onPressed: () { + setState(() { + obscureText = !obscureText; + }); + }, + icon: FaIcon( + obscureText + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 16, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsetsGeometry.all(5), + child: Text( + context.lang.backupPasswordRequirement, + style: TextStyle( + fontSize: 13, + color: (passwordCtrl.text.length < 8 && + passwordCtrl.text.isNotEmpty) + ? Colors.red + : Colors.transparent, + ), + ), + ), + const SizedBox(height: 5), + TextField( + controller: repeatedPasswordCtrl, + onChanged: (value) { + setState(() {}); + }, + style: const TextStyle(fontSize: 17), + obscureText: true, + decoration: getInputDecoration( + context, + context.lang.passwordRepeated, + ), + ), + Padding( + padding: const EdgeInsetsGeometry.all(5), + child: Text( + context.lang.passwordRepeatedNotEqual, + style: TextStyle( + fontSize: 13, + color: (passwordCtrl.text != repeatedPasswordCtrl.text && + repeatedPasswordCtrl.text.isNotEmpty) + ? Colors.red + : Colors.transparent, + ), + ), + ), + const SizedBox(height: 10), + Center( + child: OutlinedButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const TwonlySafeServerView(); + }, + ), + ); + }, + child: Text(context.lang.backupExpertSettings), + ), + ), + const SizedBox(height: 10), + Text( + context.lang.backupNoPasswordRecovery, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + const SizedBox(height: 10), + Center( + child: FilledButton.icon( + onPressed: (!isLoading && + (passwordCtrl.text == repeatedPasswordCtrl.text && + passwordCtrl.text.length >= 8 || + !kReleaseMode)) + ? onPressedEnableTwonlySafe + : null, + icon: isLoading + ? const SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator(strokeWidth: 1), + ) + : const Icon(Icons.lock_clock_rounded), + label: Text( + widget.isPasswordChangeOnly + ? context.lang.backupChangePassword + : context.lang.backupEnableBackup, + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/src/views/settings/backup/twonly_safe_server.view.dart b/lib/src/views/settings/backup/twonly_safe_server.view.dart index 973f87a..c0e4bfd 100644 --- a/lib/src/views/settings/backup/twonly_safe_server.view.dart +++ b/lib/src/views/settings/backup/twonly_safe_server.view.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:http/http.dart' as http; +import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -30,9 +31,8 @@ class _TwonlySafeServerViewState extends State { } Future initAsync() async { - final user = await getUser(); - if (user?.backupServer != null) { - final uri = Uri.parse(user!.backupServer!.serverUrl); + if (gUser.backupServer != null) { + final uri = Uri.parse(gUser.backupServer!.serverUrl); // remove user auth data final serverUrl = Uri( scheme: uri.scheme, @@ -107,7 +107,7 @@ class _TwonlySafeServerViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('twonly Safe Server'), + title: const Text('twonly Backup Server'), ), body: Padding( padding: const EdgeInsets.all(40), diff --git a/lib/src/views/settings/data_and_storage.view.dart b/lib/src/views/settings/data_and_storage.view.dart index 4c21c75..5f5b964 100644 --- a/lib/src/views/settings/data_and_storage.view.dart +++ b/lib/src/views/settings/data_and_storage.view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/views/settings/developer/automated_testing.view.dart b/lib/src/views/settings/developer/automated_testing.view.dart new file mode 100644 index 0000000..02a7b9d --- /dev/null +++ b/lib/src/views/settings/developer/automated_testing.view.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class AutomatedTestingView extends StatefulWidget { + const AutomatedTestingView({super.key}); + + @override + State createState() => _AutomatedTestingViewState(); +} + +class _AutomatedTestingViewState extends State { + String lotsOfMessagesStatus = ''; + @override + void initState() { + super.initState(); + unawaited(initAsync()); + } + + Future initAsync() async {} + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Automated Testing'), + ), + body: ListView( + children: [ + if (!kReleaseMode) + ListTile( + title: const Text('Sending a lot of messages.'), + subtitle: Text(lotsOfMessagesStatus), + onTap: () async { + final username = await showUserNameDialog(context); + if (username == null) return; + Log.info('Requested to send to $username'); + + final contacts = await twonlyDB.contactsDao + .getContactsByUsername(username.toLowerCase()); + + for (final contact in contacts) { + Log.info('Sending to ${contact.username}'); + final group = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + for (var i = 0; i < 200; i++) { + setState(() { + lotsOfMessagesStatus = + 'At message $i to ${contact.username}.'; + }); + await insertAndSendTextMessage( + group!.groupId, + 'Message $i.', + null, + ); + } + } + }, + ), + ], + ), + ); + } +} + +Future showUserNameDialog( + BuildContext context, +) async { + final controller = TextEditingController(); + + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Username'), + content: TextField( + controller: controller, + autofocus: true, + ), + actions: [ + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + TextButton( + child: Text(context.lang.ok), + onPressed: () { + Navigator.of(context) + .pop(controller.text); // Return the input text + }, + ), + ], + ); + }, + ); + return controller.text; +} diff --git a/lib/src/views/settings/developer/developer.view.dart b/lib/src/views/settings/developer/developer.view.dart index c2cd49c..42e4aa3 100644 --- a/lib/src/views/settings/developer/developer.view.dart +++ b/lib/src/views/settings/developer/developer.view.dart @@ -1,10 +1,8 @@ import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; class DeveloperSettingsView extends StatefulWidget { @@ -68,12 +66,26 @@ class _DeveloperSettingsViewState extends State { ); }, ), - if (kDebugMode) + // if (!kReleaseMode) + // ListTile( + // title: const Text('FlameSync Test'), + // onTap: () async { + // await twonlyDB.contactsDao.modifyFlameCounterForTesting(); + // await syncFlameCounters(); + // }, + // ), + if (!kReleaseMode) ListTile( - title: const Text('FlameSync Test'), + title: const Text('Automated Testing'), onTap: () async { - await twonlyDB.contactsDao.modifyFlameCounterForTesting(); - await syncFlameCounters(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AutomatedTestingView(); + }, + ), + ); }, ), ], diff --git a/lib/src/views/settings/developer/retransmission_data.view.dart b/lib/src/views/settings/developer/retransmission_data.view.dart index 34538fc..7beca18 100644 --- a/lib/src/views/settings/developer/retransmission_data.view.dart +++ b/lib/src/views/settings/developer/retransmission_data.view.dart @@ -1,14 +1,14 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' + as pb; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; class RetransmissionDataView extends StatefulWidget { const RetransmissionDataView({super.key}); @@ -19,33 +19,22 @@ class RetransmissionDataView extends StatefulWidget { class RetransMsg { RetransMsg({ - required this.json, - required this.retrans, + required this.receipt, required this.contact, }); - final MessageJson json; - final MessageRetransmission retrans; + final Receipt receipt; final Contact? contact; static List fromRaw( - List retrans, + List receipts, Map contacts, ) { final res = []; - - for (final retrans in retrans) { - final json = MessageJson.fromJson( - jsonDecode( - utf8.decode( - gzip.decode(retrans.plaintextContent), - ), - ) as Map, - ); + for (final receipt in receipts) { res.add( RetransMsg( - json: json, - retrans: retrans, - contact: contacts[retrans.contactId], + receipt: receipt, + contact: contacts[receipt.contactId], ), ); } @@ -54,9 +43,9 @@ class RetransMsg { } class _RetransmissionDataViewState extends State { - List retransmissions = []; + List retransmissions = []; Map contacts = {}; - StreamSubscription>? subscriptionRetransmission; + StreamSubscription>? subscriptionRetransmission; StreamSubscription>? subscriptionContacts; List messages = []; @@ -85,7 +74,7 @@ class _RetransmissionDataViewState extends State { setState(() {}); }); subscriptionRetransmission = - twonlyDB.messageRetransmissionDao.watchAllMessages().listen((updated) { + twonlyDB.receiptsDao.watchAll().listen((updated) { retransmissions = updated; if (contacts.isNotEmpty) { messages = RetransMsg.fromRaw(retransmissions, contacts); @@ -108,7 +97,7 @@ class _RetransmissionDataViewState extends State { .map( (retrans) => ListTile( title: Text( - '${retrans.retrans.retransmissionId}: ${retrans.json.kind}', + retrans.receipt.receiptId, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -117,63 +106,49 @@ class _RetransmissionDataViewState extends State { 'To ${retrans.contact?.username}', ), Text( - 'Server-Ack: ${retrans.retrans.acknowledgeByServerAt}', + 'Server-Ack: ${retrans.receipt.ackByServerAt}', ), + if (retrans.receipt.messageId != null) + Text( + 'MessageId: ${retrans.receipt.messageId}', + ), + if (retrans.receipt.messageId != null) + FutureBuilder( + future: getPushNotificationFromEncryptedContent( + retrans.receipt.contactId, + retrans.receipt.messageId, + pb.EncryptedContent.fromBuffer( + pb.Message.fromBuffer(retrans.receipt.message) + .encryptedContent, + ), + ), + builder: (d, a) { + if (!a.hasData) return Container(); + return Text( + 'PushKind: ${a.data?.kind}', + ); + }, + ), Text( - 'Retry: ${retrans.retrans.retryCount} : ${retrans.retrans.lastRetry}', + 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', ), ], ), - trailing: SizedBox( - width: 80, - child: Row( - children: [ - SizedBox( - height: 20, - width: 40, - child: Center( - child: GestureDetector( - onDoubleTap: () async { - await twonlyDB.messageRetransmissionDao - .deleteRetransmissionById( - retrans.retrans.retransmissionId, - ); - }, - child: const FaIcon( - FontAwesomeIcons.trash, - size: 15, - ), - ), - ), + trailing: FilledButton.icon( + onPressed: () async { + final newReceiptId = uuid.v4(); + await twonlyDB.receiptsDao.updateReceipt( + retrans.receipt.receiptId, + ReceiptsCompanion( + receiptId: Value(newReceiptId), + ackByServerAt: const Value(null), ), - SizedBox( - width: 40, - child: OutlinedButton( - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero, - ), - ), - onPressed: () async { - await twonlyDB.messageRetransmissionDao - .updateRetransmission( - retrans.retrans.retransmissionId, - const MessageRetransmissionsCompanion( - acknowledgeByServerAt: Value(null), - ), - ); - await sendRetransmitMessage( - retrans.retrans.retransmissionId, - ); - }, - child: const FaIcon( - FontAwesomeIcons.arrowRotateLeft, - size: 15, - ), - ), - ), - ], - ), + ); + await tryToSendCompleteMessage( + receiptId: newReceiptId, + ); + }, + label: const FaIcon(FontAwesomeIcons.arrowRotateRight), ), ), ) diff --git a/lib/src/views/settings/help/contact_us.view.dart b/lib/src/views/settings/help/contact_us.view.dart index 6d30400..52d644a 100644 --- a/lib/src/views/settings/help/contact_us.view.dart +++ b/lib/src/views/settings/help/contact_us.view.dart @@ -8,8 +8,6 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/services/api/media_upload.dart' - show createDownloadToken, uint8ListToHex; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/help/contact_us/submit_message.view.dart'; @@ -32,7 +30,7 @@ class _ContactUsState extends State { Future uploadDebugLog() async { if (debugLogDownloadToken != null) return debugLogDownloadToken; - final downloadToken = createDownloadToken(); + final downloadToken = getRandomUint8List(32); final debugLog = await loadLogFile(); diff --git a/lib/src/views/settings/notification.view.dart b/lib/src/views/settings/notification.view.dart index db9be48..49b4e72 100644 --- a/lib/src/views/settings/notification.view.dart +++ b/lib/src/views/settings/notification.view.dart @@ -1,13 +1,12 @@ import 'dart:io'; - -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; -import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; +import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -52,10 +51,10 @@ class NotificationView extends StatelessWidget { if (run) { final user = await getUser(); if (user != null) { - final pushData = await getPushData( + final pushData = await encryptPushNotification( user.userId, PushNotification( - messageId: Int64(), + messageId: uuid.v4(), kind: PushKind.testNotification, ), ); diff --git a/lib/src/views/settings/privacy_view_block.users.dart b/lib/src/views/settings/privacy_view_block.users.dart index ce1dd86..d80f581 100644 --- a/lib/src/views/settings/privacy_view_block.users.dart +++ b/lib/src/views/settings/privacy_view_block.users.dart @@ -1,12 +1,11 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; -import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; -import 'package:twonly/src/views/components/user_context_menu.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; +import 'package:twonly/src/views/components/user_context_menu.component.dart'; class PrivacyViewBlockUsers extends StatefulWidget { const PrivacyViewBlockUsers({super.key}); @@ -32,53 +31,50 @@ class _PrivacyViewBlockUsers extends State { appBar: AppBar( title: Text(context.lang.settingsPrivacyBlockUsers), ), - body: PieCanvas( - theme: getPieCanvasTheme(context), - child: Padding( - padding: - const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: (value) => setState(() { - filter = value; - }), - decoration: getInputDecoration( - context, - context.lang.searchUsernameInput, - ), + body: Padding( + padding: + const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: (value) => setState(() { + filter = value; + }), + decoration: getInputDecoration( + context, + context.lang.searchUsernameInput, ), ), - const SizedBox(height: 20), - Text( - context.lang.settingsPrivacyBlockUsersDesc, - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Expanded( - child: StreamBuilder( - stream: allUsers, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Container(); - } + ), + const SizedBox(height: 20), + Text( + context.lang.settingsPrivacyBlockUsersDesc, + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Expanded( + child: StreamBuilder( + stream: allUsers, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container(); + } - final filteredContacts = snapshot.data!.where((contact) { - return getContactDisplayName(contact) - .toLowerCase() - .contains(filter.toLowerCase()); - }).toList(); + final filteredContacts = snapshot.data!.where((contact) { + return getContactDisplayName(contact) + .toLowerCase() + .contains(filter.toLowerCase()); + }).toList(); - return UserList( - List.from(filteredContacts), - ); - }, - ), + return UserList( + List.from(filteredContacts), + ); + }, ), - ], - ), + ), + ], ), ), ); @@ -108,7 +104,7 @@ class UserList extends StatelessWidget { itemCount: users.length, itemBuilder: (BuildContext context, int i) { final user = users[i]; - return UserContextMenuBlocked( + return UserContextMenu( contact: user, child: ListTile( title: Row( @@ -116,7 +112,7 @@ class UserList extends StatelessWidget { Text(getContactDisplayName(user)), ], ), - leading: ContactAvatar(contact: user, fontSize: 15), + leading: AvatarIcon(contactId: user.userId, fontSize: 15), trailing: Checkbox( value: user.blocked, onChanged: (bool? value) async { diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 104bde0..5d24f45 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -1,13 +1,17 @@ import 'dart:async'; - import 'package:avatar_maker/avatar_maker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; +import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart'; class ProfileView extends StatefulWidget { @@ -18,19 +22,27 @@ class ProfileView extends StatefulWidget { } class _ProfileViewState extends State { - UserData? user; final AvatarMakerController _avatarMakerController = PersistentAvatarMakerController(customizedPropertyCategories: []); + int twonlyScore = 0; + late StreamSubscription twonlyScoreSub; + @override void initState() { + twonlyScoreSub = + twonlyDB.groupsDao.watchSumTotalMediaCounter().listen((update) { + setState(() { + twonlyScore = update; + }); + }); super.initState(); - unawaited(initAsync()); } - Future initAsync() async { - user = await getUser(); - setState(() {}); + @override + void dispose() { + twonlyScoreSub.cancel(); + super.dispose(); } Future updateUserDisplayName(String displayName) async { @@ -40,9 +52,42 @@ class _ProfileViewState extends State { ..avatarCounter = user.avatarCounter + 1; return user; }); - await notifyContactsAboutProfileChange(); - await initAsync(); + setState(() {}); // gUser has updated + } + + Future _updateUsername(String username) async { + final result = await apiService.changeUsername(username); + if (result.isError) { + if (!mounted) return; + + if (result.error == ErrorCode.UsernameAlreadyTaken) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.lang.errorUsernameAlreadyTaken), + duration: const Duration(seconds: 3), + ), + ); + return; + } + + showNetworkIssue(context); + + return; + } + + // as the username has changes, remove the old from the server and then upload it again. + await removeTwonlySafeFromServer(); + unawaited(performTwonlySafeBackup(force: true)); + + await updateUserdata((user) { + user + ..username = username + ..avatarCounter = user.avatarCounter + 1; + return user; + }); + await notifyContactsAboutProfileChange(); + setState(() {}); // gUser has updated } @override @@ -82,18 +127,61 @@ class _ProfileViewState extends State { ), const SizedBox(height: 20), const Divider(), + BetterListTile( + leading: const Padding( + padding: EdgeInsets.only(right: 5, left: 1), + child: FaIcon( + FontAwesomeIcons.at, + size: 20, + ), + ), + text: context.lang.registerUsernameDecoration, + subtitle: Text(gUser.username), + onTap: () async { + final username = await showDisplayNameChangeDialog( + context, + gUser.username, + context.lang.registerUsernameDecoration, + context.lang.registerUsernameDecoration, + maxLength: 12, + inputFormatters: [ + LengthLimitingTextInputFormatter(12), + FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')), + ], + ); + if (context.mounted && username != null && username != '') { + await _updateUsername(username); + } + }, + ), BetterListTile( icon: FontAwesomeIcons.userPen, text: context.lang.settingsProfileEditDisplayName, - subtitle: (user == null) ? null : Text(user!.displayName), + subtitle: Text(gUser.displayName), onTap: () async { - final displayName = - await showDisplayNameChangeDialog(context, user!.displayName); + final displayName = await showDisplayNameChangeDialog( + context, + gUser.displayName, + context.lang.settingsProfileEditDisplayName, + context.lang.settingsProfileEditDisplayNameNew, + maxLength: 30, + ); if (context.mounted && displayName != null && displayName != '') { await updateUserDisplayName(displayName); } }, ), + BetterListTile( + text: context.lang.yourTwonlyScore, + icon: FontAwesomeIcons.trophy, + trailing: Text( + twonlyScore.toString(), + style: TextStyle( + color: context.color.primary, + fontSize: 18, + ), + ), + ), ], ), ); @@ -103,33 +191,38 @@ class _ProfileViewState extends State { Future showDisplayNameChangeDialog( BuildContext context, String currentName, -) { + String title, + String hintText, { + List? inputFormatters, + int? maxLength, +}) { final controller = TextEditingController(text: currentName); return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: Text(context.lang.settingsProfileEditDisplayName), + title: Text(title), content: TextField( controller: controller, autofocus: true, + inputFormatters: inputFormatters, + maxLength: maxLength, decoration: InputDecoration( - hintText: context.lang.settingsProfileEditDisplayNameNew, + hintText: hintText, ), ), actions: [ TextButton( child: Text(context.lang.cancel), onPressed: () { - Navigator.of(context).pop(); // Close the dialog + Navigator.of(context).pop(); }, ), TextButton( child: Text(context.lang.ok), onPressed: () { - Navigator.of(context) - .pop(controller.text); // Return the input text + Navigator.of(context).pop(controller.text); }, ), ], diff --git a/lib/src/views/settings/settings_main.view.dart b/lib/src/views/settings/settings_main.view.dart index b1ffe1c..017fd36 100644 --- a/lib/src/views/settings/settings_main.view.dart +++ b/lib/src/views/settings/settings_main.view.dart @@ -1,12 +1,10 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; -import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/settings/account.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart'; @@ -28,247 +26,232 @@ class SettingsMainView extends StatefulWidget { } class _SettingsMainViewState extends State { - UserData? userData; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - userData = await getUser(); - setState(() {}); - } - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(context.lang.settingsTitle), ), - body: (userData == null) - ? null - : ListView( + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(30), + child: Row( children: [ - Padding( - padding: const EdgeInsets.all(30), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ProfileView(); - }, - ), - ); - await initAsync(); - }, - child: ColoredBox( - color: context.color.surface.withAlpha(0), - child: Row( - children: [ - ContactAvatar( - userData: userData, - fontSize: 30, - ), - Container(width: 20, color: Colors.transparent), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - userData!.displayName, - style: const TextStyle(fontSize: 20), - textAlign: TextAlign.left, - ), - Text( - userData!.username, - style: const TextStyle( - fontSize: 14, - ), - textAlign: TextAlign.left, - ), - ], - ), - ], - ), - ), - ), - ), - // Align( - // alignment: Alignment.centerRight, - // child: IconButton( - // onPressed: () {}, - // icon: FaIcon(FontAwesomeIcons.qrcode), - // ), - // ) - ], - ), - ), - BetterListTile( - icon: FontAwesomeIcons.user, - text: context.lang.settingsAccount, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const AccountView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.shieldHeart, - text: context.lang.settingsSubscription, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const SubscriptionView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: Icons.lock_clock_rounded, - text: context.lang.settingsBackup, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const BackupView(); - }, - ), - ); - }, - ), - const Divider(), - BetterListTile( - icon: FontAwesomeIcons.sun, - text: context.lang.settingsAppearance, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const AppearanceView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.comment, - text: context.lang.settingsChats, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ChatSettingsView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.lock, - text: context.lang.settingsPrivacy, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const PrivacyView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.bell, - text: context.lang.settingsNotification, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const NotificationView(); - }, - ), - ); - }, - ), - BetterListTile( - icon: FontAwesomeIcons.chartPie, - iconSize: 15, - text: context.lang.settingsStorageData, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const DataAndStorageView(); - }, - ), - ); - }, - ), - const Divider(), - BetterListTile( - icon: FontAwesomeIcons.circleQuestion, - text: context.lang.settingsHelp, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const HelpView(); - }, - ), - ); - }, - ), - if (userData != null && userData!.isDeveloper) - BetterListTile( - icon: FontAwesomeIcons.code, - text: 'Developer Settings', + Expanded( + child: GestureDetector( onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { - return const DeveloperSettingsView(); + return const ProfileView(); }, ), ); + setState(() {}); }, - ), - BetterListTile( - icon: FontAwesomeIcons.shareFromSquare, - text: context.lang.inviteFriends, - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const ShareWithFriendsView(); - }, + child: ColoredBox( + color: context.color.surface.withAlpha(0), + child: Row( + children: [ + const AvatarIcon( + myAvatar: true, + fontSize: 30, + ), + Container(width: 20, color: Colors.transparent), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + substringBy(gUser.displayName, 27), + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.left, + ), + Text( + gUser.username, + style: const TextStyle( + fontSize: 14, + ), + textAlign: TextAlign.left, + ), + ], + ), + ], ), - ); - }, + ), + ), ), + // Align( + // alignment: Alignment.centerRight, + // child: IconButton( + // onPressed: () {}, + // icon: FaIcon(FontAwesomeIcons.qrcode), + // ), + // ) ], ), + ), + BetterListTile( + icon: FontAwesomeIcons.user, + text: context.lang.settingsAccount, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AccountView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.shieldHeart, + text: context.lang.settingsSubscription, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: Icons.lock_clock_rounded, + text: context.lang.settingsBackup, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const BackupView(); + }, + ), + ); + }, + ), + const Divider(), + BetterListTile( + icon: FontAwesomeIcons.sun, + text: context.lang.settingsAppearance, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const AppearanceView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.comment, + text: context.lang.settingsChats, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ChatSettingsView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.lock, + text: context.lang.settingsPrivacy, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PrivacyView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.bell, + text: context.lang.settingsNotification, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const NotificationView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.chartPie, + iconSize: 15, + text: context.lang.settingsStorageData, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DataAndStorageView(); + }, + ), + ); + }, + ), + const Divider(), + BetterListTile( + icon: FontAwesomeIcons.circleQuestion, + text: context.lang.settingsHelp, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const HelpView(); + }, + ), + ); + }, + ), + if (gUser.isDeveloper) + BetterListTile( + icon: FontAwesomeIcons.code, + text: 'Developer Settings', + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DeveloperSettingsView(); + }, + ), + ); + }, + ), + BetterListTile( + icon: FontAwesomeIcons.shareFromSquare, + text: context.lang.inviteFriends, + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const ShareWithFriendsView(); + }, + ), + ); + }, + ), + ], + ), ); } } diff --git a/lib/src/views/settings/subscription/additional_users.view.dart b/lib/src/views/settings/subscription/additional_users.view.dart index 1afc62f..1bb21dd 100644 --- a/lib/src/views/settings/subscription/additional_users.view.dart +++ b/lib/src/views/settings/subscription/additional_users.view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/utils/log.dart'; @@ -23,10 +23,9 @@ Future?> loadAdditionalUserInvites() async { }); return ballance; } - final user = await getUser(); - if (user != null && user.lastPlanBallance != null) { + if (gUser.lastPlanBallance != null) { try { - final decoded = jsonDecode(user.additionalUserInvites!) as List; + final decoded = jsonDecode(gUser.additionalUserInvites!) as List; return decoded.map(Response_AddAccountsInvite.fromJson).toList(); } catch (e) { Log.error('could not parse additional user json: $e'); diff --git a/lib/src/views/settings/subscription/checkout.view.dart b/lib/src/views/settings/subscription/checkout.view.dart index 174c2d2..3d6797c 100644 --- a/lib/src/views/settings/subscription/checkout.view.dart +++ b/lib/src/views/settings/subscription/checkout.view.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/select_payment.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class CheckoutView extends StatefulWidget { const CheckoutView({ - required this.planId, + required this.plan, super.key, this.refund, this.disableMonthlyOption, }); - final String planId; + final SubscriptionPlan plan; final int? refund; final bool? disableMonthlyOption; @@ -31,7 +32,7 @@ class _CheckoutViewState extends State { } void setCheckout({bool init = false}) { - checkoutInCents = getPlanPrice(widget.planId, paidMonthly: paidMonthly); + checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly); if (!init) { setState(() {}); } @@ -52,7 +53,7 @@ class _CheckoutViewState extends State { Expanded( child: ListView( children: [ - PlanCard(planId: widget.planId), + PlanCard(plan: widget.plan), if (widget.disableMonthlyOption == null || !widget.disableMonthlyOption!) Padding( @@ -101,6 +102,7 @@ class _CheckoutViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -129,7 +131,7 @@ class _CheckoutViewState extends State { MaterialPageRoute( builder: (context) { return SelectPaymentView( - planId: widget.planId, + plan: widget.plan, payMonthly: paidMonthly, refund: widget.refund, ); diff --git a/lib/src/views/settings/subscription/manage_subscription.view.dart b/lib/src/views/settings/subscription/manage_subscription.view.dart index 7fb96ab..6c887d4 100644 --- a/lib/src/views/settings/subscription/manage_subscription.view.dart +++ b/lib/src/views/settings/subscription/manage_subscription.view.dart @@ -7,6 +7,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -64,25 +65,24 @@ class _ManageSubscriptionViewState extends State { @override Widget build(BuildContext context) { - final planId = context.read().plan; + final plan = context.read().plan; final myLocale = Localizations.localeOf(context); final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS; - final isPayingUser = planId == 'Family' || planId == 'Pro'; return Scaffold( appBar: AppBar( title: Text(context.lang.manageSubscription), ), body: ListView( children: [ - PlanCard(planId: planId, paidMonthly: paidMonthly), - if (isPayingUser) const SizedBox(height: 20), - if (widget.nextPayment != null && isPayingUser) + PlanCard(plan: plan, paidMonthly: paidMonthly), + if (isPayingUser(plan)) const SizedBox(height: 20), + if (widget.nextPayment != null && isPayingUser(plan)) ListTile( title: Text( '${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}', ), ), - if (autoRenewal != null && isPayingUser) + if (autoRenewal != null && isPayingUser(plan)) ListTile( title: Text(context.lang.autoRenewal), subtitle: Text( diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index c92f115..f9d3b8f 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -13,13 +14,13 @@ import 'package:url_launcher/url_launcher.dart'; class SelectPaymentView extends StatefulWidget { const SelectPaymentView({ super.key, - this.planId, + this.plan, this.payMonthly, this.valueInCents, this.refund, }); - final String? planId; + final SubscriptionPlan? plan; final bool? payMonthly; final int? valueInCents; final int? refund; @@ -62,9 +63,9 @@ class _SelectPaymentViewState extends State { void setCheckout(bool init) { if (widget.valueInCents != null && widget.valueInCents! > 0) { checkoutInCents = widget.valueInCents!; - } else if (widget.planId != null) { + } else if (widget.plan != null) { checkoutInCents = - getPlanPrice(widget.planId!, paidMonthly: widget.payMonthly!); + getPlanPrice(widget.plan!, paidMonthly: widget.payMonthly!); } else { /// Nothing to checkout for... Navigator.pop(context); @@ -77,7 +78,7 @@ class _SelectPaymentViewState extends State { @override Widget build(BuildContext context) { - final totalPrice = (widget.planId != null && widget.payMonthly != null) + final totalPrice = (widget.plan != null && widget.payMonthly != null) ? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}' : localePrizing(context, checkoutInCents); final canPay = paymentMethods == PaymentMethods.twonlyCredit && @@ -110,6 +111,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.all(16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -192,6 +194,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -214,6 +217,7 @@ class _SelectPaymentViewState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Card( + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.all(16), child: Row( @@ -239,13 +243,13 @@ class _SelectPaymentViewState extends State { onPressed: canPay ? () async { final res = await apiService.switchToPayedPlan( - widget.planId!, + widget.plan!.name, widget.payMonthly!, tryAutoRenewal, ); if (!context.mounted) return; if (res.isSuccess) { - await updateUsersPlan(context, widget.planId!); + await updateUsersPlan(context, widget.plan!); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index bdaeccd..d87434a 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -7,10 +7,11 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -64,7 +65,7 @@ const int MONTHLY_PAYMENT_DAYS = 30; const int YEARLY_PAYMENT_DAYS = 365; int calculateRefund(Response_PlanBallance current) { - var refund = getPlanPrice('Pro', paidMonthly: true); + var refund = getPlanPrice(SubscriptionPlan.Pro, paidMonthly: true); if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) { final elapsedDays = DateTime.now() @@ -81,7 +82,7 @@ int calculateRefund(Response_PlanBallance current) { // => 5€ refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) * - getPlanPrice('Pro', paidMonthly: false) / + getPlanPrice(SubscriptionPlan.Pro, paidMonthly: false) / 100) .ceil() * 100; @@ -144,13 +145,12 @@ class _SubscriptionViewState extends State { String? formattedBalance; DateTime? nextPayment; final currentPlan = context.read().plan; - final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro'; if (ballance != null) { final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch( ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000, ); - if (isPayingUser) { + if (isPayingUser(currentPlan)) { nextPayment = lastPaymentDateTime .add(Duration(days: ballance!.paymentPeriodDays.toInt())); } @@ -164,7 +164,7 @@ class _SubscriptionViewState extends State { } var refund = 0; - if (currentPlan == 'Pro' && ballance != null) { + if (currentPlan == SubscriptionPlan.Pro && ballance != null) { refund = calculateRefund(ballance!); } @@ -202,7 +202,7 @@ class _SubscriptionViewState extends State { ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Text( - currentPlan, + currentPlan.name, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, @@ -220,7 +220,7 @@ class _SubscriptionViewState extends State { style: const TextStyle(color: Colors.orange), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan)) Center( child: Padding( padding: const EdgeInsets.all(18), @@ -231,16 +231,17 @@ class _SubscriptionViewState extends State { ), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) PlanCard( - planId: 'Pro', + plan: SubscriptionPlan.Pro, onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { return const CheckoutView( - planId: 'Pro', + plan: SubscriptionPlan.Pro, ); }, ), @@ -248,9 +249,9 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (currentPlan != 'Family') + if (currentPlan != SubscriptionPlan.Family) PlanCard( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: refund, onTap: () async { await Navigator.push( @@ -258,11 +259,12 @@ class _SubscriptionViewState extends State { MaterialPageRoute( builder: (context) { return CheckoutView( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: (refund > 0) ? refund : null, - disableMonthlyOption: currentPlan == 'Pro' && - ballance!.paymentPeriodDays.toInt() == - YEARLY_PAYMENT_DAYS, + disableMonthlyOption: + currentPlan == SubscriptionPlan.Pro && + ballance!.paymentPeriodDays.toInt() == + YEARLY_PAYMENT_DAYS, ); }, ), @@ -270,7 +272,7 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (!isPayingUser) ...[ + if (!isPayingUser(currentPlan)) ...[ const SizedBox(height: 10), Center( child: Padding( @@ -284,15 +286,15 @@ class _SubscriptionViewState extends State { ), const SizedBox(height: 10), PlanCard( - planId: 'Plus', + plan: SubscriptionPlan.Plus, onTap: () async { - await redeemUserInviteCode(context, 'Plus'); + await redeemUserInviteCode(context, SubscriptionPlan.Plus.name); await initAsync(); }, ), ], const SizedBox(height: 10), - if (currentPlan != 'Family') const Divider(), + if (currentPlan != SubscriptionPlan.Family) const Divider(), BetterListTile( icon: FontAwesomeIcons.gears, text: context.lang.manageSubscription, @@ -337,7 +339,8 @@ class _SubscriptionViewState extends State { ); }, ), - if (isPayingUser || currentPlan == 'Tester') + if (isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) BetterListTile( icon: FontAwesomeIcons.userPlus, text: context.lang.manageAdditionalUsers, @@ -378,36 +381,38 @@ class _SubscriptionViewState extends State { } } -int getPlanPrice(String planId, {required bool paidMonthly}) { - switch (planId) { - case 'Pro': +int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) { + switch (plan) { + case SubscriptionPlan.Pro: return paidMonthly ? 100 : 1000; - case 'Family': + case SubscriptionPlan.Family: return paidMonthly ? 200 : 2000; + // ignore: no_default_cases + default: + return 0; } - return 0; } class PlanCard extends StatelessWidget { const PlanCard({ - required this.planId, + required this.plan, super.key, this.refund, this.onTap, this.paidMonthly, }); - final String planId; + final SubscriptionPlan plan; final void Function()? onTap; final int? refund; final bool? paidMonthly; @override Widget build(BuildContext context) { - final yearlyPrice = getPlanPrice(planId, paidMonthly: false); - final monthlyPrice = getPlanPrice(planId, paidMonthly: true); + final yearlyPrice = getPlanPrice(plan, paidMonthly: false); + final monthlyPrice = getPlanPrice(plan, paidMonthly: true); var features = []; - switch (planId) { + switch (plan.name) { case 'Free': features = [context.lang.freeFeature1]; case 'Plus': @@ -439,6 +444,7 @@ class PlanCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + color: context.color.surfaceContainer, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Column( @@ -447,7 +453,7 @@ class PlanCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( - planId, + plan.name, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, @@ -519,9 +525,12 @@ class PlanCard extends StatelessWidget { padding: const EdgeInsets.only(top: 10), child: FilledButton.icon( onPressed: onTap, - label: (planId == 'Free' || planId == 'Plus') + label: (plan == SubscriptionPlan.Free || + plan == SubscriptionPlan.Plus) ? Text(context.lang.redeemUserInviteCodeTitle) - : Text(context.lang.upgradeToPaidPlanButton(planId)), + : Text( + context.lang.upgradeToPaidPlanButton(plan.name), + ), ), ), ], diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart new file mode 100644 index 0000000..b2d0bff --- /dev/null +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -0,0 +1,424 @@ +import 'dart:collection' show HashSet; +import 'dart:convert'; +import 'dart:io'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/database/twonly_database_old.dart' + show TwonlyDatabaseOld; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/storage.dart'; + +class DatabaseMigrationView extends StatefulWidget { + const DatabaseMigrationView({super.key}); + + @override + State createState() => _DatabaseMigrationViewState(); +} + +class _DatabaseMigrationViewState extends State { + bool _isMigrating = false; + bool _isMigratingFinished = false; + int _contactsMigrated = 0; + int _storedMediaFiles = 0; + + Future startMigration() async { + setState(() { + _isMigrating = true; + }); + + final oldDatabase = TwonlyDatabaseOld(); + final oldContacts = await oldDatabase.contacts.select().get(); + + for (final oldContact in oldContacts) { + try { + if (oldContact.deleted) continue; + Uint8List? avatarSvg; + if (oldContact.avatarSvg != null) { + avatarSvg = Uint8List.fromList( + gzip.encode(utf8.encode(oldContact.avatarSvg!))); + } + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + userId: Value(oldContact.userId), + username: Value(oldContact.username), + displayName: Value(oldContact.displayName), + nickName: Value(oldContact.nickName), + avatarSvgCompressed: Value(avatarSvg), + senderProfileCounter: const Value(0), + accepted: Value(oldContact.accepted), + requested: Value(oldContact.requested), + blocked: Value(oldContact.blocked), + verified: Value(oldContact.verified), + createdAt: Value(oldContact.createdAt), + ), + ); + setState(() { + _contactsMigrated += 1; + }); + await twonlyDB.groupsDao.createNewDirectChat( + oldContact.userId, + GroupsCompanion( + pinned: Value(oldContact.pinned), + archived: Value(oldContact.archived), + groupName: Value(getContactDisplayNameOld(oldContact)), + totalMediaCounter: Value(oldContact.totalMediaCounter), + alsoBestFriend: Value(oldContact.alsoBestFriend), + createdAt: Value(oldContact.createdAt), + lastFlameCounterChange: Value(oldContact.lastFlameCounterChange), + lastFlameSync: Value(oldContact.lastFlameSync), + lastMessageExchange: Value(oldContact.lastMessageExchange), + lastMessageReceived: Value(oldContact.lastMessageReceived), + lastMessageSend: Value(oldContact.lastMessageSend), + flameCounter: Value(oldContact.flameCounter), + maxFlameCounter: Value(oldContact.flameCounter), + maxFlameCounterFrom: Value(DateTime.now()), + ), + ); + } catch (e) { + Log.error(e); + } + } + + final folders = ['memories', 'send', 'received']; + + final alreadyCopied = HashSet(); + + for (final folder in folders) { + final memoriesPath = Directory( + join( + (await getApplicationSupportDirectory()).path, + 'media', + folder, + ), + ); + if (memoriesPath.existsSync()) { + final files = memoriesPath.listSync(); + for (final file in files) { + try { + if (file.path.contains('thumbnail')) continue; + late MediaType type; + if (file.path.contains('mp4')) { + type = MediaType.video; + } else if (file.path.contains('png')) { + type = MediaType.image; + } else { + continue; + } + + final bytes = File(file.path).readAsBytesSync(); + final digest = (await Sha256().hash(bytes)).bytes; + if (alreadyCopied.contains(digest)) { + continue; + } + alreadyCopied.add(digest); + + final stat = FileStat.statSync(file.path); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + type: Value(type), + createdAt: Value(stat.modified), + stored: const Value(true), + ), + ); + final mediaService = await MediaFileService.fromMedia(mediaFile!); + File(file.path).copySync(mediaService.storedPath.path); + setState(() { + _storedMediaFiles += 1; + }); + } catch (e) { + Log.error(e); + } + } + } + } + + final oldContactPreKeys = + await oldDatabase.signalContactPreKeys.select().get(); + for (final oldContactPreKey in oldContactPreKeys) { + try { + await twonlyDB + .into(twonlyDB.signalContactPreKeys) + .insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson())); + } catch (e) { + Log.error(e); + } + } + + final oldSignalSessionStores = + await oldDatabase.signalSessionStores.select().get(); + for (final oldSignalSessionStore in oldSignalSessionStores) { + try { + await twonlyDB.into(twonlyDB.signalSessionStores).insert( + SignalSessionStore.fromJson(oldSignalSessionStore.toJson()), + ); + } catch (e) { + Log.error(e); + } + } + + final oldSignalSenderKeyStores = + await oldDatabase.signalSenderKeyStores.select().get(); + for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) { + try { + await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( + SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()), + ); + } catch (e) { + Log.error(e); + } + } + + final oldSignalPreyKeyStores = + await oldDatabase.signalPreKeyStores.select().get(); + for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) { + try { + await twonlyDB + .into(twonlyDB.signalPreKeyStores) + .insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson())); + } catch (e) { + Log.error(e); + } + } + + final oldSignalIdentityKeyStores = + await oldDatabase.signalIdentityKeyStores.select().get(); + for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) { + try { + await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( + SignalIdentityKeyStore.fromJson( + oldSignalIdentityKeyStore.toJson(), + ), + ); + } catch (e) { + Log.error(e); + } + } + + final oldSignalContactSignedPreKeys = + await oldDatabase.signalContactSignedPreKeys.select().get(); + for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) { + try { + await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert( + SignalContactSignedPreKey.fromJson( + oldSignalContactSignedPreKey.toJson(), + ), + ); + } catch (e) { + Log.error(e); + } + } + + await updateUserdata((u) { + u.appVersion = 62; + return u; + }); + + setState(() { + _isMigratingFinished = true; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(12), + child: _isMigratingFinished + ? ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'Deine Daten wurden migriert.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 40), + ...[ + '$_contactsMigrated Kontakte', + '$_storedMediaFiles gespeicherte Mediendateien', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + const SizedBox(height: 40), + const Text( + 'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12), + ), + const SizedBox(height: 30), + FilledButton( + onPressed: () { + Restart.restartApp( + notificationTitle: 'Deine Daten wurden migriert.', + notificationBody: 'Click here to open the app again', + ); + }, + child: const Text( + 'App neu starten', + ), + ), + ], + ) + : _isMigrating + ? ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'Deine Daten werden migriert.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 40), + const Center( + child: SizedBox( + width: 80, + height: 80, + child: CircularProgressIndicator(), + ), + ), + const SizedBox(height: 40), + const Text( + 'twonly während der Migration NICHT schließen!', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20, color: Colors.red), + ), + const SizedBox(height: 40), + const Text( + 'Aktueller Status', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + ...[ + '$_contactsMigrated Kontakte', + '$_storedMediaFiles gespeicherte Mediendateien', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + ], + ) + : ListView( + children: [ + const SizedBox(height: 40), + const Text( + 'twonly. Besser als je zuvor.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 30), + const Text( + 'Das sind die neuen Features.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + const SizedBox(height: 10), + ...[ + 'Gruppen', + 'Nachrichten bearbeiten & löschen', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 17), + ), + ), + const Text( + 'Technische Neuerungen', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 17), + ), + ...[ + 'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.', + 'Verwendung von UUIDs in der Datenbank', + 'Von Grund auf neues Datenbank-Schema', + 'Verbesserung der Zuverlässigkeit von C2C Nachrichten', + 'Verbesserung von Videos', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10), + ), + ), + const SizedBox(height: 50), + const Text( + 'Was bedeutet das für dich?', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 20), + ), + const Text( + 'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14), + ), + const SizedBox(height: 10), + const Text( + 'Was nach der Migration erhalten bleibt.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ...[ + 'Gespeicherte Bilder', + 'Kontakte', + 'Flammen', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(height: 10), + const Text( + 'Was durch die Migration verloren geht.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15, color: Colors.red), + ), + ...[ + 'Text-Nachrichten und Reaktionen', + 'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.', + ].map( + (e) => Text( + e, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(height: 30), + FilledButton( + onPressed: startMigration, + child: const Text( + 'Jetzt starten', + ), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8b26fd6..ff7558f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "8.4.0" + version: "8.4.1" archive: dependency: transitive description: @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + audio_waveforms: + dependency: "direct main" + description: + name: audio_waveforms + sha256: "658fef41bbab299184b65ba2fd749e8ec658c1f7d54a21f7cf97fa96b173b4ce" + url: "https://pub.dev" + source: hosted + version: "1.3.0" avatar_maker: dependency: "direct main" description: @@ -101,18 +109,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.0" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "4e54dbeefdc70691ba80b3bce3976af63b5425c8c07dface348dfee664a0edc1" + sha256: a9461b8e586bf018dd4afd2e13b49b08c6a844a4b226c8d1d10f3a723cdd78c3 url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.1" built_collection: dependency: transitive description: @@ -157,26 +165,27 @@ packages: dependency: "direct main" description: name: camera - sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.3" camera_android_camerax: - dependency: transitive + dependency: "direct overridden" description: - name: camera_android_camerax - sha256: "92dcc36e8ff2fa1ea3acdbb609ca2976cded55dceb719b4869c124c6d011f110" - url: "https://pub.dev" - source: hosted + path: "packages/camera/camera_android_camerax" + ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + resolved-ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + url: "https://github.com/otsmr/flutter-packages.git" + source: git version: "0.6.23+2" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "397f44f8a63c8c0a474668d500f9739d4f2bc45ac2b21801194b7d29260f03ee" + sha256: "34bcd5db30e52414f1f0783c5e3f566909fab14141a21b3b576c78bd35382bf6" url: "https://pub.dev" source: hosted - version: "0.9.22+1" + version: "0.9.22+4" camera_platform_interface: dependency: transitive description: @@ -333,10 +342,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6" + sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33 url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "12.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -357,18 +366,18 @@ packages: dependency: "direct main" description: name: drift - sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1" + sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114" url: "https://pub.dev" source: hosted - version: "2.28.2" + version: "2.29.0" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: "4db0eeedc7e8bed117a9f22d867ab7a3a294300fed5c269aac90d0b3545967ca" + sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43" url: "https://pub.dev" source: hosted - version: "2.28.3" + version: "2.29.0" drift_flutter: dependency: "direct main" description: @@ -385,6 +394,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + emoji_picker_flutter: + dependency: "direct main" + description: + name: emoji_picker_flutter + sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d" + url: "https://pub.dev" + source: hosted + version: "4.3.0" fake_async: dependency: transitive description: @@ -401,6 +418,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + ffmpeg_kit_flutter_new: + dependency: "direct main" + description: + name: ffmpeg_kit_flutter_new + sha256: d127635f27e93a7f21f0a14ce0a1a148e80919c402dac4a2118d73bfb17ce841 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + ffmpeg_kit_flutter_platform_interface: + dependency: transitive + description: + name: ffmpeg_kit_flutter_platform_interface + sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee + url: "https://pub.dev" + source: hosted + version: "0.2.1" file: dependency: transitive description: @@ -421,10 +454,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2" url: "https://pub.dev" source: hosted - version: "0.9.4+4" + version: "0.9.4+5" file_selector_platform_interface: dependency: transitive description: @@ -502,6 +535,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_android_volume_keydown: + dependency: "direct main" + description: + name: flutter_android_volume_keydown + sha256: bf7fed0be85541b939d9deb97b375cb12e6e703aa013754441318b0b9014e711 + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_cache_manager: dependency: transitive description: @@ -662,9 +703,11 @@ packages: flutter_secure_storage: dependency: "direct main" description: - path: "dependencies/flutter_secure_storage/flutter_secure_storage" - relative: true - source: path + path: flutter_secure_storage + ref: "71b75a36f35f2ce945998e20c6c6aa1820babfc6" + resolved-ref: "71b75a36f35f2ce945998e20c6c6aa1820babfc6" + url: "https://github.com/juliansteenbakker/flutter_secure_storage.git" + source: git version: "10.0.0-beta.4" flutter_secure_storage_darwin: dependency: transitive @@ -719,6 +762,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_volume_controller: + dependency: "direct main" + description: + name: flutter_volume_controller + sha256: "22edb0993ad03ecbc8d1164daeb5b39d798d409625db692675a86889403b1532" + url: "https://pub.dev" + source: hosted + version: "1.3.4" flutter_web_plugins: dependency: transitive description: flutter @@ -735,10 +786,10 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: "27af5982e6c510dec1ba038eff634fa284676ee84e3fd807225c80c4ad869177" + sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 url: "https://pub.dev" source: hosted - version: "10.10.0" + version: "10.12.0" gal: dependency: "direct main" description: @@ -847,10 +898,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6 url: "https://pub.dev" source: hosted - version: "0.8.13+5" + version: "0.8.13+7" image_picker_for_web: dependency: transitive description: @@ -863,10 +914,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58 url: "https://pub.dev" source: hosted - version: "0.8.13" + version: "0.8.13+1" image_picker_linux: dependency: transitive description: @@ -879,18 +930,18 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.2+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.11.1" image_picker_windows: dependency: transitive description: @@ -1183,10 +1234,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" path_provider_linux: dependency: transitive description: @@ -1275,13 +1326,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.0" - pie_menu: - dependency: "direct main" - description: - path: "dependencies/flutter-pie-menu" - relative: true - source: path - version: "3.3.0" platform: dependency: transitive description: @@ -1398,10 +1442,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6" + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" share_plus_platform_interface: dependency: transitive description: @@ -1430,10 +1474,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.5" shared_preferences_linux: dependency: transitive description: @@ -1579,10 +1623,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67" + sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c" url: "https://pub.dev" source: hosted - version: "0.41.2" + version: "0.42.0" stack_trace: dependency: transitive description: @@ -1663,6 +1707,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -1683,10 +1735,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" url: "https://pub.dev" source: hosted - version: "6.3.4" + version: "6.3.5" url_launcher_linux: dependency: transitive description: @@ -1699,10 +1751,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.4" url_launcher_platform_interface: dependency: transitive description: @@ -1775,14 +1827,6 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.0" - video_compress: - dependency: "direct main" - description: - name: video_compress - sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" - url: "https://pub.dev" - source: hosted - version: "3.1.4" video_player: dependency: "direct main" description: @@ -1803,18 +1847,18 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd + sha256: "19ed1162a7a5520e7d7791e0b7b73ba03161b6a69428b82e4689e435b325432d" url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.8.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b" + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.0" video_player_web: dependency: transitive description: @@ -1823,14 +1867,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" - video_thumbnail: - dependency: "direct main" - description: - name: video_thumbnail - sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" - url: "https://pub.dev" - source: hosted - version: "0.5.6" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a12ab31..245c4de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,16 +3,17 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.61+61 +version: 0.0.62+62 environment: sdk: ^3.6.0 dependencies: + audio_waveforms: ^1.3.0 avatar_maker: ^0.4.0 background_downloader: ^9.2.2 cached_network_image: ^3.4.1 - camera: ^0.11.1 + camera: ^0.11.2 collection: ^1.18.0 connectivity_plus: ^7.0.0 cryptography_flutter_plus: ^2.3.4 @@ -20,19 +21,25 @@ dependencies: device_info_plus: ^12.1.0 drift: ^2.25.1 drift_flutter: ^0.2.4 + emoji_picker_flutter: ^4.3.0 + ffmpeg_kit_flutter_new: ^4.1.0 firebase_core: ^4.2.0 firebase_messaging: ^16.0.3 fixnum: ^1.1.1 flutter: sdk: flutter + flutter_android_volume_keydown: ^1.0.1 flutter_image_compress: ^2.4.0 flutter_local_notifications: ^19.1.0 flutter_localizations: sdk: flutter - # flutter_secure_storage: ^10.0.0-beta.4 flutter_secure_storage: - path: ./dependencies/flutter_secure_storage/flutter_secure_storage + git: + url: https://github.com/juliansteenbakker/flutter_secure_storage.git + ref: 71b75a36f35f2ce945998e20c6c6aa1820babfc6 # from develop + path: flutter_secure_storage/ flutter_svg: ^2.0.17 + flutter_volume_controller: ^1.3.4 flutter_zxing: path: ./dependencies/flutter_zxing font_awesome_flutter: ^10.10.0 @@ -57,8 +64,6 @@ dependencies: path_provider: ^2.1.5 permission_handler: ^12.0.0+1 photo_view: ^0.15.0 - pie_menu: - path: ./dependencies/flutter-pie-menu protobuf: ^4.0.0 provider: ^6.1.2 restart_app: ^1.3.2 @@ -67,11 +72,18 @@ dependencies: share_plus: ^12.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1 - video_compress: ^3.1.4 video_player: ^2.9.5 - video_thumbnail: ^0.5.6 web_socket_channel: ^3.0.1 +dependency_overrides: + # hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY + camera_android_camerax: + # path: ../flutter-packages/packages/camera/camera_android_camerax + git: + url: https://github.com/otsmr/flutter-packages.git + path: packages/camera/camera_android_camerax + ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + dev_dependencies: build_runner: ^2.4.15 drift_dev: ^2.25.2 diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index 7fb971d..b842220 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -7,21 +7,25 @@ if [ ! -f "pubspec.yaml" ]; then exit 1 fi -# Definitions for twonly Safe -protoc --proto_path="./lib/src/model/protobuf/backup/" --dart_out="./lib/src/model/protobuf/backup/" "backup.proto" +# Definitions for twonly Backup +GENERATED_DIR="./lib/src/model/protobuf/client/generated/" +CLIENT_DIR="./lib/src/model/protobuf/client/" -# Definitions for the Push Notifications -protoc --proto_path="./lib/src/model/protobuf/push_notification/" --dart_out="./lib/src/model/protobuf/push_notification/" "push_notification.proto" -protoc --proto_path="./lib/src/model/protobuf/push_notification/" --swift_out="./ios/NotificationService/" "push_notification.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "backup.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "messages.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "groups.proto" + +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" +protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" # Definitions for the Server API -SRC_DIR="../twonly-server/twonly/src/" +SRC_DIR="../twonly-server/twonly-api/src/" DST_DIR="$(pwd)/lib/src/model/protobuf/" -mkdir $DST_DIR +mkdir $DST_DIR &>/dev/null ORIGINAL_DIR=$(pwd) @@ -32,9 +36,7 @@ cd "$SRC_DIR" || { for proto_file in "api/"**/*.proto; do if [[ -f "$proto_file" ]]; then - # Run the protoc command protoc --proto_path="." --dart_out="$DST_DIR" "$proto_file" - echo "Processed: $proto_file" else echo "No .proto files found in $SRC_DIR" fi @@ -45,4 +47,4 @@ cd "$ORIGINAL_DIR" || { exit 1 } -echo "Finished processing .proto files." \ No newline at end of file +echo "Finished processing .proto files :)" \ No newline at end of file diff --git a/test/drift/twonly_database/migration_test.dart b/test/drift/twonly_database/migration_test.dart index ee37ec9..50fc215 100644 --- a/test/drift/twonly_database/migration_test.dart +++ b/test/drift/twonly_database/migration_test.dart @@ -2,7 +2,7 @@ // ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations_native.dart'; -import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/database/twonly_database_old.dart'; import 'package:flutter_test/flutter_test.dart'; import 'generated/schema.dart'; @@ -27,7 +27,7 @@ void main() { for (final toVersion in versions.skip(i + 1)) { test('to $toVersion', () async { final schema = await verifier.schemaAt(fromVersion); - final db = TwonlyDatabase(schema.newConnection()); + final db = TwonlyDatabaseOld(schema.newConnection()); await verifier.migrateAndValidate(db, toVersion); await db.close(); }); @@ -70,7 +70,7 @@ void main() { newVersion: 2, createOld: v1.DatabaseAtV1.new, createNew: v2.DatabaseAtV2.new, - openTestedDatabase: TwonlyDatabase.new, + openTestedDatabase: TwonlyDatabaseOld.new, createItems: (batch, oldDb) { batch.insertAll(oldDb.contacts, oldContactsData); batch.insertAll(oldDb.messages, oldMessagesData); diff --git a/test/signal/key_generation.dart b/test/signal/key_generation.dart new file mode 100644 index 0000000..ae45c74 --- /dev/null +++ b/test/signal/key_generation.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; + +void main() { + group('testing api', () { + test('testing api connection', () async { + const offset = 100; + const count = 400; + + var prekeys = generatePreKeys(offset, count); + expect(count, prekeys.length); + + for (var i = 0; i < prekeys.length; i++) { + expect(prekeys[i].id, offset + i); + } + + prekeys += generatePreKeys(offset + count, count); + expect(count * 2, prekeys.length); + + for (var i = 0; i < (count * 2); i++) { + expect(prekeys[i].id, offset + i); + } + }); + }); +} diff --git a/test/unit_test.dart b/test/unit_test.dart index b37a39f..cb4229d 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; - import 'package:flutter_test/flutter_test.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:hashlib/random.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -15,21 +15,51 @@ void main() { }); test('test proof-of-work simple', () async { - expect(await calculatePoW(Uint8List.fromList([41, 41, 41, 41]), 6), 33); - }); - - test('test utils', () async { - final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); - final list2 = Uint8List.fromList([42, 42, 42]); - final combined = combineUint8Lists(list1, list2); - final lists = extractUint8Lists(combined); - expect(list1, lists[0]); - expect(list2, lists[1]); + // ignore: prefer_single_quotes + expect(await calculatePoW("testing", 10), 783); }); test('encode hex', () async { final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); expect(list1, hexToUint8List(uint8ListToHex(list1))); }); + + test('Zero inputs produce all-zero UUID', () { + expect( + getUUIDforDirectChat(0, 0), + '00000000-0000-0000-0000-000000000000', + ); + expect(getUUIDforDirectChat(0, 0).length, uuid.v1().length); + }); + + test('Max int values (0x7fffffff)', () { + const max32 = 0x7fffffff; // 2147483647 + expect( + getUUIDforDirectChat(max32, max32), + '00000000-7fff-ffff-0000-00007fffffff', + ); + }); + + test('Bigger goes front', () { + expect( + getUUIDforDirectChat(1, 0), + '00000000-0000-0001-0000-000000000000', + ); + expect( + getUUIDforDirectChat(0, 1), + '00000000-0000-0001-0000-000000000000', + ); + }); + + test('Arbitrary within 32-bit range', () { + expect( + getUUIDforDirectChat(0x12345678, 0x0abcdef0), + '00000000-1234-5678-0000-00000abcdef0', + ); + }); + + test('Reject values > 0x7fffffff', () { + expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError); + }); }); }