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