Merge pull request #393 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run

- New: Groups can now collect flames as well
- New: Background execution to pre-load messages 
- New: Adds a link if the image contains a QR code
- Improve: Video compression with progress updates
- Improve: Show message "Flames restored"
- Improve: Show toast message if user was added via QR 
- Fix: Media file appears as a white square and is not listed.
- Fix: Issue with media files required to be reuploaded
- Fix: Problem during contact requests
- Fix: Problem with deleting a contact
- Fix: Problem with restoring from backup
- Fix: Issue with the log file
This commit is contained in:
Tobi 2026-03-15 12:07:53 +01:00 committed by GitHub
commit 5e759df55b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
137 changed files with 56909 additions and 78995 deletions

View file

@ -25,8 +25,11 @@ jobs:
- name: Cloning sub-repos - name: Cloning sub-repos
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Check flutter code - name: flutter pub get
run: | run: flutter pub get
flutter pub get
flutter analyze - name: flutter analyze
flutter test run: flutter analyze
- name: flutter test
run: flutter test

View file

@ -1,12 +1,27 @@
# Changelog # Changelog
## 0.0.99
- New: Groups can now collect flames as well
- New: Background execution to pre-load messages
- New: Adds a link if the image contains a QR code
- Improve: Video compression with progress updates
- Improve: Show message "Flames restored"
- Improve: Show toast message if user was added via QR
- Fix: Media file appears as a white square and is not listed.
- Fix: Issue with media files required to be reuploaded
- Fix: Problem during contact requests
- Fix: Problem with deleting a contact
- Fix: Problem with restoring from backup
- Fix: Issue with the log file
## 0.0.96 ## 0.0.96
Feature: Show link in chat if the saved media file contains one - Feature: Show link in chat if the saved media file contains one
Improve: Verification badge for groups - Improve: Verification badge for groups
Improve: Huge reduction in app size - Improve: Huge reduction in app size
Fix: Crash on older devices when compressing a video - Fix: Crash on older devices when compressing a video
Fix: Problem with decrypting messages fixed - Fix: Problem with decrypting messages fixed
## 0.0.93 ## 0.0.93

View file

@ -23,12 +23,12 @@ android {
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
defaultConfig { defaultConfig {
@ -72,4 +72,5 @@ flutter {
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.otaliastudios:transcoder:0.11.0'
} }

View file

@ -1,7 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application <application
android:label="twonly" android:label="twonly"
android:name="${applicationName}" android:name=".MyApplication"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View file

@ -1,27 +1,14 @@
package eu.twonly package eu.twonly
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import android.view.KeyEvent import android.view.KeyEvent
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP import android.view.KeyEvent.KEYCODE_VOLUME_UP
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
class MainActivity : FlutterFragmentActivity() { class MainActivity : FlutterFragmentActivity() {
private val MEDIA_STORE_CHANNEL = "eu.twonly/mediaStore"
private lateinit var channel: MethodChannel
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) { if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
eventSink!!.success(true) eventSink!!.success(true)
@ -37,80 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MEDIA_STORE_CHANNEL) MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext)
channel.setMethodCallHandler {call, result ->
try {
if (call.method == "safeFileToDownload") {
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
val sourceFile = arguments["sourceFile"]
if (sourceFile == null) {
result.success(false)
} else {
val context = applicationContext
val inputStream = FileInputStream(File(sourceFile))
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
val savedUri = saveZipToDownloads(context, outputName, inputStream)
if (savedUri != null) {
result.success(savedUri.toString())
} else {
result.error("SAVE_FAILED", "Could not save ZIP", null)
}
}
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
}
fun saveZipToDownloads(
context: Context,
fileName: String = "archive.zip",
sourceStream: InputStream
): android.net.Uri? {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri("external")
}
val uri = resolver.insert(collection, contentValues) ?: return null
try {
resolver.openOutputStream(uri).use { out: OutputStream? ->
requireNotNull(out) { "Unable to open output stream" }
sourceStream.use { input ->
input.copyTo(out)
}
out.flush()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
resolver.update(uri, done, null, null)
}
return uri
} catch (e: Exception) {
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
return null
} }
} }

View file

@ -0,0 +1,92 @@
package eu.twonly
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
object MediaStoreChannel {
private const val CHANNEL = "eu.twonly/mediaStore"
fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
try {
if (call.method == "safeFileToDownload") {
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
val sourceFile = arguments["sourceFile"]
if (sourceFile == null) {
result.success(false)
} else {
val inputStream = FileInputStream(File(sourceFile))
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
val savedUri = saveZipToDownloads(context, outputName, inputStream)
if (savedUri != null) {
result.success(savedUri.toString())
} else {
result.error("SAVE_FAILED", "Could not save ZIP", null)
}
}
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
private fun saveZipToDownloads(
context: Context,
fileName: String = "archive.zip",
sourceStream: InputStream
): android.net.Uri? {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri("external")
}
val uri = resolver.insert(collection, contentValues) ?: return null
try {
resolver.openOutputStream(uri).use { out: OutputStream? ->
requireNotNull(out) { "Unable to open output stream" }
sourceStream.use { input ->
input.copyTo(out)
}
out.flush()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
resolver.update(uri, done, null, null)
}
return uri
} catch (e: Exception) {
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
return null
}
}
}

View file

@ -0,0 +1,13 @@
package eu.twonly
import io.flutter.app.FlutterApplication
import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler
class MyApplication : FlutterApplication() {
override fun onCreate() {
super.onCreate()
// This enables the internal plugin logging to Logcat
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
}
}

View file

@ -1,6 +0,0 @@
package eu.twonly
class MyMediaStorageProxy {
}

View file

@ -0,0 +1,107 @@
package eu.twonly
import android.content.Context
import android.media.MediaFormat
import android.os.Handler
import android.os.Looper
import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import com.otaliastudios.transcoder.strategy.TrackStrategy
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
object VideoCompressionChannel {
private const val CHANNEL = "eu.twonly/videoCompression"
// Compression parameters defined natively (as requested)
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
// Audio parameters defined natively
private const val AUDIO_BITRATE = 128_000L // 128 kbps
private const val AUDIO_SAMPLE_RATE = 44_100
private const val AUDIO_CHANNELS = 2
fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
try {
if (call.method == "compressVideo") {
val arguments = call.arguments<Map<String, Any>>() ?: emptyMap()
val inputPath = arguments["input"] as? String
val outputPath = arguments["output"] as? String
if (inputPath == null || outputPath == null) {
result.error("INVALID_ARGS", "Input or output path missing", null)
return@setMethodCallHandler
}
val mainHandler = Handler(Looper.getMainLooper())
val baseVideoStrategy = DefaultVideoStrategy.Builder()
.keyFrameInterval(3f)
.bitRate(VIDEO_BITRATE)
.addResizer(com.otaliastudios.transcoder.resize.AtMostResizer(1920, 1080))
.build()
val trackStrategyClass = TrackStrategy::class.java
val hevcStrategy = java.lang.reflect.Proxy.newProxyInstance(
trackStrategyClass.classLoader,
arrayOf(trackStrategyClass)
) { _, method, args ->
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
if (method.name == "createOutputFormat" && result is MediaFormat) {
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
}
result
} as TrackStrategy
Transcoder.into(outputPath)
.addDataSource(inputPath)
.setVideoTrackStrategy(hevcStrategy)
.setAudioTrackStrategy(
DefaultAudioStrategy.builder()
.channels(AUDIO_CHANNELS)
.sampleRate(AUDIO_SAMPLE_RATE)
.bitRate(AUDIO_BITRATE)
.build()
)
.setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) {
mainHandler.post {
val mappedProgress = (progress * 100).toInt()
channel.invokeMethod("onProgress", mapOf("progress" to mappedProgress))
}
}
override fun onTranscodeCompleted(successCode: Int) {
mainHandler.post {
result.success(outputPath)
}
}
override fun onTranscodeCanceled() {
mainHandler.post {
result.error("CANCELED", "Video compression canceled", null)
}
}
override fun onTranscodeFailed(exception: Throwable) {
mainHandler.post {
result.error("FAILED", exception.message, null)
}
}
})
.transcode()
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

@ -1 +1 @@
Subproject commit 33111edeb285db34edeb9fd21762825babe71ab0 Subproject commit 24d048b4abbe5c266b09965cc6f3ebdf83f97855

View file

@ -141,20 +141,20 @@ struct PushNotification: Sendable {
var kind: PushKind = .reaction var kind: PushKind = .reaction
var messageID: String { var messageID: String {
get {return _messageID ?? String()} get {_messageID ?? String()}
set {_messageID = newValue} set {_messageID = newValue}
} }
/// Returns true if `messageID` has been explicitly set. /// Returns true if `messageID` has been explicitly set.
var hasMessageID: Bool {return self._messageID != nil} var hasMessageID: Bool {self._messageID != nil}
/// Clears the value of `messageID`. Subsequent reads from it will return its default value. /// Clears the value of `messageID`. Subsequent reads from it will return its default value.
mutating func clearMessageID() {self._messageID = nil} mutating func clearMessageID() {self._messageID = nil}
var additionalContent: String { var additionalContent: String {
get {return _additionalContent ?? String()} get {_additionalContent ?? String()}
set {_additionalContent = newValue} set {_additionalContent = newValue}
} }
/// Returns true if `additionalContent` has been explicitly set. /// Returns true if `additionalContent` has been explicitly set.
var hasAdditionalContent: Bool {return self._additionalContent != nil} var hasAdditionalContent: Bool {self._additionalContent != nil}
/// Clears the value of `additionalContent`. Subsequent reads from it will return its default value. /// Clears the value of `additionalContent`. Subsequent reads from it will return its default value.
mutating func clearAdditionalContent() {self._additionalContent = nil} mutating func clearAdditionalContent() {self._additionalContent = nil}
@ -190,11 +190,11 @@ struct PushUser: Sendable {
var blocked: Bool = false var blocked: Bool = false
var lastMessageID: String { var lastMessageID: String {
get {return _lastMessageID ?? String()} get {_lastMessageID ?? String()}
set {_lastMessageID = newValue} set {_lastMessageID = newValue}
} }
/// Returns true if `lastMessageID` has been explicitly set. /// Returns true if `lastMessageID` has been explicitly set.
var hasLastMessageID: Bool {return self._lastMessageID != nil} var hasLastMessageID: Bool {self._lastMessageID != nil}
/// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value. /// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value.
mutating func clearLastMessageID() {self._lastMessageID = nil} mutating func clearLastMessageID() {self._lastMessageID = nil}

View file

@ -49,55 +49,55 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Firebase (12.8.0): - Firebase (12.9.0):
- Firebase/Core (= 12.8.0) - Firebase/Core (= 12.9.0)
- Firebase/Core (12.8.0): - Firebase/Core (12.9.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 12.8.0) - FirebaseAnalytics (~> 12.9.0)
- Firebase/CoreOnly (12.8.0): - Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.8.0) - FirebaseCore (~> 12.9.0)
- Firebase/Messaging (12.8.0): - Firebase/Messaging (12.9.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.8.0) - FirebaseMessaging (~> 12.9.0)
- firebase_core (4.4.0): - firebase_core (4.5.0):
- Firebase/CoreOnly (= 12.8.0) - Firebase/CoreOnly (= 12.9.0)
- Flutter - Flutter
- firebase_messaging (16.1.1): - firebase_messaging (16.1.2):
- Firebase/Messaging (= 12.8.0) - Firebase/Messaging (= 12.9.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseAnalytics (12.8.0): - FirebaseAnalytics (12.9.0):
- FirebaseAnalytics/Default (= 12.8.0) - FirebaseAnalytics/Default (= 12.9.0)
- FirebaseCore (~> 12.8.0) - FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.8.0) - FirebaseInstallations (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.8.0): - FirebaseAnalytics/Default (12.9.0):
- FirebaseCore (~> 12.8.0) - FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.8.0) - FirebaseInstallations (~> 12.9.0)
- GoogleAppMeasurement/Default (= 12.8.0) - GoogleAppMeasurement/Default (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (12.8.0): - FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.8.0) - FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.8.0): - FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.8.0): - FirebaseInstallations (12.9.0):
- FirebaseCore (~> 12.8.0) - FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (12.8.0): - FirebaseMessaging (12.9.0):
- FirebaseCore (~> 12.8.0) - FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.8.0) - FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1) - GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
@ -140,23 +140,23 @@ PODS:
- GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.8.0): - GoogleAppMeasurement/Core (12.9.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.8.0): - GoogleAppMeasurement/Default (12.9.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0) - GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.8.0) - GoogleAppMeasurement/Core (= 12.9.0)
- GoogleAppMeasurement/IdentitySupport (= 12.8.0) - GoogleAppMeasurement/IdentitySupport (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)" - "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.8.0): - GoogleAppMeasurement/IdentitySupport (12.9.0):
- GoogleAppMeasurement/Core (= 12.8.0) - GoogleAppMeasurement/Core (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1) - GoogleUtilities/Network (~> 8.1)
@ -267,7 +267,7 @@ PODS:
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- no_screenshot (0.3.2-beta.3): - no_screenshot (0.10.0):
- Flutter - Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
@ -276,7 +276,7 @@ PODS:
- pro_video_editor (0.0.1): - pro_video_editor (0.0.1):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- restart_app (0.0.1): - restart_app (1.7.3):
- Flutter - Flutter
- SDWebImage (5.21.6): - SDWebImage (5.21.6):
- SDWebImage/Core (= 5.21.6) - SDWebImage/Core (= 5.21.6)
@ -285,7 +285,7 @@ PODS:
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2) - Sentry/HybridSDK (8.56.2)
- sentry_flutter (9.13.0): - sentry_flutter (9.14.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.56.2) - Sentry/HybridSDK (= 8.56.2)
@ -326,11 +326,11 @@ PODS:
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_compress (0.3.0):
- Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- workmanager_apple (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
@ -375,8 +375,8 @@ DEPENDENCIES:
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf - SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - 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_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@ -484,10 +484,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
:path: ".symlinks/plugins/video_compress/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
workmanager_apple:
:path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
@ -501,14 +501,14 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 firebase_core: afac1aac13c931e0401c7e74ed1276112030efab
firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02 firebase_messaging: 7cb2727feb789751fc6936bcc8e08408970e2820
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21 FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0 FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
@ -521,7 +521,7 @@ SPEC CHECKSUMS:
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166 google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36 google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17 GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444 GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
@ -538,16 +538,16 @@ SPEC CHECKSUMS:
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961 MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 5e345998c43ffcad5d6834f249590483fcc037bd no_screenshot: 03c8ac6586f9652cd45e3d12d74e5992256403ac
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: dbed9a62ae39716b685a80140705c330d200d941 sentry_flutter: 841fa2fe08dc72eb95e2320b76e3f751f3400cf5
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
@ -556,8 +556,8 @@ SPEC CHECKSUMS:
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7 PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7

View file

@ -21,6 +21,7 @@
D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; }; D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; };
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -107,6 +108,7 @@
D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; }; D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCompressionChannel.swift; sourceTree = "<group>"; };
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
@ -235,6 +237,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */,
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */, D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */, D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
D2265DD42D920142000D99BB /* Runner.entitlements */, D2265DD42D920142000D99BB /* Runner.entitlements */,
@ -624,6 +627,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );

View file

@ -4,6 +4,7 @@ import Foundation
import UIKit import UIKit
import UserNotifications import UserNotifications
import flutter_sharing_intent import flutter_sharing_intent
import workmanager_apple
@main @main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
@ -12,6 +13,22 @@ import flutter_sharing_intent
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
VideoCompressionChannel.register(with: registrar.messenger())
}
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "eu.twonly.periodic_task",
frequency: NSNumber(value: 20 * 60)
)
WorkmanagerPlugin.registerBGProcessingTask(
withIdentifier: "eu.twonly.processing_task"
)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }

View file

@ -2,27 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIApplicationSceneManifest</key> <key>AppGroupId</key>
<dict> <string>$(CUSTOM_GROUP_ID)</string>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -43,6 +24,17 @@
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key> <key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
@ -51,10 +43,10 @@
<false/> <false/>
<key>FlutterDeepLinkingEnabled</key> <key>FlutterDeepLinkingEnabled</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string> <string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
@ -63,12 +55,39 @@
<string>Use your microphone to enable audio when making videos.</string> <string>Use your microphone to enable audio when making videos.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>twonly will save photos or videos to your library.</string> <string>twonly will save photos or videos to your library.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>eu.twonly.periodic_task</string>
<string>eu.twonly.processing_task</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
@ -89,19 +108,5 @@
</array> </array>
<key>firebase_performance_collection_deactivated</key> <key>firebase_performance_collection_deactivated</key>
<true/> <true/>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -0,0 +1,253 @@
import Foundation
import Flutter
import AVFoundation
class VideoCompressionChannel {
private let channelName = "eu.twonly/videoCompression"
// Hold a strong reference so the instance isn't immediately deallocated
private static var activeInstance: VideoCompressionChannel?
static func register(with messenger: FlutterBinaryMessenger) {
let instance = VideoCompressionChannel()
activeInstance = instance
let channel = FlutterMethodChannel(name: instance.channelName, binaryMessenger: messenger)
print("[VideoCompressionChannel] Registered channel: \(instance.channelName)")
channel.setMethodCallHandler { [weak instance] (call: FlutterMethodCall, result: @escaping FlutterResult) in
print("[VideoCompressionChannel] Received method call: \(call.method)")
instance?.handle(call, result: result, channel: channel)
}
}
init() {}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult, channel: FlutterMethodChannel) {
if call.method == "compressVideo" {
guard let args = call.arguments as? [String: Any],
let inputPath = args["input"] as? String,
let outputPath = args["output"] as? String else {
print("[VideoCompressionChannel] Error: Missing input or output path in arguments")
result(FlutterError(code: "INVALID_ARGS", message: "Input or output path missing", details: nil))
return
}
print("[VideoCompressionChannel] Starting compressVideo from \(inputPath) to \(outputPath)")
compress(inputPath: inputPath, outputPath: outputPath, channel: channel, result: result)
} else {
print("[VideoCompressionChannel] Method not implemented: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
func compress(inputPath: String, outputPath: String, channel: FlutterMethodChannel, result: @escaping FlutterResult) {
let inputURL = URL(fileURLWithPath: inputPath)
let outputURL = URL(fileURLWithPath: outputPath)
if FileManager.default.fileExists(atPath: outputURL.path) {
print("[VideoCompressionChannel] Removing existing file at output path")
try? FileManager.default.removeItem(at: outputURL)
}
let asset = AVAsset(url: inputURL)
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
print("[VideoCompressionChannel] Error: No video track found in asset")
result(FlutterError(code: "NO_VIDEO_TRACK", message: "Video track not found", details: nil))
return
}
let naturalSize = videoTrack.naturalSize
let transform = videoTrack.preferredTransform
let isPortrait = transform.a == 0 && abs(transform.b) == 1.0 && abs(transform.c) == 1.0 && transform.d == 0
let originalWidth = isPortrait ? naturalSize.height : naturalSize.width
let originalHeight = isPortrait ? naturalSize.width : naturalSize.height
let maxDimension: CGFloat = 1920.0
let minDimension: CGFloat = 1080.0
var targetWidth = originalWidth
var targetHeight = originalHeight
if targetWidth > maxDimension || targetHeight > maxDimension {
let widthRatio = maxDimension / targetWidth
let heightRatio = minDimension / targetHeight
let scaleFactor = min(widthRatio, heightRatio)
targetWidth *= scaleFactor
targetHeight *= scaleFactor
}
let targetBitrate = 3_000_000
do {
let reader = try AVAssetReader(asset: asset)
let writer = try AVAssetWriter(outputURL: outputURL, fileType: .mp4)
writer.shouldOptimizeForNetworkUse = true
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.hevc,
AVVideoWidthKey: Int(targetWidth),
AVVideoHeightKey: Int(targetHeight),
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: targetBitrate
]
]
let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
])
let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
writerInput.expectsMediaDataInRealTime = false
writerInput.transform = videoTrack.preferredTransform
guard writer.canAdd(writerInput) else {
result(FlutterError(code: "WRITER_ERROR", message: "Cannot add video writer input", details: nil))
return
}
guard reader.canAdd(readerOutput) else {
result(FlutterError(code: "READER_ERROR", message: "Cannot add video reader output", details: nil))
return
}
reader.add(readerOutput)
writer.add(writerInput)
// Audio processing (re-encode to AAC)
var audioReaderOutput: AVAssetReaderTrackOutput?
var audioWriterInput: AVAssetWriterInput?
if let audioTrack = asset.tracks(withMediaType: .audio).first {
let audioReaderSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM
]
let aReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioReaderSettings)
let audioWriterSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 2,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000
]
let aWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioWriterSettings)
aWriterInput.expectsMediaDataInRealTime = false
if reader.canAdd(aReaderOutput) && writer.canAdd(aWriterInput) {
reader.add(aReaderOutput)
writer.add(aWriterInput)
audioReaderOutput = aReaderOutput
audioWriterInput = aWriterInput
} else {
print("[VideoCompressionChannel] Warning: Cannot add audio tracks, proceeding without audio")
}
}
guard reader.startReading() else {
result(FlutterError(code: "READER_ERROR", message: "Cannot start reading: \(reader.error?.localizedDescription ?? "unknown error")", details: nil))
return
}
guard writer.startWriting() else {
result(FlutterError(code: "WRITER_ERROR", message: "Cannot start writing: \(writer.error?.localizedDescription ?? "unknown error")", details: nil))
return
}
writer.startSession(atSourceTime: .zero)
let duration = CMTimeGetSeconds(asset.duration)
let videoQueue = DispatchQueue(label: "videoQueue")
let audioQueue = DispatchQueue(label: "audioQueue")
let group = DispatchGroup()
// State tracking flag to avoid sending completed messages prematurely
var isVideoCompleted = false
var isAudioCompleted = audioWriterInput == nil
group.enter()
writerInput.requestMediaDataWhenReady(on: videoQueue) {
while writerInput.isReadyForMoreMediaData {
if reader.status != .reading {
if !isVideoCompleted {
isVideoCompleted = true
writerInput.markAsFinished()
group.leave()
}
return
}
if let sampleBuffer = readerOutput.copyNextSampleBuffer() {
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let timeInSeconds = CMTimeGetSeconds(presentationTime)
if duration > 0 {
let progress = Int((timeInSeconds / duration) * 100)
DispatchQueue.main.async {
channel.invokeMethod("onProgress", arguments: ["progress": progress])
}
}
writerInput.append(sampleBuffer)
} else {
if !isVideoCompleted {
isVideoCompleted = true
writerInput.markAsFinished()
group.leave()
}
break
}
}
}
if let audioWriterInput = audioWriterInput, let audioReaderOutput = audioReaderOutput {
group.enter()
audioWriterInput.requestMediaDataWhenReady(on: audioQueue) {
while audioWriterInput.isReadyForMoreMediaData {
if reader.status != .reading {
if !isAudioCompleted {
isAudioCompleted = true
audioWriterInput.markAsFinished()
group.leave()
}
return
}
if let sampleBuffer = audioReaderOutput.copyNextSampleBuffer() {
audioWriterInput.append(sampleBuffer)
} else {
if !isAudioCompleted {
isAudioCompleted = true
audioWriterInput.markAsFinished()
group.leave()
}
break
}
}
}
}
group.notify(queue: .main) {
if reader.status == .completed {
writer.finishWriting {
if writer.status == .completed {
print("[VideoCompressionChannel] Compression completed successfully!")
result(outputPath)
} else {
print("[VideoCompressionChannel] Writer Error: \(writer.error?.localizedDescription ?? "Unknown error")")
result(FlutterError(code: "WRITER_ERROR", message: writer.error?.localizedDescription, details: nil))
}
}
} else {
writer.cancelWriting()
print("[VideoCompressionChannel] Reader Error: \(reader.error?.localizedDescription ?? "Unknown error")")
result(FlutterError(code: "READER_ERROR", message: reader.error?.localizedDescription, details: nil))
}
}
} catch {
print("[VideoCompressionChannel] Exception: \(error.localizedDescription)")
result(FlutterError(code: "COMPRESS_ERROR", message: error.localizedDescription, details: nil))
}
}
}

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -188,7 +187,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) { if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.')); child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) { } else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) { if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () { callBack: () {
_skipBackup = true; _skipBackup = true;

View file

@ -31,7 +31,9 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
Map<String, VoidCallback> globalUserDataChangedCallBack = {}; Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
bool globalIsInBackgroundTask = false;
bool globalAllowErrorTrackingViaSentry = false; bool globalAllowErrorTrackingViaSentry = false;
bool globalGotMessageFromServer = false;
late String globalApplicationCacheDirectory; late String globalApplicationCacheDirectory;
late String globalApplicationSupportDirectory; late String globalApplicationSupportDirectory;

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -16,10 +17,11 @@ import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.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/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.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/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -27,6 +29,10 @@ import 'package:twonly/src/utils/storage.dart';
void main() async { void main() async {
SentryWidgetsFlutterBinding.ensureInitialized(); SentryWidgetsFlutterBinding.ensureInitialized();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
await initFCMService(); await initFCMService();
final user = await getUser(); final user = await getUser();
@ -45,12 +51,12 @@ void main() async {
} }
unawaited(performTwonlySafeBackup()); unawaited(performTwonlySafeBackup());
unawaited(initializeBackgroundTaskManager());
} else {
Log.info('User is not yet register. Ensure all local data is removed.');
await deleteLocalUserData();
} }
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
initLogger(); initLogger();
final settingsController = SettingsChangeProvider(); final settingsController = SettingsChangeProvider();

View file

@ -0,0 +1,45 @@
import 'package:flutter/services.dart';
import 'package:twonly/src/utils/log.dart';
abstract class VideoCompressionChannel {
static const MethodChannel _channel =
MethodChannel('eu.twonly/videoCompression');
static void Function(double)? _currentProgressCallback;
static bool _handlerSetup = false;
static void _setupProgressHandler() {
if (_handlerSetup) return;
_channel.setMethodCallHandler((call) async {
if (call.method == 'onProgress') {
// ignore: avoid_dynamic_calls
final progress = call.arguments['progress'] as int;
_currentProgressCallback?.call(progress / 100.0);
}
});
_handlerSetup = true;
}
static Future<String?> compressVideo({
required String inputPath,
required String outputPath,
void Function(double progress)? onProgress,
}) async {
try {
_setupProgressHandler();
_currentProgressCallback = onProgress;
await _channel.invokeMethod('compressVideo', {
'input': inputPath,
'output': outputPath,
});
return outputPath;
} on PlatformException catch (e) {
Log.error('Failed to compress video: $e');
return null;
} finally {
_currentProgressCallback = null;
}
}
}

View file

@ -0,0 +1,4 @@
class KeyValueKeys {
static const String lastPeriodicTaskExecution =
'last_periodic_task_execution';
}

View file

@ -110,7 +110,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return query.map((row) => row.read(count)).watchSingle(); return query.map((row) => row.read(count)).watchSingle();
} }
Stream<int?> watchContactsRequested() { Stream<int?> watchContactsRequestedCount() {
final count = contacts.requested.count(distinct: true); final count = contacts.requested.count(distinct: true);
final query = selectOnly(contacts) final query = selectOnly(contacts)
..where( ..where(

View file

@ -23,9 +23,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
GroupsDao(super.db); GroupsDao(super.db);
Future<bool> isContactInGroup(int contactId, String groupId) async { Future<bool> isContactInGroup(int contactId, String groupId) async {
final entry = await (select(groupMembers)..where( final entry =
// ignore: require_trailing_commas await (select(groupMembers)..where(
(t) => t.contactId.equals(contactId) & t.groupId.equals(groupId))) (t) => t.contactId.equals(contactId) & t.groupId.equals(groupId),
))
.getSingleOrNull(); .getSingleOrNull();
return entry != null; return entry != null;
} }
@ -38,13 +39,13 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
String groupId, String groupId,
GroupsCompanion updates, GroupsCompanion updates,
) async { ) async {
await (update(groups)..where((c) => c.groupId.equals(groupId))) await (update(
.write(updates); groups,
)..where((c) => c.groupId.equals(groupId))).write(updates);
} }
Future<List<GroupMember>> getGroupNonLeftMembers(String groupId) async { Future<List<GroupMember>> getGroupNonLeftMembers(String groupId) async {
return (select(groupMembers) return (select(groupMembers)..where(
..where(
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
(t.memberState.equals(MemberState.leftGroup.name).not() | (t.memberState.equals(MemberState.leftGroup.name).not() |
@ -54,14 +55,15 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Future<List<GroupMember>> getAllGroupMembers(String groupId) async { Future<List<GroupMember>> getAllGroupMembers(String groupId) async {
return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) return (select(
.get(); groupMembers,
)..where((t) => t.groupId.equals(groupId))).get();
} }
Future<GroupMember?> getGroupMemberByPublicKey(Uint8List publicKey) async { Future<GroupMember?> getGroupMemberByPublicKey(Uint8List publicKey) async {
return (select(groupMembers) return (select(
..where((t) => t.groupPublicKey.equals(publicKey))) groupMembers,
.getSingleOrNull(); )..where((t) => t.groupPublicKey.equals(publicKey))).getSingleOrNull();
} }
Future<Group?> createNewGroup(GroupsCompanion group) async { Future<Group?> createNewGroup(GroupsCompanion group) async {
@ -94,16 +96,14 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int contactId, int contactId,
GroupMembersCompanion updates, GroupMembersCompanion updates,
) async { ) async {
await (update(groupMembers) await (update(groupMembers)..where(
..where(
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
)) ))
.write(updates); .write(updates);
} }
Future<void> removeMember(String groupId, int contactId) async { Future<void> removeMember(String groupId, int contactId) async {
await (delete(groupMembers) await (delete(groupMembers)..where(
..where(
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId), (c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
)) ))
.go(); .go();
@ -138,9 +138,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
Future<Group?> _insertGroup(GroupsCompanion group) async { Future<Group?> _insertGroup(GroupsCompanion group) async {
try { try {
await into(groups).insert(group); await into(groups).insert(group);
return await (select(groups) return await (select(
..where((t) => t.groupId.equals(group.groupId.value))) groups,
.getSingle(); )..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
} catch (e) { } catch (e) {
Log.error('Could not insert group: $e'); Log.error('Could not insert group: $e');
return null; return null;
@ -148,7 +148,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Future<List<Contact>> getGroupContact(String groupId) async { Future<List<Contact>> getGroupContact(String groupId) async {
final query = (select(contacts).join([ final query =
(select(contacts).join([
leftOuterJoin( leftOuterJoin(
groupMembers, groupMembers,
groupMembers.contactId.equalsExp(contacts.userId), groupMembers.contactId.equalsExp(contacts.userId),
@ -161,7 +162,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Stream<List<Contact>> watchGroupContact(String groupId) { Stream<List<Contact>> watchGroupContact(String groupId) {
final query = (select(contacts).join([ final query =
(select(contacts).join([
leftOuterJoin( leftOuterJoin(
groupMembers, groupMembers,
groupMembers.contactId.equalsExp(contacts.userId), groupMembers.contactId.equalsExp(contacts.userId),
@ -187,30 +189,30 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Stream<List<Group>> watchGroupsForShareImage() { Stream<List<Group>> watchGroupsForShareImage() {
return (select(groups) return (select(groups)..where(
..where(
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false), (g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
)) ))
.watch(); .watch();
} }
Stream<List<GroupMember>> watchContactGroupMember(int contactId) { Stream<List<GroupMember>> watchContactGroupMember(int contactId) {
return (select(groupMembers) return (select(groupMembers)..where(
..where(
(g) => g.contactId.equals(contactId), (g) => g.contactId.equals(contactId),
)) ))
.watch(); .watch();
} }
Stream<Group?> watchGroup(String groupId) { Stream<Group?> watchGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId))) return (select(
.watchSingleOrNull(); groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
} }
Stream<Group?> watchDirectChat(int contactId) { Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(contactId, gUser.userId); final groupId = getUUIDforDirectChat(contactId, gUser.userId);
return (select(groups)..where((t) => t.groupId.equals(groupId))) return (select(
.watchSingleOrNull(); groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
} }
Stream<List<Group>> watchGroupsForChatList() { Stream<List<Group>> watchGroupsForChatList() {
@ -228,13 +230,13 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Future<Group?> getGroup(String groupId) { Future<Group?> getGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId))) return (select(
.getSingleOrNull(); groups,
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
} }
Stream<int> watchFlameCounter(String groupId) { Stream<int> watchFlameCounter(String groupId) {
return (select(groups) return (select(groups)..where(
..where(
(u) => (u) =>
u.groupId.equals(groupId) & u.groupId.equals(groupId) &
u.lastMessageReceived.isNotNull() & u.lastMessageReceived.isNotNull() &
@ -248,9 +250,12 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return (select(groups)..where((t) => t.isDirectChat.equals(true))).get(); return (select(groups)..where((t) => t.isDirectChat.equals(true))).get();
} }
Future<List<Group>> getAllGroups() {
return select(groups).get();
}
Future<List<Group>> getAllNotJoinedGroups() { Future<List<Group>> getAllNotJoinedGroups() {
return (select(groups) return (select(groups)..where(
..where(
(t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false), (t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false),
)) ))
.get(); .get();
@ -258,15 +263,15 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
Future<List<GroupMember>> getAllGroupMemberWithoutPublicKey() async { Future<List<GroupMember>> getAllGroupMemberWithoutPublicKey() async {
try { try {
final query = ((select(groupMembers) final query =
..where((t) => t.groupPublicKey.isNull())) ((select(groupMembers)..where((t) => t.groupPublicKey.isNull())).join(
.join([ [
leftOuterJoin( leftOuterJoin(
groups, groups,
groups.groupId.equalsExp(groupMembers.groupId), groups.groupId.equalsExp(groupMembers.groupId),
), ),
]) ],
..where(groups.isDirectChat.isNull())); )..where(groups.isDirectChat.isNull()));
return query.map((row) => row.readTable(groupMembers)).get(); return query.map((row) => row.readTable(groupMembers)).get();
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
@ -281,8 +286,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groupMembers, groupMembers,
groupMembers.groupId.equalsExp(groups.groupId), groupMembers.groupId.equalsExp(groups.groupId),
), ),
]) ])..where(groupMembers.contactId.equals(userId)));
..where(groupMembers.contactId.equals(userId)));
return query.map((row) => row.readTable(groups)).getSingleOrNull(); return query.map((row) => row.readTable(groups)).getSingleOrNull();
} }
@ -300,8 +304,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
String groupId, String groupId,
DateTime newLastMessage, DateTime newLastMessage,
) async { ) async {
await (update(groups) await (update(groups)..where(
..where(
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)), (t.lastMessageExchange.isSmallerThanValue(newLastMessage)),

View file

@ -22,5 +22,7 @@ class GroupsDaoManager {
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers); $$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
$$GroupHistoriesTableTableManager get groupHistories => $$GroupHistoriesTableTableManager get groupHistories =>
$$GroupHistoriesTableTableManager( $$GroupHistoriesTableTableManager(
_db.attachedDatabase, _db.groupHistories); _db.attachedDatabase,
_db.groupHistories,
);
} }

View file

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

View file

@ -44,10 +44,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Stream<List<Message>> watchMediaNotOpened(String groupId) { Stream<List<Message>> watchMediaNotOpened(String groupId) {
final query = select(messages).join([ final query =
leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)), select(messages).join([
]) leftOuterJoin(
..where( mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId),
),
])..where(
mediaFiles.downloadState mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name) .equals(DownloadState.reuploadRequested.name)
.not() & .not() &
@ -70,8 +73,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Stream<List<Message>> watchByGroupId(String groupId) { Stream<List<Message>> watchByGroupId(String groupId) {
return ((select(messages) return ((select(messages)..where(
..where(
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
(t.isDeletedFromSender.equals(true) | (t.isDeletedFromSender.equals(true) |
@ -92,21 +94,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
contacts, contacts,
contacts.userId.equalsExp(groupMembers.contactId), contacts.userId.equalsExp(groupMembers.contactId),
), ),
]) ])..where(groupMembers.groupId.equals(groupId)));
..where(groupMembers.groupId.equals(groupId)));
return query return query
.map((row) => (row.readTable(groupMembers), row.readTable(contacts))) .map((row) => (row.readTable(groupMembers), row.readTable(contacts)))
.watch(); .watch();
} }
Stream<List<MessageAction>> watchMessageActionChanges(String messageId) { Stream<List<MessageAction>> watchMessageActionChanges(String messageId) {
return (select(messageActions)..where((t) => t.messageId.equals(messageId))) return (select(
.watch(); messageActions,
)..where((t) => t.messageId.equals(messageId))).watch();
} }
Stream<Message?> watchMessageById(String messageId) { Stream<Message?> watchMessageById(String messageId) {
return (select(messages)..where((t) => t.messageId.equals(messageId))) return (select(
.watchSingleOrNull(); messages,
)..where((t) => t.messageId.equals(messageId))).watchSingleOrNull();
} }
Future<void> purgeMessageTable() async { Future<void> purgeMessageTable() async {
@ -118,8 +121,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
milliseconds: group.deleteMessagesAfterMilliseconds, milliseconds: group.deleteMessagesAfterMilliseconds,
), ),
); );
await (delete(messages) await (delete(messages)..where(
..where(
(m) => (m) =>
m.groupId.equals(group.groupId) & m.groupId.equals(group.groupId) &
(m.mediaStored.equals(true) & (m.mediaStored.equals(true) &
@ -135,8 +137,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
Future<void> openedAllTextMessages(String groupId) { Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(clock.now())); final updates = MessagesCompanion(openedAt: Value(clock.now()));
return (update(messages) return (update(messages)..where(
..where(
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
t.senderId.isNotNull() & t.senderId.isNotNull() &
@ -158,20 +159,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
if (msg.mediaId != null && contactId != null) { 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... // 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!))) await (delete(
.go(); mediaFiles,
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!); final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) { if (mediaService != null) {
mediaService.fullMediaRemoval(); mediaService.fullMediaRemoval();
} }
} }
await (delete(messageHistories) await (delete(
..where((t) => t.messageId.equals(messageId))) messageHistories,
.go(); )..where((t) => t.messageId.equals(messageId))).go();
await (update(messages) await (update(messages)..where(
..where(
(t) => t.messageId.equals(messageId), (t) => t.messageId.equals(messageId),
)) ))
.write( .write(
@ -200,8 +201,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
createdAt: Value(timestamp), createdAt: Value(timestamp),
), ),
); );
await (update(messages) await (update(messages)..where(
..where(
(t) => t.messageId.equals(messageId), (t) => t.messageId.equals(messageId),
)) ))
.write( .write(
@ -232,8 +232,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
for (final messageId in messageIds) { for (final messageId in messageIds) {
final isOpenedByAll = final isOpenedByAll = await haveAllMembers(
await haveAllMembers(messageId, MessageActionType.openedAt); messageId,
MessageActionType.openedAt,
);
final now = clock.now(); final now = clock.now();
batch.update( batch.update(
@ -271,14 +273,16 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
MessageActionType action, MessageActionType action,
) async { ) async {
final message = final message = await twonlyDB.messagesDao
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); .getMessageById(messageId)
.getSingleOrNull();
if (message == null) return true; if (message == null) return true;
final members = final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); message.groupId,
);
final actions = await (select(messageActions) final actions =
..where( await (select(messageActions)..where(
(t) => t.type.equals(action.name) & t.messageId.equals(messageId), (t) => t.type.equals(action.name) & t.messageId.equals(messageId),
)) ))
.get(); .get();
@ -290,16 +294,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
MessagesCompanion updatedValues, MessagesCompanion updatedValues,
) async { ) async {
await (update(messages)..where((c) => c.messageId.equals(messageId))) await (update(
.write(updatedValues); messages,
)..where((c) => c.messageId.equals(messageId))).write(updatedValues);
} }
Future<void> updateMessagesByMediaId( Future<void> updateMessagesByMediaId(
String mediaId, String mediaId,
MessagesCompanion updatedValues, MessagesCompanion updatedValues,
) { ) {
return (update(messages)..where((c) => c.mediaId.equals(mediaId))) return (update(
.write(updatedValues); messages,
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
} }
Future<Message?> insertMessage(MessagesCompanion message) async { Future<Message?> insertMessage(MessagesCompanion message) async {
@ -333,8 +339,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
} }
return await (select(messages)..where((t) => t.rowId.equals(rowId))) return await (select(
.getSingle(); messages,
)..where((t) => t.rowId.equals(rowId))).getSingle();
} catch (e) { } catch (e) {
Log.error('Could not insert message: $e'); Log.error('Could not insert message: $e');
return null; return null;
@ -342,8 +349,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Future<MessageAction?> getLastMessageAction(String messageId) async { Future<MessageAction?> getLastMessageAction(String messageId) async {
return (((select(messageActions) return (((select(messageActions)..where(
..where(
(t) => t.messageId.equals(messageId), (t) => t.messageId.equals(messageId),
)) ))
..orderBy([(t) => OrderingTerm.desc(t.actionAt)])) ..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
@ -373,8 +379,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
contacts, contacts,
contacts.userId.equalsExp(messageActions.contactId), contacts.userId.equalsExp(messageActions.contactId),
), ),
]) ])..where(messageActions.messageId.equals(messageId)));
..where(messageActions.messageId.equals(messageId)));
return query return query
.map((row) => (row.readTable(messageActions), row.readTable(contacts))) .map((row) => (row.readTable(messageActions), row.readTable(contacts)))
.watch(); .watch();

View file

@ -31,10 +31,14 @@ class MessagesDaoManager {
$$ReactionsTableTableManager(_db.attachedDatabase, _db.reactions); $$ReactionsTableTableManager(_db.attachedDatabase, _db.reactions);
$$MessageHistoriesTableTableManager get messageHistories => $$MessageHistoriesTableTableManager get messageHistories =>
$$MessageHistoriesTableTableManager( $$MessageHistoriesTableTableManager(
_db.attachedDatabase, _db.messageHistories); _db.attachedDatabase,
_db.messageHistories,
);
$$GroupMembersTableTableManager get groupMembers => $$GroupMembersTableTableManager get groupMembers =>
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers); $$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
$$MessageActionsTableTableManager get messageActions => $$MessageActionsTableTableManager get messageActions =>
$$MessageActionsTableTableManager( $$MessageActionsTableTableManager(
_db.attachedDatabase, _db.messageActions); _db.attachedDatabase,
_db.messageActions,
);
} }

View file

@ -1,14 +1,18 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.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/messages.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart'; part 'receipts.dao.g.dart';
@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts]) @DriftAccessor(
tables: [Receipts, Messages, MessageActions, ReceivedReceipts, Contacts],
)
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin { class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
@ -16,10 +20,11 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
ReceiptsDao(super.db); ReceiptsDao(super.db);
Future<void> confirmReceipt(String receiptId, int fromUserId) async { Future<void> confirmReceipt(String receiptId, int fromUserId) async {
final receipt = await (select(receipts) final receipt =
..where( await (select(receipts)..where(
(t) => (t) =>
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId), t.receiptId.equals(receiptId) &
t.contactId.equals(fromUserId),
)) ))
.getSingleOrNull(); .getSingleOrNull();
@ -33,27 +38,24 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
type: const Value(MessageActionType.ackByUserAt), type: const Value(MessageActionType.ackByUserAt),
), ),
); );
await handleMediaRelatedResponseFromReceiver(receipt.messageId!);
} }
await (delete(receipts) await (delete(receipts)..where(
..where( (t) => t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
(t) =>
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
)) ))
.go(); .go();
} }
Future<void> deleteReceipt(String receiptId) async { Future<void> deleteReceipt(String receiptId) async {
await (delete(receipts) await (delete(receipts)..where(
..where(
(t) => t.receiptId.equals(receiptId), (t) => t.receiptId.equals(receiptId),
)) ))
.go(); .go();
} }
Future<void> purgeReceivedReceipts() async { Future<void> purgeReceivedReceipts() async {
await (delete(receivedReceipts) await (delete(receivedReceipts)..where(
..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract( clock.now().subtract(
const Duration(days: 25), const Duration(days: 25),
@ -61,6 +63,17 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)), )),
)) ))
.go(); .go();
final deletedContacts = await (select(
contacts,
)..where((t) => t.accountDeleted.equals(true))).get();
for (final contact in deletedContacts) {
await (delete(receipts)..where(
(t) => t.contactId.equals(contact.userId),
))
.go();
}
} }
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async { Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
@ -72,8 +85,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
); );
} }
final id = await into(receipts).insert(insertEntry); final id = await into(receipts).insert(insertEntry);
return await (select(receipts)..where((t) => t.rowId.equals(id))) return await (select(
.getSingle(); receipts,
)..where((t) => t.rowId.equals(id))).getSingle();
} catch (e) { } catch (e) {
// ignore error, receipts is already in the database... // ignore error, receipts is already in the database...
return null; return null;
@ -82,8 +96,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<Receipt?> getReceiptById(String receiptId) async { Future<Receipt?> getReceiptById(String receiptId) async {
try { try {
return await (select(receipts) return await (select(receipts)..where(
..where(
(t) => t.receiptId.equals(receiptId), (t) => t.receiptId.equals(receiptId),
)) ))
.getSingleOrNull(); .getSingleOrNull();
@ -100,13 +113,14 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
seconds: 20, seconds: 20,
), ),
); );
return (select(receipts) return (select(receipts)..where(
..where(
(t) => (t) =>
t.ackByServerAt.isNull() | (t.ackByServerAt.isNull() |
t.markForRetry.isSmallerThanValue(markedRetriesTime) | t.markForRetry.isSmallerThanValue(markedRetriesTime) |
t.markForRetryAfterAccepted t.markForRetryAfterAccepted.isSmallerThanValue(
.isSmallerThanValue(markedRetriesTime), markedRetriesTime,
)) &
t.willBeRetriedByMediaUpload.equals(false),
)) ))
.get(); .get();
} }
@ -119,8 +133,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
String receiptId, String receiptId,
ReceiptsCompanion updates, ReceiptsCompanion updates,
) async { ) async {
await (update(receipts)..where((c) => c.receiptId.equals(receiptId))) await (update(
.write(updates); receipts,
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
} }
Future<void> updateReceiptWidthUserId( Future<void> updateReceiptWidthUserId(
@ -128,16 +143,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
String receiptId, String receiptId,
ReceiptsCompanion updates, ReceiptsCompanion updates,
) async { ) async {
await (update(receipts) await (update(receipts)..where(
..where( (c) => c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
(c) =>
c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
)) ))
.write(updates); .write(updates);
} }
Future<void> markMessagesForRetry(int contactId) async { Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write( await (update(receipts)..where(
(c) =>
c.contactId.equals(contactId) &
c.willBeRetriedByMediaUpload.equals(false),
))
.write(
ReceiptsCompanion( ReceiptsCompanion(
markForRetry: Value(clock.now()), markForRetry: Value(clock.now()),
), ),
@ -145,14 +163,15 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
} }
Future<bool> isDuplicated(String receiptId) async { Future<bool> isDuplicated(String receiptId) async {
return await (select(receivedReceipts) return await (select(
..where((t) => t.receiptId.equals(receiptId))) receivedReceipts,
.getSingleOrNull() != )..where((t) => t.receiptId.equals(receiptId))).getSingleOrNull() !=
null; null;
} }
Future<void> gotReceipt(String receiptId) async { Future<void> gotReceipt(String receiptId) async {
await into(receivedReceipts) await into(
.insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId))); receivedReceipts,
).insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
} }
} }

View file

@ -30,8 +30,12 @@ class ReceiptsDaoManager {
$$ReceiptsTableTableManager(_db.attachedDatabase, _db.receipts); $$ReceiptsTableTableManager(_db.attachedDatabase, _db.receipts);
$$MessageActionsTableTableManager get messageActions => $$MessageActionsTableTableManager get messageActions =>
$$MessageActionsTableTableManager( $$MessageActionsTableTableManager(
_db.attachedDatabase, _db.messageActions); _db.attachedDatabase,
_db.messageActions,
);
$$ReceivedReceiptsTableTableManager get receivedReceipts => $$ReceivedReceiptsTableTableManager get receivedReceipts =>
$$ReceivedReceiptsTableTableManager( $$ReceivedReceiptsTableTableManager(
_db.attachedDatabase, _db.receivedReceipts); _db.attachedDatabase,
_db.receivedReceipts,
);
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,9 @@ enum UploadState {
uploaded, uploaded,
uploadLimitReached, uploadLimitReached,
// File is to big to be uploaded
fileLimitReached,
// readyToUpload, // readyToUpload,
// uploadTaskStarted, // uploadTaskStarted,
// receiverNotified, // receiverNotified,
@ -48,6 +51,8 @@ class MediaFiles extends Table {
BoolColumn get stored => boolean().withDefault(const Constant(false))(); BoolColumn get stored => boolean().withDefault(const Constant(false))();
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
IntColumn get preProgressingProcess => integer().nullable()();
TextColumn get reuploadRequestedBy => TextColumn get reuploadRequestedBy =>
text().map(IntListTypeConverter()).nullable()(); text().map(IntListTypeConverter()).nullable()();

View file

@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.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/mediafiles.table.dart';
enum MessageType { media, text, contacts } enum MessageType { media, text, contacts, restoreFlameCounter }
@DataClassName('Message') @DataClassName('Message')
class Messages extends Table { class Messages extends Table {

View file

@ -20,6 +20,9 @@ class Receipts extends Table {
BoolColumn get contactWillSendsReceipt => BoolColumn get contactWillSendsReceipt =>
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get willBeRetriedByMediaUpload =>
boolean().withDefault(const Constant(false))();
DateTimeColumn get markForRetry => dateTime().nullable()(); DateTimeColumn get markForRetry => dateTime().nullable()();
DateTimeColumn get markForRetryAfterAccepted => dateTime().nullable()(); DateTimeColumn get markForRetryAfterAccepted => dateTime().nullable()();

View file

@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 8; int get schemaVersion => 10;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -131,6 +131,18 @@ class TwonlyDB extends _$TwonlyDB {
// ignore: experimental_member_use // ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageActions)); await m.alterTable(TableMigration(schema.messageActions));
}, },
from8To9: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.preProgressingProcess,
);
},
from9To10: (m, schema) async {
await m.addColumn(
schema.receipts,
schema.receipts.willBeRetriedByMediaUpload,
);
},
)(m, from, to); )(m, from, to);
}, },
); );
@ -154,9 +166,9 @@ class TwonlyDB extends _$TwonlyDB {
} }
Future<void> deleteDataForTwonlySafe() async { Future<void> deleteDataForTwonlySafe() async {
await (delete(messages) await (delete(messages)..where(
..where( (t) =>
(t) => (t.mediaStored.equals(false) & (t.mediaStored.equals(false) &
t.isDeletedFromSender.equals(false)), t.isDeletedFromSender.equals(false)),
)) ))
.go(); .go();
@ -165,8 +177,7 @@ class TwonlyDB extends _$TwonlyDB {
downloadToken: Value(null), downloadToken: Value(null),
), ),
); );
await (delete(mediaFiles) await (delete(mediaFiles)..where(
..where(
(t) => (t.stored.equals(false)), (t) => (t.stored.equals(false)),
)) ))
.go(); .go();
@ -178,8 +189,7 @@ class TwonlyDB extends _$TwonlyDB {
senderProfileCounter: Value(0), senderProfileCounter: Value(0),
), ),
); );
await (delete(signalPreKeyStores) await (delete(signalPreKeyStores)..where(
..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract( clock.now().subtract(
const Duration(days: 25), const Duration(days: 25),

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -97,7 +97,7 @@ abstract class AppLocalizations {
static const List<Locale> supportedLocales = <Locale>[ static const List<Locale> supportedLocales = <Locale>[
Locale('de'), Locale('de'),
Locale('en'), Locale('en'),
Locale('sv') Locale('sv'),
]; ];
/// No description provided for @registerTitle. /// No description provided for @registerTitle.
@ -685,7 +685,7 @@ abstract class AppLocalizations {
/// No description provided for @settingsPrivacy. /// No description provided for @settingsPrivacy.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Privacy'** /// **'Privacy & Security'**
String get settingsPrivacy; String get settingsPrivacy;
/// No description provided for @settingsPrivacyBlockUsers. /// No description provided for @settingsPrivacyBlockUsers.
@ -2104,6 +2104,12 @@ abstract class AppLocalizations {
/// **'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.'** /// **'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.'**
String get uploadLimitReached; String get uploadLimitReached;
/// No description provided for @fileLimitReached.
///
/// In en, this message translates to:
/// **'Maximum file size\nexceeded'**
String get fileLimitReached;
/// No description provided for @retransmissionRequested. /// No description provided for @retransmissionRequested.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3033,6 +3039,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Unknown contact whose identity has not yet been verified.'** /// **'Unknown contact whose identity has not yet been verified.'**
String get verificationBadgeRedDesc; String get verificationBadgeRedDesc;
/// No description provided for @chatEntryFlameRestored.
///
/// In en, this message translates to:
/// **'{count} flames restored'**
String chatEntryFlameRestored(Object count);
/// No description provided for @requestedUserToastText.
///
/// In en, this message translates to:
/// **'{username} was successfully requested.'**
String requestedUserToastText(Object username);
/// No description provided for @profileYourQrCode.
///
/// In en, this message translates to:
/// **'Your QR code'**
String get profileYourQrCode;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate
@ -3067,5 +3091,6 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue ' 'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'); 'that was used.',
);
} }

View file

@ -326,7 +326,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsAppearance => 'Erscheinungsbild'; String get settingsAppearance => 'Erscheinungsbild';
@override @override
String get settingsPrivacy => 'Datenschutz'; String get settingsPrivacy => 'Datenschutz & Sicherheit';
@override @override
String get settingsPrivacyBlockUsers => 'Benutzer blockieren'; String get settingsPrivacyBlockUsers => 'Benutzer blockieren';
@ -1116,6 +1116,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get uploadLimitReached => String get uploadLimitReached =>
'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.'; 'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.';
@override
String get fileLimitReached => 'Maximale Dateigröße\nerreicht';
@override @override
String get retransmissionRequested => 'Wird erneut versucht.'; String get retransmissionRequested => 'Wird erneut versucht.';
@ -1695,4 +1698,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.'; 'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.';
@override
String chatEntryFlameRestored(Object count) {
return '$count Flammen wiederhergestellt';
}
@override
String requestedUserToastText(Object username) {
return '$username wurde erfolgreich angefragt.';
}
@override
String get profileYourQrCode => 'Dein QR-Code';
} }

View file

@ -322,7 +322,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsAppearance => 'Appearance'; String get settingsAppearance => 'Appearance';
@override @override
String get settingsPrivacy => 'Privacy'; String get settingsPrivacy => 'Privacy & Security';
@override @override
String get settingsPrivacyBlockUsers => 'Block users'; String get settingsPrivacyBlockUsers => 'Block users';
@ -1109,6 +1109,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get uploadLimitReached => String get uploadLimitReached =>
'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.'; 'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.';
@override
String get fileLimitReached => 'Maximum file size\nexceeded';
@override @override
String get retransmissionRequested => 'Retransmission requested'; String get retransmissionRequested => 'Retransmission requested';
@ -1683,4 +1686,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.'; 'Unknown contact whose identity has not yet been verified.';
@override
String chatEntryFlameRestored(Object count) {
return '$count flames restored';
}
@override
String requestedUserToastText(Object username) {
return '$username was successfully requested.';
}
@override
String get profileYourQrCode => 'Your QR code';
} }

View file

@ -322,7 +322,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get settingsAppearance => 'Appearance'; String get settingsAppearance => 'Appearance';
@override @override
String get settingsPrivacy => 'Privacy'; String get settingsPrivacy => 'Privacy & Security';
@override @override
String get settingsPrivacyBlockUsers => 'Block users'; String get settingsPrivacyBlockUsers => 'Block users';
@ -1109,6 +1109,9 @@ class AppLocalizationsSv extends AppLocalizations {
String get uploadLimitReached => String get uploadLimitReached =>
'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.'; 'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.';
@override
String get fileLimitReached => 'Maximum file size\nexceeded';
@override @override
String get retransmissionRequested => 'Retransmission requested'; String get retransmissionRequested => 'Retransmission requested';
@ -1683,4 +1686,17 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Unknown contact whose identity has not yet been verified.'; 'Unknown contact whose identity has not yet been verified.';
@override
String chatEntryFlameRestored(Object count) {
return '$count flames restored';
}
@override
String requestedUserToastText(Object username) {
return '$username was successfully requested.';
}
@override
String get profileYourQrCode => 'Your QR code';
} }

@ -1 +1 @@
Subproject commit 6147155ce50caa97864d56e42e49a6f54702785d Subproject commit 284c602b507e77addc8f21c4fc8a321f237cac1b

View file

@ -8,14 +8,16 @@ part of 'signal_identity.dart';
SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) => SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) =>
SignalIdentity( SignalIdentity(
identityKeyPairU8List: const Uint8ListConverter() identityKeyPairU8List: const Uint8ListConverter().fromJson(
.fromJson(json['identityKeyPairU8List'] as String), json['identityKeyPairU8List'] as String,
),
registrationId: (json['registrationId'] as num).toInt(), registrationId: (json['registrationId'] as num).toInt(),
); );
Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) => Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) =>
<String, dynamic>{ <String, dynamic>{
'registrationId': instance.registrationId, 'registrationId': instance.registrationId,
'identityKeyPairU8List': 'identityKeyPairU8List': const Uint8ListConverter().toJson(
const Uint8ListConverter().toJson(instance.identityKeyPairU8List), instance.identityKeyPairU8List,
),
}; };

View file

@ -6,7 +6,8 @@ part of 'userdata.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData( UserData _$UserDataFromJson(Map<String, dynamic> json) =>
UserData(
userId: (json['userId'] as num).toInt(), userId: (json['userId'] as num).toInt(),
username: json['username'] as String, username: json['username'] as String,
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
@ -38,8 +39,10 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
.toList() .toList()
..autoDownloadOptions = ..autoDownloadOptions =
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map( (json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
(k, e) => (k, e) => MapEntry(
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()), k,
(e as List<dynamic>).map((e) => e as String).toList(),
),
) )
..storeMediaFilesInGallery = ..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? false json['storeMediaFilesInGallery'] as bool? ?? false
@ -75,7 +78,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..twonlySafeBackup = json['twonlySafeBackup'] == null ..twonlySafeBackup = json['twonlySafeBackup'] == null
? null ? null
: TwonlySafeBackup.fromJson( : TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>) json['twonlySafeBackup'] as Map<String, dynamic>,
)
..askedForUserStudyPermission = ..askedForUserStudyPermission =
json['askedForUserStudyPermission'] as bool? ?? false json['askedForUserStudyPermission'] as bool? ?? false
..userStudyParticipantsToken = ..userStudyParticipantsToken =
@ -102,8 +106,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut, 'showFeedbackShortcut': instance.showFeedbackShortcut,
'showShowImagePreviewWhenSending': 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
instance.showShowImagePreviewWhenSending,
'startWithCameraOpen': instance.startWithCameraOpen, 'startWithCameraOpen': instance.startWithCameraOpen,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions, 'autoDownloadOptions': instance.autoDownloadOptions,
@ -114,23 +117,23 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed, 'tutorialDisplayed': instance.tutorialDisplayed,
'myBestFriendGroupId': instance.myBestFriendGroupId, 'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated': 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
instance.signalLastSignedPreKeyUpdated?.toIso8601String(), ?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog, 'hideChangeLog': instance.hideChangeLog,
'updateFCMToken': instance.updateFCMToken, 'updateFCMToken': instance.updateFCMToken,
'nextTimeToShowBackupNotice': 'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice
instance.nextTimeToShowBackupNotice?.toIso8601String(), ?.toIso8601String(),
'backupServer': instance.backupServer, 'backupServer': instance.backupServer,
'twonlySafeBackup': instance.twonlySafeBackup, 'twonlySafeBackup': instance.twonlySafeBackup,
'askedForUserStudyPermission': instance.askedForUserStudyPermission, 'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'lastUserStudyDataUpload': 'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
instance.lastUserStudyDataUpload?.toIso8601String(), ?.toIso8601String(),
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
ThemeMode.system: 'system', ThemeMode.system: 'system',
@ -148,8 +151,10 @@ TwonlySafeBackup _$TwonlySafeBackupFromJson(Map<String, dynamic> json) =>
.toList(), .toList(),
) )
..lastBackupSize = (json['lastBackupSize'] as num).toInt() ..lastBackupSize = (json['lastBackupSize'] as num).toInt()
..backupUploadState = ..backupUploadState = $enumDecode(
$enumDecode(_$LastBackupUploadStateEnumMap, json['backupUploadState']) _$LastBackupUploadStateEnumMap,
json['backupUploadState'],
)
..lastBackupDone = json['lastBackupDone'] == null ..lastBackupDone = json['lastBackupDone'] == null
? null ? null
: DateTime.parse(json['lastBackupDone'] as String); : DateTime.parse(json['lastBackupDone'] as String);
@ -175,7 +180,7 @@ BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
serverUrl: json['serverUrl'] as String, serverUrl: json['serverUrl'] as String,
retentionDays: (json['retentionDays'] as num).toInt(), retentionDays: (json['retentionDays'] as num).toInt(),
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(), maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
); );
Map<String, dynamic> _$BackupServerToJson(BackupServer instance) => Map<String, dynamic> _$BackupServerToJson(BackupServer instance) =>
<String, dynamic>{ <String, dynamic>{

View file

@ -536,12 +536,14 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$core.List<$core.int>? authToken, $core.List<$core.int>? authToken,
$core.String? appVersion, $core.String? appVersion,
$fixnum.Int64? deviceId, $fixnum.Int64? deviceId,
$core.bool? inBackground,
}) { }) {
final result = create(); final result = create();
if (userId != null) result.userId = userId; if (userId != null) result.userId = userId;
if (authToken != null) result.authToken = authToken; if (authToken != null) result.authToken = authToken;
if (appVersion != null) result.appVersion = appVersion; if (appVersion != null) result.appVersion = appVersion;
if (deviceId != null) result.deviceId = deviceId; if (deviceId != null) result.deviceId = deviceId;
if (inBackground != null) result.inBackground = inBackground;
return result; return result;
} }
@ -564,6 +566,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
2, _omitFieldNames ? '' : 'authToken', $pb.PbFieldType.OY) 2, _omitFieldNames ? '' : 'authToken', $pb.PbFieldType.OY)
..aOS(3, _omitFieldNames ? '' : 'appVersion') ..aOS(3, _omitFieldNames ? '' : 'appVersion')
..aInt64(4, _omitFieldNames ? '' : 'deviceId') ..aInt64(4, _omitFieldNames ? '' : 'deviceId')
..aOB(5, _omitFieldNames ? '' : 'inBackground')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -624,6 +627,15 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
$core.bool hasDeviceId() => $_has(3); $core.bool hasDeviceId() => $_has(3);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearDeviceId() => $_clearField(4); void clearDeviceId() => $_clearField(4);
@$pb.TagNumber(5)
$core.bool get inBackground => $_getBF(4);
@$pb.TagNumber(5)
set inBackground($core.bool value) => $_setBool(4, value);
@$pb.TagNumber(5)
$core.bool hasInBackground() => $_has(4);
@$pb.TagNumber(5)
void clearInBackground() => $_clearField(5);
} }
enum Handshake_Handshake { enum Handshake_Handshake {

View file

@ -229,10 +229,20 @@ const Handshake_Authenticate$json = {
'10': 'deviceId', '10': 'deviceId',
'17': true '17': true
}, },
{
'1': 'in_background',
'3': 5,
'4': 1,
'5': 8,
'9': 2,
'10': 'inBackground',
'17': true
},
], ],
'8': [ '8': [
{'1': '_app_version'}, {'1': '_app_version'},
{'1': '_device_id'}, {'1': '_device_id'},
{'1': '_in_background'},
], ],
}; };
@ -254,10 +264,11 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK' 'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK'
'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX' 'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX'
'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS' 'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS'
'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGqwBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB' 'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB'
'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy' 'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy'
'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2' 'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2'
'VJZIgBAUIOCgxfYXBwX3ZlcnNpb25CDAoKX2RldmljZV9pZEILCglIYW5kc2hha2U='); 'VJZIgBARIoCg1pbl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBw'
'X3ZlcnNpb25CDAoKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZEILCglIYW5kc2hha2U=');
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = { const ApplicationData$json = {

View file

@ -91,6 +91,8 @@ class ErrorCode extends $pb.ProtobufEnum {
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired'); ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
static const ErrorCode UserIsNotInFreePlan = static const ErrorCode UserIsNotInFreePlan =
ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan'); ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan');
static const ErrorCode ForegroundSessionConnected =
ErrorCode._(1036, _omitEnumNames ? '' : 'ForegroundSessionConnected');
static const $core.List<ErrorCode> values = <ErrorCode>[ static const $core.List<ErrorCode> values = <ErrorCode>[
Unknown, Unknown,
@ -131,6 +133,7 @@ class ErrorCode extends $pb.ProtobufEnum {
RegistrationDisabled, RegistrationDisabled,
IPAPaymentExpired, IPAPaymentExpired,
UserIsNotInFreePlan, UserIsNotInFreePlan,
ForegroundSessionConnected,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = static final $core.Map<$core.int, ErrorCode> _byValue =

View file

@ -56,6 +56,7 @@ const ErrorCode$json = {
{'1': 'RegistrationDisabled', '2': 1033}, {'1': 'RegistrationDisabled', '2': 1033},
{'1': 'IPAPaymentExpired', '2': 1034}, {'1': 'IPAPaymentExpired', '2': 1034},
{'1': 'UserIsNotInFreePlan', '2': 1035}, {'1': 'UserIsNotInFreePlan', '2': 1035},
{'1': 'ForegroundSessionConnected', '2': 1036},
], ],
}; };
@ -77,4 +78,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu' 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh' 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ' 'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsI'); 'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsIEh8KGkZvcmVncm'
'91bmRTZXNzaW9uQ29ubmVjdGVkEIwI');

View file

@ -10,9 +10,11 @@ message AdditionalMessageData {
enum Type { enum Type {
LINK = 0; LINK = 0;
CONTACTS = 1; CONTACTS = 1;
RESTORED_FLAME_COUNTER = 2;
} }
Type type = 1; Type type = 1;
optional string link = 2; optional string link = 2;
repeated SharedContact contacts = 3; repeated SharedContact contacts = 3;
optional int64 restored_flame_counter = 4;
} }

View file

@ -106,11 +106,14 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
AdditionalMessageData_Type? type, AdditionalMessageData_Type? type,
$core.String? link, $core.String? link,
$core.Iterable<SharedContact>? contacts, $core.Iterable<SharedContact>? contacts,
$fixnum.Int64? restoredFlameCounter,
}) { }) {
final result = create(); final result = create();
if (type != null) result.type = type; if (type != null) result.type = type;
if (link != null) result.link = link; if (link != null) result.link = link;
if (contacts != null) result.contacts.addAll(contacts); if (contacts != null) result.contacts.addAll(contacts);
if (restoredFlameCounter != null)
result.restoredFlameCounter = restoredFlameCounter;
return result; return result;
} }
@ -135,6 +138,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
..pc<SharedContact>( ..pc<SharedContact>(
3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM, 3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM,
subBuilder: SharedContact.create) subBuilder: SharedContact.create)
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -180,6 +184,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
@$pb.TagNumber(3) @$pb.TagNumber(3)
$pb.PbList<SharedContact> get contacts => $_getList(2); $pb.PbList<SharedContact> get contacts => $_getList(2);
@$pb.TagNumber(4)
$fixnum.Int64 get restoredFlameCounter => $_getI64(3);
@$pb.TagNumber(4)
set restoredFlameCounter($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasRestoredFlameCounter() => $_has(3);
@$pb.TagNumber(4)
void clearRestoredFlameCounter() => $_clearField(4);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -19,15 +19,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK'); AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK');
static const AdditionalMessageData_Type CONTACTS = static const AdditionalMessageData_Type CONTACTS =
AdditionalMessageData_Type._(1, _omitEnumNames ? '' : 'CONTACTS'); AdditionalMessageData_Type._(1, _omitEnumNames ? '' : 'CONTACTS');
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
AdditionalMessageData_Type._(
2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER');
static const $core.List<AdditionalMessageData_Type> values = static const $core.List<AdditionalMessageData_Type> values =
<AdditionalMessageData_Type>[ <AdditionalMessageData_Type>[
LINK, LINK,
CONTACTS, CONTACTS,
RESTORED_FLAME_COUNTER,
]; ];
static final $core.List<AdditionalMessageData_Type?> _byValue = static final $core.List<AdditionalMessageData_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 1); $pb.ProtobufEnum.$_initByValueList(values, 2);
static AdditionalMessageData_Type? valueOf($core.int value) => static AdditionalMessageData_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -57,10 +57,20 @@ const AdditionalMessageData$json = {
'6': '.SharedContact', '6': '.SharedContact',
'10': 'contacts' '10': 'contacts'
}, },
{
'1': 'restored_flame_counter',
'3': 4,
'4': 1,
'5': 3,
'9': 1,
'10': 'restoredFlameCounter',
'17': true
},
], ],
'4': [AdditionalMessageData_Type$json], '4': [AdditionalMessageData_Type$json],
'8': [ '8': [
{'1': '_link'}, {'1': '_link'},
{'1': '_restored_flame_counter'},
], ],
}; };
@ -70,6 +80,7 @@ const AdditionalMessageData_Type$json = {
'2': [ '2': [
{'1': 'LINK', '2': 0}, {'1': 'LINK', '2': 0},
{'1': 'CONTACTS', '2': 1}, {'1': 'CONTACTS', '2': 1},
{'1': 'RESTORED_FLAME_COUNTER', '2': 2},
], ],
}; };
@ -77,5 +88,7 @@ const AdditionalMessageData_Type$json = {
final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Decode(
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD' 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzIh4KBFR5cGUSCAoETElOSxAAEgwKCENPTl' 'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
'RBQ1RTEAFCBwoFX2xpbms='); 'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI'
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3'
'RvcmVkX2ZsYW1lX2NvdW50ZXI=');

View file

@ -30,9 +30,9 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
@ -46,6 +46,7 @@ import 'package:web_socket_channel/io.dart';
final lockConnecting = Mutex(); final lockConnecting = Mutex();
final lockRetransStore = Mutex(); final lockRetransStore = Mutex();
final lockAuthentication = Mutex();
/// The ApiProvider is responsible for communicating with the server. /// The ApiProvider is responsible for communicating with the server.
/// It handles errors and does automatically tries to reconnect on /// It handles errors and does automatically tries to reconnect on
@ -86,11 +87,14 @@ class ApiService {
// Function is called after the user is authenticated at the server // Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async { Future<void> onAuthenticated() async {
isAuthenticated = true;
await initFCMAfterAuthenticated(); await initFCMAfterAuthenticated();
globalCallbackConnectionState(isConnected: true); globalCallbackConnectionState(isConnected: true);
if (!globalIsAppInBackground) { if (globalIsInBackgroundTask) {
await retransmitRawBytes();
await tryTransmitMessages();
await tryDownloadAllMediaFiles();
} else if (!globalIsAppInBackground) {
unawaited(retransmitRawBytes()); unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages()); unawaited(tryTransmitMessages());
unawaited(tryDownloadAllMediaFiles()); unawaited(tryDownloadAllMediaFiles());
@ -124,6 +128,7 @@ class ApiService {
} }
Future<void> startReconnectionTimer() async { Future<void> startReconnectionTimer() async {
if (globalIsInBackgroundTask) return;
if (reconnectionTimer?.isActive ?? false) { if (reconnectionTimer?.isActive ?? false) {
return; return;
} }
@ -152,8 +157,9 @@ class ApiService {
if (connectivitySubscription != null) { if (connectivitySubscription != null) {
return; return;
} }
connectivitySubscription = connectivitySubscription = Connectivity().onConnectivityChanged.listen((
Connectivity().onConnectivityChanged.listen((result) async { result,
) async {
if (!result.contains(ConnectivityResult.none)) { if (!result.contains(ConnectivityResult.none)) {
await connect(); await connect();
} }
@ -330,7 +336,9 @@ class ApiService {
} }
} }
if (res.isError) { if (res.isError) {
if (res.error != ErrorCode.ForegroundSessionConnected) {
Log.warn('Got error from server: ${res.error}'); Log.warn('Got error from server: ${res.error}');
}
if (res.error == ErrorCode.AppVersionOutdated) { if (res.error == ErrorCode.AppVersionOutdated) {
globalCallbackAppIsOutdated(); globalCallbackAppIsOutdated();
Log.warn('App Version is OUTDATED.'); Log.warn('App Version is OUTDATED.');
@ -348,7 +356,6 @@ class ApiService {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
if (res.error == ErrorCode.SessionNotAuthenticated) { if (res.error == ErrorCode.SessionNotAuthenticated) {
isAuthenticated = false;
if (authenticated) { if (authenticated) {
await authenticate(); await authenticate();
if (isAuthenticated) { if (isAuthenticated) {
@ -380,8 +387,9 @@ class ApiService {
Future<bool> tryAuthenticateWithToken(int userId) async { Future<bool> tryAuthenticateWithToken(int userId) async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final apiAuthToken = final apiAuthToken = await storage.read(
await storage.read(key: SecureStorageKeys.apiAuthToken); key: SecureStorageKeys.apiAuthToken,
);
final user = await getUser(); final user = await getUser();
if (apiAuthToken != null && user != null) { if (apiAuthToken != null && user != null) {
@ -395,6 +403,7 @@ class ApiService {
..userId = Int64(userId) ..userId = Int64(userId)
..appVersion = (await PackageInfo.fromPlatform()).version ..appVersion = (await PackageInfo.fromPlatform()).version
..deviceId = Int64(user.deviceId) ..deviceId = Int64(user.deviceId)
..inBackground = globalIsInBackgroundTask
..authToken = base64Decode(apiAuthToken); ..authToken = base64Decode(apiAuthToken);
final handshake = Handshake()..authenticate = authenticate; final handshake = Handshake()..authenticate = authenticate;
@ -404,12 +413,20 @@ class ApiService {
if (result.isSuccess) { if (result.isSuccess) {
Log.info('websocket is authenticated'); Log.info('websocket is authenticated');
isAuthenticated = true;
if (globalIsInBackgroundTask) {
await onAuthenticated();
} else {
unawaited(onAuthenticated()); unawaited(onAuthenticated());
}
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid) { if (result.error != ErrorCode.AuthTokenNotValid &&
Log.error('got error while authenticating to the server: $result'); result.error != ErrorCode.ForegroundSessionConnected) {
Log.error(
'got error while authenticating to the server: ${result.error}',
);
return false; return false;
} }
} }
@ -418,6 +435,7 @@ class ApiService {
} }
Future<void> authenticate() async { Future<void> authenticate() async {
return lockAuthentication.protect(() async {
if (isAuthenticated) return; if (isAuthenticated) return;
if (await getSignalIdentity() == null) { if (await getSignalIdentity() == null) {
return; return;
@ -445,7 +463,11 @@ class ApiService {
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return; if (privKey == null) return;
final random = getRandomUint8List(32); final random = getRandomUint8List(32);
final signature = sign(privKey.serialize(), challenge as Uint8List, random); final signature = sign(
privKey.serialize(),
challenge as Uint8List,
random,
);
privKey = null; privKey = null;
final getAuthToken = Handshake_GetAuthToken() final getAuthToken = Handshake_GetAuthToken()
@ -472,6 +494,7 @@ class ApiService {
); );
await tryAuthenticateWithToken(userData.userId); await tryAuthenticateWithToken(userData.userId);
});
} }
Future<Result> register( Future<Result> register(
@ -490,8 +513,9 @@ class ApiService {
final register = Handshake_Register() final register = Handshake_Register()
..username = username ..username = username
..publicIdentityKey = ..publicIdentityKey = (await signalStore.getIdentityKeyPair())
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize() .getPublicKey()
.serialize()
..registrationId = Int64(signalIdentity.registrationId) ..registrationId = Int64(signalIdentity.registrationId)
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
..signedPrekeySignature = signedPreKey.signature ..signedPrekeySignature = signedPreKey.signature
@ -511,8 +535,10 @@ class ApiService {
} }
Future<void> checkForDeletedUsernames() async { Future<void> checkForDeletedUsernames() async {
final users = await twonlyDB.contactsDao final users = await twonlyDB.contactsDao.getContactsByUsername(
.getContactsByUsername('[deleted]', username2: '[Unknown]'); '[deleted]',
username2: '[Unknown]',
);
for (final user in users) { for (final user in users) {
final userData = await getUserById(user.userId); final userData = await getUserById(user.userId);
if (userData != null) { if (userData != null) {

View file

@ -18,7 +18,15 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
if (contact != null) { if (contact != null) {
if (contact.accepted) { // Either the contact has accepted the fromUserId already: Then just blindly accept the request.
// Or the user has also requested fromUserId. This means that both user have requested each other (while been
// offline for example): In this case the contact can also be accepted blindly.
if (contact.accepted || (!contact.requested && !contact.deletedByUser)) {
if (!contact.accepted) {
// User has also requested the fromUserId, so mark the user as accepted.
await handleContactAccept(fromUserId);
}
// contact was already accepted, so just accept the request in the background. // contact was already accepted, so just accept the request in the background.
await sendCipherText( await sendCipherText(
contact.userId, contact.userId,
@ -50,16 +58,7 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
return true; return true;
} }
Future<bool> handleContactRequest( Future<void> handleContactAccept(int fromUserId) async {
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId');
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
fromUserId, fromUserId,
const ContactsCompanion( const ContactsCompanion(
@ -79,6 +78,19 @@ Future<bool> handleContactRequest(
), ),
); );
} }
}
Future<bool> handleContactRequest(
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId');
await handleContactAccept(fromUserId);
case EncryptedContent_ContactRequest_Type.REJECT: case EncryptedContent_ContactRequest_Type.REJECT:
Log.info('Got a contact reject from $fromUserId'); Log.info('Got a contact reject from $fromUserId');
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
@ -125,12 +137,12 @@ Future<void> handleContactUpdate(
} }
Future<void> handleFlameSync( Future<void> handleFlameSync(
int contactId, String groupId,
EncryptedContent_FlameSync flameSync, EncryptedContent_FlameSync flameSync,
) async { ) async {
Log.info('Got a flameSync from $contactId'); Log.info('Got a flameSync for group $groupId');
final group = await twonlyDB.groupsDao.getDirectChat(contactId); final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null || group.lastFlameCounterChange == null) return; if (group == null || group.lastFlameCounterChange == null) return;
var updates = GroupsCompanion( var updates = GroupsCompanion(

View file

@ -4,7 +4,8 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.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/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
hide Message;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
@ -31,7 +32,7 @@ Future<void> handleMedia(
message.senderId != fromUserId || message.senderId != fromUserId ||
message.mediaId == null) { message.mediaId == null) {
Log.warn( Log.warn(
'Got reupload for a message that either does not exists or sender != fromUserId or not a media file', 'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
); );
return; return;
} }
@ -53,8 +54,9 @@ Future<void> handleMedia(
), ),
); );
final mediaFile = final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); message.mediaId!,
);
if (mediaFile != null) { if (mediaFile != null) {
unawaited(startDownloadMedia(mediaFile, false)); unawaited(startDownloadMedia(mediaFile, false));
@ -89,7 +91,11 @@ Future<void> handleMedia(
} }
} }
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( late MediaFile? mediaFile;
late Message? message;
await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion( MediaFilesCompanion(
downloadState: const Value(DownloadState.pending), downloadState: const Value(DownloadState.pending),
type: Value(mediaType), type: Value(mediaType),
@ -110,12 +116,12 @@ Future<void> handleMedia(
return; return;
} }
final message = await twonlyDB.messagesDao.insertMessage( message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
messageId: Value(media.senderMessageId), messageId: Value(media.senderMessageId),
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaFile.mediaId), mediaId: Value(mediaFile!.mediaId),
type: Value(MessageType.media.name), type: Value(MessageType.media.name),
additionalMessageData: Value.absentIfNull( additionalMessageData: Value.absentIfNull(
media.hasAdditionalMessageData() media.hasAdditionalMessageData()
@ -128,17 +134,21 @@ Future<void> handleMedia(
createdAt: Value(fromTimestamp(media.timestamp)), createdAt: Value(fromTimestamp(media.timestamp)),
), ),
); );
});
if (message != null) { if (message != null) {
await twonlyDB.groupsDao await twonlyDB.groupsDao.increaseLastMessageExchange(
.increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); groupId,
Log.info('Inserted a new media message with ID: ${message.messageId}'); fromTimestamp(media.timestamp),
);
Log.info('Inserted a new media message with ID: ${message!.messageId}');
await incFlameCounter( await incFlameCounter(
message.groupId, message!.groupId,
true, true,
fromTimestamp(media.timestamp), fromTimestamp(media.timestamp),
); );
unawaited(startDownloadMedia(mediaFile, false)); unawaited(startDownloadMedia(mediaFile!, false));
} }
} }
@ -163,8 +173,9 @@ Future<void> handleMediaUpdate(
); );
return; return;
} }
final mediaFile = final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); message.mediaId!,
);
if (mediaFile == null) { if (mediaFile == null) {
Log.info( Log.info(
'Got media file update, but media file was not found ${message.mediaId}', 'Got media file update, but media file was not found ${message.mediaId}',
@ -203,8 +214,9 @@ Future<void> handleMediaUpdate(
reuploadRequestedBy: Value(reuploadRequestedBy), reuploadRequestedBy: Value(reuploadRequestedBy),
), ),
); );
final mediaFileUpdated = final mediaFileUpdated = await MediaFileService.fromMediaId(
await MediaFileService.fromMediaId(mediaFile.mediaId); mediaFile.mediaId,
);
if (mediaFileUpdated != null) { if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) { if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync(); mediaFileUpdated.uploadRequestPath.deleteSync();

View file

@ -1,6 +1,7 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleReaction( Future<void> handleReaction(
@ -17,6 +18,8 @@ Future<void> handleReaction(
reaction.remove, reaction.remove,
); );
await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId);
if (!reaction.remove) { if (!reaction.remove) {
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
} }

View file

@ -19,8 +19,8 @@ import 'package:twonly/src/utils/misc.dart';
Future<void> tryDownloadAllMediaFiles({bool force = false}) async { Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted. // This is called when WebSocket is newly connected, so allow all downloads to be restarted.
final mediaFiles = final mediaFiles = await twonlyDB.mediaFilesDao
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload(); .getAllMediaFilesPendingDownload();
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
if (await canMediaFileBeDownloaded(mediaFile)) { if (await canMediaFileBeDownloaded(mediaFile)) {
@ -30,8 +30,9 @@ Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
} }
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async { Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(mediaFile.mediaId); mediaFile.mediaId,
);
// Verify that the sender of the original image / message does still exists. // Verify that the sender of the original image / message does still exists.
// If not delete the message as it can not be downloaded from the server anymore. // If not delete the message as it can not be downloaded from the server anymore.
@ -56,8 +57,9 @@ Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
return false; return false;
} }
final contact = final contact = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(messages.first.senderId!); messages.first.senderId!,
);
if (contact == null || contact.accountDeleted) { if (contact == null || contact.accountDeleted) {
Log.info( Log.info(
@ -98,23 +100,27 @@ Future<bool> isAllowedToDownload(MediaType type) async {
if (connectivityResult.contains(ConnectivityResult.mobile)) { if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (type == MediaType.video) { if (type == MediaType.video) {
if (options[ConnectivityResult.mobile.name]! if (options[ConnectivityResult.mobile.name]!.contains(
.contains(DownloadMediaTypes.video.name)) { DownloadMediaTypes.video.name,
)) {
return true; return true;
} }
} else if (options[ConnectivityResult.mobile.name]! } else if (options[ConnectivityResult.mobile.name]!.contains(
.contains(DownloadMediaTypes.image.name)) { DownloadMediaTypes.image.name,
)) {
return true; return true;
} }
} }
if (connectivityResult.contains(ConnectivityResult.wifi)) { if (connectivityResult.contains(ConnectivityResult.wifi)) {
if (type == MediaType.video) { if (type == MediaType.video) {
if (options[ConnectivityResult.wifi.name]! if (options[ConnectivityResult.wifi.name]!.contains(
.contains(DownloadMediaTypes.video.name)) { DownloadMediaTypes.video.name,
)) {
return true; return true;
} }
} else if (options[ConnectivityResult.wifi.name]! } else if (options[ConnectivityResult.wifi.name]!.contains(
.contains(DownloadMediaTypes.image.name)) { DownloadMediaTypes.image.name,
)) {
return true; return true;
} }
} }
@ -230,8 +236,9 @@ Future<void> downloadFileFast(
String apiUrl, String apiUrl,
File filePath, File filePath,
) async { ) async {
final response = final response = await http
await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); .get(Uri.parse(apiUrl))
.timeout(const Duration(seconds: 30));
if (response.statusCode == 200) { if (response.statusCode == 200) {
await filePath.writeAsBytes(response.bodyBytes); await filePath.writeAsBytes(response.bodyBytes);
@ -308,8 +315,10 @@ Future<void> handleEncryptedFile(String mediaId) async {
mac: Mac(mediaService.mediaFile.encryptionMac!), mac: Mac(mediaService.mediaFile.encryptionMac!),
); );
final plaintextBytes = final plaintextBytes = await chacha20.decrypt(
await chacha20.decrypt(secretBox, secretKey: secretKeyData); secretBox,
secretKey: secretKeyData,
);
final rawMediaBytes = Uint8List.fromList(plaintextBytes); final rawMediaBytes = Uint8List.fromList(plaintextBytes);
@ -337,8 +346,8 @@ Future<void> handleEncryptedFile(String mediaId) async {
} }
Future<void> makeMigrationToVersion91() async { Future<void> makeMigrationToVersion91() async {
final messages = final messages = await twonlyDB.mediaFilesDao
await twonlyDB.mediaFilesDao.getAllMediaFilesReuploadRequested(); .getAllMediaFilesReuploadRequested();
for (final message in messages) { for (final message in messages) {
await requestMediaReupload(message.mediaId); await requestMediaReupload(message.mediaId);
} }

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -8,8 +8,8 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> initFileDownloader() async { Future<void> initFileDownloader() async {
@ -74,30 +74,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.complete) { if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) { if (update.responseStatusCode == 200) {
Log.info('Upload of ${media.mediaId} success!'); Log.info('Upload of ${media.mediaId} success!');
await markUploadAsSuccessful(media);
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
),
);
/// As the messages where send in a bulk acknowledge all messages.
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
for (final message in messages) {
final contacts =
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId);
for (final contact in contacts) {
await twonlyDB.messagesDao.handleMessageAckByServer(
contact.contactId,
message.messageId,
clock.now(),
);
}
}
return; return;
} }
Log.error( Log.error(
@ -122,6 +99,20 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ', 'Background status $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
); );
if (update.status == TaskStatus.waitingToRetry) {
if (update.responseStatusCode == 401) {
// auth token is not valid, so either create a new task with a new token, or cancel task
final mediaService = MediaFileService(media);
await FileDownloader().cancelTaskWithId(update.task.taskId);
Log.info('Cancel task, already uploaded or will be reuploaded');
if (mediaService.mediaFile.uploadState != UploadState.uploaded) {
await mediaService.setUploadState(UploadState.uploading);
// In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService);
}
}
}
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error( Log.error(
@ -129,8 +120,11 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
); );
final mediaService = MediaFileService(media); final mediaService = MediaFileService(media);
// in case the media file is already uploaded to not reqtry
if (mediaService.mediaFile.uploadState != UploadState.uploaded) {
await mediaService.setUploadState(UploadState.uploading); await mediaService.setUploadState(UploadState.uploading);
// In all other cases just try the upload again... // In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService); await startBackgroundMediaUpload(mediaService);
} }
}
} }

View file

@ -24,10 +24,13 @@ import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:workmanager/workmanager.dart' hide TaskStatus;
Future<void> finishStartedPreprocessing() async { Future<void> finishStartedPreprocessing() async {
final mediaFiles = final mediaFiles = await twonlyDB.mediaFilesDao
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); .getAllMediaFilesPendingUpload();
Log.info('There are ${mediaFiles.length} media files pending');
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
if (mediaFile.isDraftMedia) { if (mediaFile.isDraftMedia) {
@ -55,6 +58,50 @@ Future<void> finishStartedPreprocessing() async {
} }
} }
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
/// For example because the background_downloader plugin has not yet reported the finished upload.
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded.
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null || message.mediaId == null) return;
final media = await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (media == null) return;
if (media.uploadState != UploadState.uploaded) {
Log.info('Media was not yet marked as uploaded. Doing it now.');
await markUploadAsSuccessful(media);
}
}
Future<void> markUploadAsSuccessful(MediaFile media) async {
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
),
);
/// As the messages where send in a bulk acknowledge all messages.
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
media.mediaId,
);
for (final message in messages) {
final contacts = await twonlyDB.groupsDao.getGroupNonLeftMembers(
message.groupId,
);
for (final contact in contacts) {
await twonlyDB.messagesDao.handleMessageAckByServer(
contact.contactId,
message.messageId,
clock.now(),
);
}
}
}
Future<MediaFileService?> initializeMediaUpload( Future<MediaFileService?> initializeMediaUpload(
MediaType type, MediaType type,
int? displayLimitInMilliseconds, { int? displayLimitInMilliseconds, {
@ -103,8 +150,9 @@ Future<void> insertMediaFileInMessagesTable(
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaService.mediaFile.mediaId), mediaId: Value(mediaService.mediaFile.mediaId),
type: Value(MessageType.media.name), type: Value(MessageType.media.name),
additionalMessageData: additionalMessageData: Value.absentIfNull(
Value.absentIfNull(additionalData?.writeToBuffer()), additionalData?.writeToBuffer(),
),
), ),
); );
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
@ -192,8 +240,9 @@ Future<void> _encryptMediaFiles(MediaFileService mediaService) async {
await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes)); await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes));
mediaService.encryptedPath mediaService.encryptedPath.writeAsBytesSync(
.writeAsBytesSync(Uint8List.fromList(secretBox.cipherText)); Uint8List.fromList(secretBox.cipherText),
);
} }
Future<void> _createUploadRequest(MediaFileService media) async { Future<void> _createUploadRequest(MediaFileService media) async {
@ -201,8 +250,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final messagesOnSuccess = <TextMessage>[]; final messagesOnSuccess = <TextMessage>[];
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId); media.mediaFile.mediaId,
);
if (messages.isEmpty) { if (messages.isEmpty) {
// There where no user selected who should receive the image, so waiting with this step... // There where no user selected who should receive the image, so waiting with this step...
@ -210,8 +260,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
} }
for (final message in messages) { for (final message in messages) {
final groupMembers = final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); message.groupId,
);
if (media.mediaFile.reuploadRequestedBy == null) { if (media.mediaFile.reuploadRequestedBy == null) {
await incFlameCounter(message.groupId, false, message.createdAt); await incFlameCounter(message.groupId, false, message.createdAt);
@ -220,8 +271,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
for (final groupMember in groupMembers) { for (final groupMember in groupMembers) {
/// only send the upload to the users /// only send the upload to the users
if (media.mediaFile.reuploadRequestedBy != null) { if (media.mediaFile.reuploadRequestedBy != null) {
if (!media.mediaFile.reuploadRequestedBy! if (!media.mediaFile.reuploadRequestedBy!.contains(
.contains(groupMember.contactId)) { groupMember.contactId,
)) {
continue; continue;
} }
} }
@ -260,8 +312,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
); );
if (media.mediaFile.displayLimitInMilliseconds != null) { if (media.mediaFile.displayLimitInMilliseconds != null) {
notEncryptedContent.media.displayLimitInMilliseconds = notEncryptedContent.media.displayLimitInMilliseconds = Int64(
Int64(media.mediaFile.displayLimitInMilliseconds!); media.mediaFile.displayLimitInMilliseconds!,
);
} }
final cipherText = await sendCipherText( final cipherText = await sendCipherText(
@ -299,6 +352,19 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer(); final uploadRequestBytes = uploadRequest.writeToBuffer();
if (uploadRequestBytes.length > 49_000_000) {
await media.setUploadState(UploadState.fileLimitReached);
await twonlyDB.messagesDao.updateMessagesByMediaId(
media.mediaFile.mediaId,
MessagesCompanion(
openedAt: Value(DateTime.now()),
ackByServer: Value(DateTime.now()),
),
);
return;
}
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
} }
@ -306,8 +372,9 @@ Mutex protectUpload = Mutex();
Future<void> _uploadUploadRequest(MediaFileService media) async { Future<void> _uploadUploadRequest(MediaFileService media) async {
await protectUpload.protect(() async { await protectUpload.protect(() async {
final currentMedia = final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId); media.mediaFile.mediaId,
);
if (currentMedia == null || if (currentMedia == null ||
currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) { currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) {
@ -315,8 +382,9 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
return null; return null;
} }
final apiAuthTokenRaw = await const FlutterSecureStorage() final apiAuthTokenRaw = await const FlutterSecureStorage().read(
.read(key: SecureStorageKeys.apiAuthToken); key: SecureStorageKeys.apiAuthToken,
);
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {
Log.error('api auth token not defined.'); Log.error('api auth token not defined.');
@ -344,7 +412,8 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
if (!connectivityResult.contains(ConnectivityResult.mobile) && if (globalIsInBackgroundTask ||
!connectivityResult.contains(ConnectivityResult.mobile) &&
!connectivityResult.contains(ConnectivityResult.wifi)) { !connectivityResult.contains(ConnectivityResult.wifi)) {
// no internet, directly put it into the background... // no internet, directly put it into the background...
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
@ -376,15 +445,30 @@ Future<void> uploadFileFastOrEnqueue(
); );
try { try {
final workmanagerUniqueName =
'progressing_finish_uploads_${media.mediaFile.mediaId}';
await Workmanager().registerOneOffTask(
workmanagerUniqueName,
'eu.twonly.processing_task',
initialDelay: const Duration(minutes: 15),
constraints: Constraints(
networkType: NetworkType.connected,
),
);
Log.info('Uploading fast: ${task.taskId}'); Log.info('Uploading fast: ${task.taskId}');
final response =
await requestMultipart.send().timeout(const Duration(seconds: 8)); final response = await requestMultipart.send();
var status = TaskStatus.failed; var status = TaskStatus.failed;
if (response.statusCode == 200) { if (response.statusCode == 200) {
status = TaskStatus.complete; status = TaskStatus.complete;
} else if (response.statusCode == 404) { } else if (response.statusCode == 404) {
status = TaskStatus.notFound; status = TaskStatus.notFound;
} }
await Workmanager().cancelByUniqueName(workmanagerUniqueName);
await handleUploadStatusUpdate( await handleUploadStatusUpdate(
TaskStatusUpdate( TaskStatusUpdate(
task, task,

View file

@ -79,8 +79,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
// ignore: parameter_assignments // ignore: parameter_assignments
receiptId = receipt.receiptId; receiptId = receipt.receiptId;
final contact = final contact = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(receipt.contactId); receipt.contactId,
);
if (contact == null || contact.accountDeleted) { if (contact == null || contact.accountDeleted) {
Log.warn('Will not send message again as user does not exist anymore.'); Log.warn('Will not send message again as user does not exist anymore.');
await twonlyDB.receiptsDao.deleteReceipt(receiptId); await twonlyDB.receiptsDao.deleteReceipt(receiptId);
@ -99,8 +100,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
final message = pb.Message.fromBuffer(receipt.message) final message = pb.Message.fromBuffer(receipt.message)
..receiptId = receiptId; ..receiptId = receiptId;
final encryptedContent = final encryptedContent = pb.EncryptedContent.fromBuffer(
pb.EncryptedContent.fromBuffer(message.encryptedContent); message.encryptedContent,
);
final pushNotification = await getPushNotificationFromEncryptedContent( final pushNotification = await getPushNotificationFromEncryptedContent(
receipt.contactId, receipt.contactId,
@ -111,8 +113,10 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
Uint8List? pushData; Uint8List? pushData;
if (pushNotification != null && receipt.retryCount <= 3) { if (pushNotification != null && receipt.retryCount <= 3) {
/// In case the message has to be resend more than three times, do not show a notification again... /// In case the message has to be resend more than three times, do not show a notification again...
pushData = pushData = await encryptPushNotification(
await encryptPushNotification(receipt.contactId, pushNotification); receipt.contactId,
pushNotification,
);
} }
if (message.type == pb.Message_Type.TEST_NOTIFICATION) { if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
@ -331,7 +335,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
contactId: Value(contactId), contactId: Value(contactId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
messageId: Value(messageId), messageId: Value(messageId),
ackByServerAt: Value(onlyReturnEncryptedData ? clock.now() : null), willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
), ),
); );

View file

@ -26,6 +26,7 @@ import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -62,6 +63,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
..response = response; ..response = response;
await apiService.sendResponse(ClientToServer()..v0 = v0); await apiService.sendResponse(ClientToServer()..v0 = v0);
globalGotMessageFromServer = true;
}); });
} }
@ -94,6 +96,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
var retry = false; var retry = false;
if (message.hasPlaintextContent()) { if (message.hasPlaintextContent()) {
if (message.plaintextContent.hasDecryptionErrorMessage()) { if (message.plaintextContent.hasDecryptionErrorMessage()) {
if (message.plaintextContent.decryptionErrorMessage.type ==
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN) {
// Get a new prekey from the server, and establish a new signal session.
await handleSessionResync(fromUserId);
}
Log.info( Log.info(
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
); );
@ -251,11 +258,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null); return (null, null);
} }
if (content.hasFlameSync()) {
await handleFlameSync(fromUserId, content.flameSync);
return (null, null);
}
if (content.hasPushKeys()) { if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys); await handlePushKey(fromUserId, content.pushKeys);
return (null, null); return (null, null);
@ -340,6 +342,11 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
} }
} }
if (content.hasFlameSync()) {
await handleFlameSync(content.groupId, content.flameSync);
return (null, null);
}
if (content.hasGroupUpdate()) { if (content.hasGroupUpdate()) {
await handleGroupUpdate( await handleGroupUpdate(
fromUserId, fromUserId,

View file

@ -81,14 +81,18 @@ Future<void> handleMediaError(MediaFile media) async {
); );
} }
Future<void> importSignalContactAndCreateRequest( Future<bool> importSignalContactAndCreateRequest(
server.Response_UserData userdata, server.Response_UserData userdata,
) async { ) async {
if (await processSignalUserData(userdata)) { if (!await processSignalUserData(userdata)) {
return false;
}
// 1. Setup notifications keys with the other user // 1. Setup notifications keys with the other user
await setupNotificationWithUsers( await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(), forceContact: userdata.userId.toInt(),
); );
// 2. Then send user request // 2. Then send user request
await sendCipherText( await sendCipherText(
userdata.userId.toInt(), userdata.userId.toInt(),
@ -98,5 +102,6 @@ Future<void> importSignalContactAndCreateRequest(
), ),
), ),
); );
}
return true;
} }

View file

@ -0,0 +1,130 @@
import 'dart:async';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:workmanager/workmanager.dart';
// ignore: unreachable_from_main
Future<void> initializeBackgroundTaskManager() async {
await Workmanager().initialize(callbackDispatcher);
await Workmanager().registerPeriodicTask(
'fetch_data_from_server',
'eu.twonly.periodic_task',
frequency: const Duration(minutes: 20),
initialDelay: const Duration(minutes: 5),
constraints: Constraints(
networkType: NetworkType.connected,
),
);
}
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
switch (task) {
case 'eu.twonly.periodic_task':
if (await initBackgroundExecution()) {
await handlePeriodicTask();
}
case 'eu.twonly.processing_task':
if (await initBackgroundExecution()) {
await handleProcessingTask();
}
default:
Log.error('Unknown task was executed: $task');
}
return Future.value(true);
});
}
Future<bool> initBackgroundExecution() async {
SentryWidgetsFlutterBinding.ensureInitialized();
initLogger();
final user = await getUser();
if (user == null) return false;
gUser = user;
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
twonlyDB = TwonlyDB();
apiService = ApiService();
globalIsInBackgroundTask = true;
return true;
}
Future<bool> handlePeriodicTask() async {
final lastExecution =
await KeyValueStore.get(KeyValueKeys.lastPeriodicTaskExecution);
if (lastExecution == null || !lastExecution.containsKey('timestamp')) {
final lastExecutionTime = lastExecution?['timestamp'] as int?;
if (lastExecutionTime != null) {
final lastExecution =
DateTime.fromMillisecondsSinceEpoch(lastExecutionTime);
if (DateTime.now().difference(lastExecution).inMinutes < 2) {
Log.info(
'eu.twonly.periodic_task not executed as last execution was within the last two minutes.',
);
return true;
}
}
}
await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, {
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
Log.info('eu.twonly.periodic_task was called.');
final stopwatch = Stopwatch()..start();
if (!await apiService.connect()) {
Log.info('Could not connect to the api. Returning early.');
return false;
}
if (!apiService.isAuthenticated) {
Log.info('Api is not authenticated. Returning early.');
return false;
}
while (!globalGotMessageFromServer) {
if (stopwatch.elapsed.inSeconds >= 15) {
Log.info('No new message from the server after 15 seconds.');
break;
}
await Future.delayed(const Duration(milliseconds: 500));
}
if (globalGotMessageFromServer) {
Log.info('Received a server message from the server.');
}
await finishStartedPreprocessing();
await Future.delayed(const Duration(milliseconds: 2000));
await apiService.close(() {});
stopwatch.stop();
Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.');
return true;
}
Future<void> handleProcessingTask() async {
Log.info('eu.twonly.processing_task was called.');
final stopwatch = Stopwatch()..start();
await finishStartedPreprocessing();
Log.info('eu.twonly.processing_task finished after ${stopwatch.elapsed}.');
}

View file

@ -5,7 +5,7 @@ import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

@ -10,13 +10,12 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -42,15 +41,16 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Log.info('Starting new twonly Backup!'); Log.info('Starting new twonly Backup!');
final baseDir = (await getApplicationSupportDirectory()).path; final baseDir = globalApplicationSupportDirectory;
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/')); final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
await backupDir.create(recursive: true); await backupDir.create(recursive: true);
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite')); final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
final backupDatabaseFileCleaned = final backupDatabaseFileCleaned = File(
File(join(backupDir.path, 'twonly.backup.cleaned.sqlite')); join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
);
// copy database // copy database
final originalDatabase = File(join(baseDir, 'twonly.sqlite')); final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
@ -70,8 +70,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
await backupDB.deleteDataForTwonlySafe(); await backupDB.deleteDataForTwonlySafe();
await backupDB await backupDB.customStatement('VACUUM INTO ?', [
.customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]); backupDatabaseFileCleaned.path,
]);
await backupDB.printTableSizes(); await backupDB.printTableSizes();
@ -80,10 +81,11 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
// ignore: inference_failure_on_collection_literal // ignore: inference_failure_on_collection_literal
final secureStorageBackup = {}; final secureStorageBackup = {};
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] = secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read(
await storage.read(key: SecureStorageKeys.signalIdentity); key: SecureStorageKeys.signalIdentity,
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = );
await storage.read(key: SecureStorageKeys.signalSignedPreKey); secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage
.read(key: SecureStorageKeys.signalSignedPreKey);
final userBackup = await getUser(); final userBackup = await getUser();
if (userBackup == null) return; if (userBackup == null) return;
@ -117,13 +119,15 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (gUser.twonlySafeBackup!.lastBackupDone == null || if (gUser.twonlySafeBackup!.lastBackupDone == null ||
gUser.twonlySafeBackup!.lastBackupDone! gUser.twonlySafeBackup!.lastBackupDone!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 90)))) { clock.now().subtract(const Duration(days: 90)),
)) {
force = true; force = true;
} }
final lastHash = final lastHash = await storage.read(
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash); key: SecureStorageKeys.twonlySafeLastBackupHash,
);
if (lastHash != null && !force) { if (lastHash != null && !force) {
if (backupHash == lastHash) { if (backupHash == lastHash) {
@ -155,8 +159,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Log.info('Backup files created.'); Log.info('Backup files created.');
final encryptedBackupBytesFile = final encryptedBackupBytesFile = File(
File(join(backupDir.path, 'twonly_safe.backup')); join(backupDir.path, 'twonly_safe.backup'),
);
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);

View file

@ -8,23 +8,25 @@ import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future<void> recoverTwonlySafe( Future<void> recoverBackup(
String username, String username,
String password, String password,
BackupServer? server, BackupServer? server,
) async { ) async {
final (backupId, encryptionKey) = await getMasterKey(password, username); final (backupId, encryptionKey) = await getMasterKey(password, username);
final backupServerUrl = final backupServerUrl = await getTwonlySafeBackupUrlFromServer(
await getTwonlySafeBackupUrlFromServer(backupId, server); backupId,
server,
);
if (backupServerUrl == null) { if (backupServerUrl == null) {
Log.error('Could not create backup url'); Log.error('Could not create backup url');
@ -87,8 +89,9 @@ Future<void> handleBackupData(
plaintextBytes, plaintextBytes,
); );
final baseDir = (await getApplicationSupportDirectory()).path; final originalDatabase = File(
final originalDatabase = File(join(baseDir, 'twonly.sqlite')); join(globalApplicationSupportDirectory, 'twonly.sqlite'),
);
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase); await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();

View file

@ -10,7 +10,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future<void> syncFlameCounters({String? forceForGroup}) async { Future<void> syncFlameCounters({String? forceForGroup}) async {
final groups = await twonlyDB.groupsDao.getAllDirectChats(); final groups = await twonlyDB.groupsDao.getAllGroups();
if (groups.isEmpty) return; if (groups.isEmpty) return;
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
final bestFriend = final bestFriend =
@ -37,14 +37,8 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
// only sync when flame counter is higher three or when they are bestFriends // only sync when flame counter is higher three or when they are bestFriends
if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue; if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue;
final groupMembers = await sendCipherTextToGroup(
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); group.groupId,
if (groupMembers.length != 1) {
continue; // flame sync is only done for groups of two
}
await sendCipherText(
groupMembers.first.contactId,
EncryptedContent( EncryptedContent(
flameSync: EncryptedContent_FlameSync( flameSync: EncryptedContent_FlameSync(
flameCounter: Int64(flameCounter), flameCounter: Int64(flameCounter),

View file

@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:pro_video_editor/pro_video_editor.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/channels/video_compression.channel.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:video_compress/video_compress.dart';
Future<void> compressImage( Future<void> compressImage(
File sourceFile, File sourceFile,
@ -16,8 +17,6 @@ Future<void> compressImage(
) async { ) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
// // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp
try { try {
var compressedBytes = await FlutterImageCompress.compressWithFile( var compressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path, sourceFile.path,
@ -74,37 +73,53 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
try { try {
final task = VideoRenderData( final task = VideoRenderData(
video: EditorVideo.file(media.originalPath), video: EditorVideo.file(media.originalPath),
// qualityPreset: VideoQualityPreset.p720High,
imageBytes: media.overlayImagePath.readAsBytesSync(), imageBytes: media.overlayImagePath.readAsBytesSync(),
enableAudio: !media.removeAudio, enableAudio: !media.removeAudio,
); );
final result = await ProVideoEditor.instance.renderVideo(task); await ProVideoEditor.instance
media.ffmpegOutputPath.writeAsBytesSync(result); .renderVideoToFile(media.ffmpegOutputPath.path, task);
MediaInfo? mediaInfo; if (Platform.isIOS ||
media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
!kReleaseMode) {
String? compressedPath;
try { try {
mediaInfo = await VideoCompress.compressVideo( compressedPath = await VideoCompressionChannel.compressVideo(
media.ffmpegOutputPath.path, inputPath: media.ffmpegOutputPath.path,
quality: VideoQuality.Res640x480Quality, outputPath: media.tempPath.path,
includeAudio: true, onProgress: (progress) async {
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaFile.mediaId,
MediaFilesCompanion(
preProgressingProcess: Value((progress * 100).toInt()),
),
);
},
); );
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
} catch (e) { } catch (e) {
Log.error('during video compression: $e'); Log.error('during video compression: $e');
} }
if (mediaInfo == null) { if (compressedPath == null) {
Log.error('Could not compress video using original video.'); Log.error('Could not compress video using original video.');
// as a fall back use the non compressed version // as a fall back use the non compressed version
media.ffmpegOutputPath.copySync(media.tempPath.path); media.ffmpegOutputPath.copySync(media.tempPath.path);
}
} else { } else {
mediaInfo.file!.copySync(media.tempPath.path); // In case the video is smaller than 10MB do not compress it...
media.ffmpegOutputPath.copySync(media.tempPath.path);
} }
stopwatch.stop(); stopwatch.stop();
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
.toStringAsFixed(2);
final sizeTo =
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2);
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from ${media.ffmpegOutputPath.statSync().size} to ${media.tempPath.statSync().size} bytes.', 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.',
); );
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);

View file

@ -8,11 +8,12 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import '../../firebase_options.dart'; import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
@ -111,8 +112,15 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
initLogger(); initLogger();
// Log.info('Handling a background message: ${message.messageId}'); // Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message); await handleRemoteMessage(message);
if (Platform.isAndroid) {
if (await initBackgroundExecution()) {
await handlePeriodicTask();
}
} else {
// make sure every thing run... // make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000)); await Future.delayed(const Duration(milliseconds: 2000));
}
} }
Future<void> handleRemoteMessage(RemoteMessage message) async { Future<void> handleRemoteMessage(RemoteMessage message) async {

View file

@ -1,12 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class KeyValueStore { class KeyValueStore {
static Future<File> _getFilePath(String key) async { static Future<File> _getFilePath(String key) async {
final directory = await getApplicationSupportDirectory(); return File('$globalApplicationSupportDirectory/keyvalue/$key.json');
return File('${directory.path}/keyvalue/$key.json');
} }
static Future<void> delete(String key) async { static Future<void> delete(String key) async {

View file

@ -1,8 +1,9 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:mutex/mutex.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -10,7 +11,7 @@ void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) async { Logger.root.onRecord.listen((record) async {
await _writeLogToFile(record); unawaited(_writeLogToFile(record));
if (!kReleaseMode) { if (!kReleaseMode) {
// ignore: avoid_print // ignore: avoid_print
print( print(
@ -66,54 +67,98 @@ class Log {
} }
Future<String> loadLogFile() async { Future<String> loadLogFile() async {
final directory = await getApplicationSupportDirectory(); return _protectFileAccess(() async {
final logFile = File('${directory.path}/app.log'); final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) { if (logFile.existsSync()) {
return logFile.readAsString(); return logFile.readAsString();
} else { } else {
return 'Log file does not exist.'; return 'Log file does not exist.';
} }
});
} }
Future<String> readLast1000Lines() async { Future<String> readLast1000Lines() async {
final dir = await getApplicationSupportDirectory(); return _protectFileAccess(() async {
final file = File('${dir.path}/app.log'); final file = File('$globalApplicationSupportDirectory/app.log');
if (!file.existsSync()) return ''; if (!file.existsSync()) return '';
final all = await file.readAsLines(); final all = await file.readAsLines();
final start = all.length > 1000 ? all.length - 1000 : 0; final start = all.length > 1000 ? all.length - 1000 : 0;
return all.sublist(start).join('\n'); return all.sublist(start).join('\n');
});
}
final Mutex _logMutex = Mutex();
Future<T> _protectFileAccess<T>(Future<T> Function() action) async {
return _logMutex.protect(() async {
final lockFile = File('$globalApplicationSupportDirectory/app.log.lock');
var lockAcquired = false;
while (!lockAcquired) {
try {
lockFile.createSync(exclusive: true);
lockAcquired = true;
} on FileSystemException catch (e) {
final isExists = e is PathExistsException || e.osError?.errorCode == 17;
if (!isExists) {
break;
}
try {
final stat = lockFile.statSync();
if (stat.type != FileSystemEntityType.notFound) {
final age = DateTime.now().difference(stat.modified).inMilliseconds;
if (age > 1000) {
lockFile.deleteSync();
continue;
}
}
} catch (_) {}
await Future.delayed(const Duration(milliseconds: 50));
} catch (_) {
break;
}
}
try {
return await action();
} finally {
if (lockAcquired) {
try {
if (lockFile.existsSync()) {
lockFile.deleteSync();
}
} catch (_) {}
}
}
});
} }
Future<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final logFile = File('$globalApplicationSupportDirectory/app.log');
final logFile = File('${directory.path}/app.log');
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
final logMessage = final logMessage =
'${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n'; '${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
return _protectFileAccess(() async {
if (!logFile.existsSync()) {
logFile.createSync(recursive: true);
}
final raf = await logFile.open(mode: FileMode.writeOnlyAppend); final raf = await logFile.open(mode: FileMode.writeOnlyAppend);
try { try {
// Use FileLock.blockingExclusive to wait until the lock is available
await raf.lock(FileLock.blockingExclusive);
await raf.writeString(logMessage); await raf.writeString(logMessage);
await raf.flush(); await raf.flush();
} catch (e) { } catch (e) {
// ignore: avoid_print // ignore: avoid_print
print('Error during file access: $e'); print('Error during file access: $e');
} finally { } finally {
await raf.unlock();
await raf.close(); await raf.close();
} }
});
} }
Future<void> cleanLogFile() async { Future<void> cleanLogFile() async {
final directory = await getApplicationSupportDirectory(); return _protectFileAccess(() async {
final logFile = File('${directory.path}/app.log'); final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) { if (logFile.existsSync()) {
final lines = await logFile.readAsLines(); final lines = await logFile.readAsLines();
@ -126,17 +171,19 @@ Future<void> cleanLogFile() async {
final sink = logFile.openWrite()..writeAll(remaining, '\n'); final sink = logFile.openWrite()..writeAll(remaining, '\n');
await sink.close(); await sink.close();
} }
});
} }
Future<bool> deleteLogFile() async { Future<bool> deleteLogFile() async {
final directory = await getApplicationSupportDirectory(); return _protectFileAccess(() async {
final logFile = File('${directory.path}/app.log'); final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) { if (logFile.existsSync()) {
await logFile.delete(); await logFile.delete();
return true; return true;
} }
return false; return false;
});
} }
String _getCallerSourceCodeFilename() { String _getCallerSourceCodeFilename() {
@ -152,8 +199,11 @@ String _getCallerSourceCodeFilename() {
lineNumber = parts.last.split(':')[1]; // Extract the line number lineNumber = parts.last.split(':')[1]; // Extract the line number
} else { } else {
final firstLine = stackTraceString.split('\n')[0]; final firstLine = stackTraceString.split('\n')[0];
fileName = fileName = firstLine
firstLine.split('/').last.split(':').first; // Extract the file name .split('/')
.last
.split(':')
.first; // Extract the file name
lineNumber = firstLine.split(':')[1]; // Extract the line number lineNumber = firstLine.split(':')[1]; // Extract the line number
} }
lineNumber = lineNumber.replaceAll(')', ''); lineNumber = lineNumber.replaceAll(')', '');

View file

@ -302,7 +302,9 @@ Color getMessageColorFromType(
) { ) {
Color color; Color color;
if (message.type == MessageType.text.name) { if (message.type == MessageType.restoreFlameCounter.name) {
color = Colors.orange;
} else if (message.type == MessageType.text.name) {
color = Colors.blueAccent; color = Colors.blueAccent;
} else if (mediaFile != null) { } else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) { if (mediaFile.requiresAuthentication) {

View file

@ -55,7 +55,7 @@ PublicProfile? parseQrCodeData(Uint8List rawBytes) {
return null; return null;
} }
Future<void> addNewContactFromPublicProfile(PublicProfile profile) async { Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
final userdata = Response_UserData( final userdata = Response_UserData(
userId: profile.userId, userId: profile.userId,
publicIdentityKey: profile.publicIdentityKey, publicIdentityKey: profile.publicIdentityKey,
@ -77,5 +77,8 @@ Future<void> addNewContactFromPublicProfile(PublicProfile profile) async {
), ),
); );
if (added > 0) await importSignalContactAndCreateRequest(userdata); if (added > 0) {
return importSignalContactAndCreateRequest(userdata);
}
return false;
} }

View file

@ -21,12 +21,12 @@ Future<bool> isUserCreated() async {
} }
Future<UserData?> getUser() async { Future<UserData?> getUser() async {
final userJson = try {
await const FlutterSecureStorage().read(key: SecureStorageKeys.userData); final userJson = await const FlutterSecureStorage()
.read(key: SecureStorageKeys.userData);
if (userJson == null) { if (userJson == null) {
return null; return null;
} }
try {
final userMap = jsonDecode(userJson) as Map<String, dynamic>; final userMap = jsonDecode(userJson) as Map<String, dynamic>;
final user = UserData.fromJson(userMap); final user = UserData.fromJson(userMap);
return user; return user;

View file

@ -849,7 +849,21 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
onTap: () async { onTap: () async {
c.isLoading = true; c.isLoading = true;
widget.mainCameraController.setState(); widget.mainCameraController.setState();
await addNewContactFromPublicProfile(c.profile); if (await addNewContactFromPublicProfile(
c.profile,
) &&
context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang.requestedUserToastText(
c.profile.username,
),
),
duration: const Duration(seconds: 8),
),
);
}
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),

View file

@ -6,7 +6,6 @@ enum FaceFilterType {
none, none,
dogBrown, dogBrown,
beardUpperLipGreen, beardUpperLipGreen,
beardUpperLip,
} }
extension FaceFilterTypeExtension on FaceFilterType { extension FaceFilterTypeExtension on FaceFilterType {
@ -27,8 +26,6 @@ extension FaceFilterTypeExtension on FaceFilterType {
return Container(); return Container();
case FaceFilterType.dogBrown: case FaceFilterType.dogBrown:
return DogFilterPainter.getPreview(); return DogFilterPainter.getPreview();
case FaceFilterType.beardUpperLip:
return BeardFilterPainter.getPreview(this);
case FaceFilterType.beardUpperLipGreen: case FaceFilterType.beardUpperLipGreen:
return BeardFilterPainter.getPreview(this); return BeardFilterPainter.getPreview(this);
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -25,19 +26,13 @@ import 'package:twonly/src/views/camera/camera_preview_components/painters/face_
import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
class ScannedVerifiedContact { class ScannedVerifiedContact {
ScannedVerifiedContact({ ScannedVerifiedContact({required this.contact, required this.verificationOk});
required this.contact,
required this.verificationOk,
});
Contact contact; Contact contact;
bool verificationOk; bool verificationOk;
} }
class ScannedNewProfile { class ScannedNewProfile {
ScannedNewProfile({ ScannedNewProfile({required this.profile, this.isLoading = false});
required this.profile,
this.isLoading = false,
});
PublicProfile profile; PublicProfile profile;
bool isLoading; bool isLoading;
} }
@ -53,14 +48,15 @@ class MainCameraController {
String? scannedUrl; String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey(); GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey(); GlobalKey cameraPreviewKey = GlobalKey();
bool isSelectingFaceFilters = false;
bool isSelectingFaceFilters = false;
bool isSharePreviewIsShown = false; bool isSharePreviewIsShown = false;
bool isVideoRecording = false; bool isVideoRecording = false;
DateTime? timeSharedLinkWasSetWithQr;
Uri? sharedLinkForPreview; Uri? sharedLinkForPreview;
void setSharedLinkForPreview(Uri url) { void setSharedLinkForPreview(Uri? url) {
sharedLinkForPreview = url; sharedLinkForPreview = url;
setState(); setState();
} }
@ -92,9 +88,8 @@ class MainCameraController {
scannedUrl = null; scannedUrl = null;
try { try {
await cameraController?.stopImageStream(); await cameraController?.stopImageStream();
} catch (e) { // ignore: empty_catches
Log.warn(e); } catch (e) {}
}
final cameraControllerTemp = cameraController; final cameraControllerTemp = cameraController;
cameraController = null; cameraController = null;
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.) // prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
@ -159,8 +154,9 @@ class MainCameraController {
} }
} }
await cameraController await cameraController?.lockCaptureOrientation(
?.lockCaptureOrientation(DeviceOrientation.portraitUp); DeviceOrientation.portraitUp,
);
await cameraController?.setFlashMode( await cameraController?.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off, selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
); );
@ -169,7 +165,8 @@ class MainCameraController {
selectedCameraDetails.minAvailableZoom = selectedCameraDetails.minAvailableZoom =
await cameraController?.getMinZoomLevel() ?? 1; await cameraController?.getMinZoomLevel() ?? 1;
selectedCameraDetails selectedCameraDetails
..isZoomAble = selectedCameraDetails.maxAvailableZoom != ..isZoomAble =
selectedCameraDetails.maxAvailableZoom !=
selectedCameraDetails.minAvailableZoom selectedCameraDetails.minAvailableZoom
..cameraLoaded = true ..cameraLoaded = true
..cameraId = cameraId; ..cameraId = cameraId;
@ -325,11 +322,23 @@ class MainCameraController {
); );
customPaint = CustomPaint(painter: painter); customPaint = CustomPaint(painter: painter);
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
if (timeSharedLinkWasSetWithQr!.isAfter(
DateTime.now().subtract(const Duration(seconds: 2)),
)) {
setSharedLinkForPreview(null);
}
}
for (final barcode in barcodes) { for (final barcode in barcodes) {
if (barcode.displayValue != null) { if (barcode.displayValue != null) {
if (barcode.displayValue!.startsWith('http://') || if (barcode.displayValue!.startsWith('http://') ||
barcode.displayValue!.startsWith('https://')) { barcode.displayValue!.startsWith('https://')) {
scannedUrl = barcode.displayValue; scannedUrl = barcode.displayValue;
if (sharedLinkForPreview == null) {
timeSharedLinkWasSetWithQr = clock.now();
setSharedLinkForPreview(Uri.parse(scannedUrl!));
}
} }
} }
if (barcode.rawBytes == null) continue; if (barcode.rawBytes == null) continue;
@ -338,16 +347,19 @@ class MainCameraController {
if (profile == null) continue; if (profile == null) continue;
final contact = final contact = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(profile.userId.toInt()); profile.userId.toInt(),
);
if (contact != null && contact.accepted) { if (contact != null && contact.accepted) {
if (contactsVerified[contact.userId] == null) { if (contactsVerified[contact.userId] == null) {
final storedPublicKey = final storedPublicKey = await getPublicKeyFromContact(
await getPublicKeyFromContact(contact.userId); contact.userId,
);
if (storedPublicKey != null) { if (storedPublicKey != null) {
final verificationOk = final verificationOk = profile.publicIdentityKey.equals(
profile.publicIdentityKey.equals(storedPublicKey.toList()); storedPublicKey.toList(),
);
contactsVerified[contact.userId] = ScannedVerifiedContact( contactsVerified[contact.userId] = ScannedVerifiedContact(
contact: contact, contact: contact,
verificationOk: verificationOk, verificationOk: verificationOk,
@ -408,7 +420,6 @@ class MainCameraController {
inputImage.metadata!.rotation, inputImage.metadata!.rotation,
cameraController!.description.lensDirection, cameraController!.description.lensDirection,
); );
case FaceFilterType.beardUpperLip:
case FaceFilterType.beardUpperLipGreen: case FaceFilterType.beardUpperLipGreen:
painter = BeardFilterPainter( painter = BeardFilterPainter(
_currentFilterType, _currentFilterType,

View file

@ -27,8 +27,6 @@ class BeardFilterPainter extends FaceFilterPainter {
static String getAssetPath(FaceFilterType beardType) { static String getAssetPath(FaceFilterType beardType) {
switch (beardType) { switch (beardType) {
case FaceFilterType.beardUpperLip:
return 'assets/filters/beard_upper_lip.webp';
case FaceFilterType.beardUpperLipGreen: case FaceFilterType.beardUpperLipGreen:
return 'assets/filters/beard_upper_lip_green.webp'; return 'assets/filters/beard_upper_lip_green.webp';
case FaceFilterType.dogBrown: case FaceFilterType.dogBrown:

View file

@ -33,7 +33,7 @@ class FilterSkeleton extends StatelessWidget {
child: Stack( child: Stack(
children: [ children: [
Positioned.fill(child: Container()), Positioned.fill(child: Container()),
if (child != null) child!, ?child,
], ],
), ),
); );
@ -89,7 +89,8 @@ class _FilterLayerState extends State<FilterLayer> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final stickers = (await getStickerIndex()) final stickers =
(await getStickerIndex())
.where((x) => x.imageSrc.contains('/imagefilter/')) .where((x) => x.imageSrc.contains('/imagefilter/'))
.toList() .toList()
..sortBy((x) => x.imageSrc); ..sortBy((x) => x.imageSrc);

View file

@ -138,7 +138,7 @@ class _ChatListViewState extends State<ChatListView> {
actions: [ actions: [
const FeedbackIconButton(), const FeedbackIconButton(),
StreamBuilder( StreamBuilder(
stream: twonlyDB.contactsDao.watchContactsRequested(), stream: twonlyDB.contactsDao.watchContactsRequestedCount(),
builder: (context, snapshot) { builder: (context, snapshot) {
var count = 0; var count = 0;
if (snapshot.hasData && snapshot.data != null) { if (snapshot.hasData && snapshot.data != null) {

View file

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class DemoUserCard extends StatelessWidget {
const DemoUserCard({super.key});
@override
Widget build(BuildContext context) {
return ColoredBox(
color: isDarkMode(context) ? Colors.white : Colors.black,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
'This is a Demo-Preview.',
textAlign: TextAlign.center,
style: TextStyle(
color: !isDarkMode(context) ? Colors.white : Colors.black,
fontSize: 18,
),
),
FilledButton(
onPressed: () async {
await deleteLocalUserData();
await Restart.restartApp(
notificationTitle: 'Demo-Mode exited.',
notificationBody: 'Click here to open the app again',
);
},
child: const Text('Register'),
),
],
),
);
}
}

View file

@ -26,15 +26,17 @@ class _ShareAdditionalViewState extends State<ShareAdditionalView> {
} }
Future<void> openShareContactView() async { Future<void> openShareContactView() async {
final selectedContacts = await context.navPush( final selectedContacts =
await context.navPush(
SelectContactsView( SelectContactsView(
text: SelectedContactViewText( text: SelectedContactViewText(
title: context.lang.shareContactsTitle, title: context.lang.shareContactsTitle,
submitButton: (_, __) => context.lang.shareContactsSubmit, submitButton: (_, _) => context.lang.shareContactsSubmit,
submitIcon: FontAwesomeIcons.shareNodes, submitIcon: FontAwesomeIcons.shareNodes,
), ),
), ),
) as List<int>?; )
as List<int>?;
if (selectedContacts != null && selectedContacts.isNotEmpty) { if (selectedContacts != null && selectedContacts.isNotEmpty) {
await insertAndSendContactShareMessage( await insertAndSendContactShareMessage(
widget.group.groupId, widget.group.groupId,

View file

@ -10,9 +10,11 @@ import 'package:twonly/src/database/tables/messages.table.dart'
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart';
@ -78,6 +80,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
if (mediaFiles != null) { if (mediaFiles != null) {
mediaService = MediaFileService(mediaFiles); mediaService = MediaFileService(mediaFiles);
if (mounted) setState(() {}); if (mounted) setState(() {});
} else {
Log.error(
'Media file not found for ${widget.message.messageId} => ${widget.message.mediaId}',
);
} }
}); });
} }
@ -132,6 +138,12 @@ class _ChatListEntryState extends State<ChatListEntry> {
); );
} }
if (widget.message.type == MessageType.restoreFlameCounter.name) {
return ChatFlameRestoredEntry(
message: widget.message,
);
}
return const ChatUnknownEntry(); return const ChatUnknownEntry();
} }

View file

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/better_text.dart';
class ChatFlameRestoredEntry extends StatelessWidget {
const ChatFlameRestoredEntry({
required this.message,
super.key,
});
final Message message;
@override
Widget build(BuildContext context) {
AdditionalMessageData? data;
if (message.additionalMessageData != null) {
try {
data = AdditionalMessageData.fromBuffer(
message.additionalMessageData!,
);
} catch (e) {
data = null;
}
}
if (data == null || !data.hasRestoredFlameCounter()) {
return const SizedBox.shrink();
}
return Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(12),
),
child: BetterText(
text: context.lang
.chatEntryFlameRestored(data.restoredFlameCounter.toInt()),
textColor: isDarkMode(context) ? Colors.black : Colors.black,
),
);
}
}

View file

@ -101,8 +101,9 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
final mediaFile = message.mediaId == null final mediaFile = message.mediaId == null
? null ? null
: widget.mediaFiles : widget.mediaFiles.firstWhereOrNull(
.firstWhereOrNull((t) => t.mediaId == message.mediaId); (t) => t.mediaId == message.mediaId,
);
final color = getMessageColorFromType(message, mediaFile, context); final color = getMessageColorFromType(message, mediaFile, context);
@ -144,8 +145,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
} }
case MessageSendState.send: case MessageSendState.send:
icon = icon = FaIcon(
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color); FontAwesomeIcons.solidPaperPlane,
size: 12,
color: color,
);
text = context.lang.messageSendState_Send; text = context.lang.messageSendState_Send;
case MessageSendState.sending: case MessageSendState.sending:
icon = getLoaderIcon(color); icon = getLoaderIcon(color);
@ -163,13 +167,20 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
context.lang.uploadLimitReached, context.lang.uploadLimitReached,
style: const TextStyle(fontSize: 9), style: const TextStyle(fontSize: 9),
); );
onTap = () => context.push(Routes.settingsSubscription); onTap = () => context.push(Routes.settingsSubscription);
} }
if (mediaFile.uploadState == UploadState.preprocessing ||
mediaFile.uploadState == UploadState.initialized) { if (mediaFile.uploadState == UploadState.initialized) {
text = context.lang.inProcess; text = context.lang.inProcess;
} }
if (mediaFile.uploadState == UploadState.preprocessing) {
final progress = mediaFile.preProgressingProcess ?? 0;
if (progress > 0) {
text = '${context.lang.inProcess} ($progress%)';
} else {
text = context.lang.inProcess;
}
}
} }
hasLoader = true; hasLoader = true;
@ -191,13 +202,28 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
if (mediaFile.downloadState == DownloadState.reuploadRequested) { if (mediaFile.downloadState == DownloadState.reuploadRequested) {
icon = icon = FaIcon(
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); FontAwesomeIcons.clockRotateLeft,
size: 12,
color: color,
);
textWidget = Text( textWidget = Text(
context.lang.retransmissionRequested, context.lang.retransmissionRequested,
style: const TextStyle(fontSize: 9), style: const TextStyle(fontSize: 9),
); );
} }
if (mediaFile.uploadState == UploadState.fileLimitReached) {
icon = FaIcon(
FontAwesomeIcons.triangleExclamation,
size: 12,
color: color,
);
textWidget = Text(
context.lang.fileLimitReached,
style: const TextStyle(fontSize: 9),
);
}
} }
if (message.isDeletedFromSender) { if (message.isDeletedFromSender) {
@ -220,10 +246,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
if (!widget.messages.any((t) => t.openedAt == null)) { if (!widget.messages.any((t) => t.openedAt == null)) {
if (widget.lastReaction != null) { if (widget.lastReaction != null) {
/// No messages are still open, so check if the reaction is the last message received. /// No messages are still open, so check if the reaction is the last message received.
if (!widget.messages if (!widget.messages.any(
.any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) { (m) => m.createdAt.isAfter(widget.lastReaction!.createdAt),
if (EmojiAnimation.animatedIcons )) {
.containsKey(widget.lastReaction!.emoji)) { if (EmojiAnimation.animatedIcons.containsKey(
widget.lastReaction!.emoji,
)) {
icons = [ icons = [
SizedBox( SizedBox(
height: 18, height: 18,

View file

@ -113,6 +113,7 @@ class EmojiAnimation extends StatelessWidget {
'💯': '100.lottie', '💯': '100.lottie',
'🎉': 'party-popper.lottie', '🎉': 'party-popper.lottie',
'🎊': 'confetti-ball.lottie', '🎊': 'confetti-ball.lottie',
'🎂': 'birthday-cake.json',
'🧡': 'orange-heart.lottie', '🧡': 'orange-heart.lottie',
'💛': 'yellow-heart.lottie', '💛': 'yellow-heart.lottie',
'💚': 'green-heart.lottie', '💚': 'green-heart.lottie',

View file

@ -52,8 +52,7 @@ class _AvatarIconState extends State<AvatarIcon> {
super.dispose(); super.dispose();
} }
// ignore: strict_top_level_inference Widget errorBuilder(_, _, _) {
Widget errorBuilder(_, __, ___) {
return const SvgPicture( return const SvgPicture(
AssetBytesLoader('assets/images/default_avatar.svg.vec'), AssetBytesLoader('assets/images/default_avatar.svg.vec'),
); );

View file

@ -44,11 +44,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!); group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
groupId = group?.groupId; groupId = group?.groupId;
} else if (groupId != null) { } else if (groupId != null) {
// do not display the flame counter for groups
group = await twonlyDB.groupsDao.getGroup(groupId); group = await twonlyDB.groupsDao.getGroup(groupId);
if (!(group?.isDirectChat ?? false)) {
return;
}
} }
if (groupId != null && group != null) { if (groupId != null && group != null) {
isBestFriend = isBestFriend =
@ -67,11 +63,22 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (flameCounter < 1) return Container(); if (flameCounter < 1) return Container();
var flameEmoji = '🔥';
if (isBestFriend) flameEmoji = '❤️‍🔥';
if (flameCounter == 100) flameEmoji = '💯';
if (flameCounter >= 365 && flameCounter % 365 == 0) {
flameEmoji = '🎂';
}
return Row( return Row(
children: [ children: [
if (widget.prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
if (widget.prefix) const Text(''), if (widget.prefix) const Text(''),
if (widget.prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
if (flameCounter != 100)
Text( Text(
flameCounter.toString(), flameCounter.toString(),
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
@ -79,7 +86,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( child: EmojiAnimation(
emoji: isBestFriend ? '❤️‍🔥' : '🔥', emoji: flameEmoji,
), ),
), ),
], ],

View file

@ -74,7 +74,7 @@ class _ThreeRotatingDotsState extends State<ThreeRotatingDots>
height: size, height: size,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _animationController, animation: _animationController,
builder: (_, __) => Transform.translate( builder: (_, _) => Transform.translate(
offset: Offset(0, size / 12), offset: Offset(0, size / 12),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
@ -110,7 +110,6 @@ class _ThreeRotatingDotsState extends State<ThreeRotatingDots>
), ),
/// Next 3 dots /// Next 3 dots
_BuildDot.second( _BuildDot.second(
controller: _animationController, controller: _animationController,
beginAngle: 0, beginAngle: 0,

View file

@ -1,11 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -46,7 +53,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
} }
Future<void> _restoreFlames() async { Future<void> _restoreFlames() async {
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) { if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames) &&
kReleaseMode) {
await context.push(Routes.settingsSubscription); await context.push(Routes.settingsSubscription);
return; return;
} }
@ -60,7 +68,40 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
lastFlameCounterChange: Value(clock.now()), lastFlameCounterChange: Value(clock.now()),
), ),
); );
final addData = AdditionalMessageData(
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
restoredFlameCounter: Int64(_group!.maxFlameCounter),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(_groupId),
type: Value(MessageType.restoreFlameCounter.name),
additionalMessageData: Value(addData.writeToBuffer()),
),
);
if (message == null) {
Log.error('Could not insert message into database');
return;
}
final encryptedContent = pb.EncryptedContent(
additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage(
senderMessageId: message.messageId,
additionalMessageData: addData.writeToBuffer(),
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
type: MessageType.restoreFlameCounter.name,
),
);
await syncFlameCounters(forceForGroup: _groupId); await syncFlameCounters(forceForGroup: _groupId);
await sendCipherTextToGroup(
_groupId,
encryptedContent,
messageId: message.messageId,
);
} }
@override @override

View file

@ -29,7 +29,7 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> { class _ContactViewState extends State<ContactView> {
Contact? _contact; Contact? _contact;
bool _contactIsStillAGroupMember = true; List<GroupMember> _memberOfGroups = [];
late StreamSubscription<Contact?> _contactSub; late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub; late StreamSubscription<List<GroupMember>> _groupMemberSub;
@ -44,10 +44,8 @@ class _ContactViewState extends State<ContactView> {
}); });
_groupMemberSub = twonlyDB.groupsDao _groupMemberSub = twonlyDB.groupsDao
.watchContactGroupMember(widget.userId) .watchContactGroupMember(widget.userId)
.listen((update) { .listen((groups) async {
setState(() { _memberOfGroups = groups;
_contactIsStillAGroupMember = update.isNotEmpty;
});
}); });
super.initState(); super.initState();
} }
@ -60,7 +58,18 @@ class _ContactViewState extends State<ContactView> {
} }
Future<void> handleUserRemoveRequest(Contact contact) async { Future<void> handleUserRemoveRequest(Contact contact) async {
if (_contactIsStillAGroupMember) { var delete = true;
for (final groupM in _memberOfGroups) {
final group = await twonlyDB.groupsDao.getGroup(groupM.groupId);
if (group?.deletedContent ?? false) {
await twonlyDB.groupsDao.deleteGroup(group!.groupId);
} else {
delete = false;
}
}
if (!mounted) return;
if (!delete) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(context.lang.deleteUserErrorMessage), content: Text(context.lang.deleteUserErrorMessage),
@ -211,26 +220,6 @@ class _ContactViewState extends State<ContactView> {
setState(() {}); setState(() {});
}, },
), ),
// BetterListTile(
// icon: FontAwesomeIcons.eraser,
// iconSize: 16,
// text: context.lang.deleteAllContactMessages,
// onTap: () async {
// final block = await showAlertDialog(
// context,
// context.lang.deleteAllContactMessages,
// context.lang.deleteAllContactMessagesBody(
// getContactDisplayName(contact),
// ),
// );
// if (block) {
// if (context.mounted) {
// await twonlyDB.messagesDao
// .deleteMessagesByContactId(contact.userId);
// }
// }
// },
// ),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.flag, icon: FontAwesomeIcons.flag,
text: context.lang.reportUser, text: context.lang.reportUser,

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
@ -181,6 +182,10 @@ class _GroupViewState extends State<GroupView> {
substringBy(_group!.groupName, 25), substringBy(_group!.groupName, 25),
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
), ),
FlameCounterWidget(
groupId: _group?.groupId,
prefix: true,
),
], ],
), ),
const SizedBox(height: 50), const SizedBox(height: 50),

View file

@ -93,7 +93,9 @@ class GroupMemberContextMenu extends StatelessWidget {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
member.contactId, member.contactId,
const ContactsCompanion( const ContactsCompanion(
requested: Value(true), accepted: Value(false),
requested: Value(false),
deletedByUser: Value(false),
), ),
); );
await sendCipherText( await sendCipherText(

View file

@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:restart_app/restart_app.dart'; import 'package:restart_app/restart_app.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/twonly_safe/restore.twonly_safe.dart'; import 'package:twonly/src/services/backup/restore.backup.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
@ -29,7 +29,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
}); });
try { try {
await recoverTwonlySafe( await recoverBackup(
usernameCtrl.text, usernameCtrl.text,
passwordCtrl.text, passwordCtrl.text,
backupServer, backupServer,
@ -38,6 +38,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
await Restart.restartApp( await Restart.restartApp(
notificationTitle: 'Backup successfully recovered.', notificationTitle: 'Backup successfully recovered.',
notificationBody: 'Click here to open the app again', notificationBody: 'Click here to open the app again',
forceKill: true,
); );
} catch (e) { } catch (e) {
// in case something was already written from the backup... // in case something was already written from the backup...

View file

@ -140,6 +140,7 @@ class _AccountViewState extends State<AccountView> {
await Restart.restartApp( await Restart.restartApp(
notificationTitle: 'Account successfully deleted', notificationTitle: 'Account successfully deleted',
notificationBody: 'Click here to open the app again', notificationBody: 'Click here to open the app again',
forceKill: true,
); );
} }
}, },

View file

@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
void Function() gUpdateBackupView = () {}; void Function() gUpdateBackupView = () {};

View file

@ -4,7 +4,7 @@ import 'package:flutter/services.dart' show rootBundle;
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';

Some files were not shown because too many files have changed in this diff Show more