mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-20 08:12:52 +00:00
Merge pull request #393 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
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:
commit
5e759df55b
137 changed files with 56909 additions and 78995 deletions
13
.github/workflows/dev_github.yml
vendored
13
.github/workflows/dev_github.yml
vendored
|
|
@ -25,8 +25,11 @@ jobs:
|
|||
- name: Cloning sub-repos
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Check flutter code
|
||||
run: |
|
||||
flutter pub get
|
||||
flutter analyze
|
||||
flutter test
|
||||
- name: flutter pub get
|
||||
run: flutter pub get
|
||||
|
||||
- name: flutter analyze
|
||||
run: flutter analyze
|
||||
|
||||
- name: flutter test
|
||||
run: flutter test
|
||||
|
|
|
|||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,12 +1,27 @@
|
|||
# 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
|
||||
|
||||
Feature: Show link in chat if the saved media file contains one
|
||||
Improve: Verification badge for groups
|
||||
Improve: Huge reduction in app size
|
||||
Fix: Crash on older devices when compressing a video
|
||||
Fix: Problem with decrypting messages fixed
|
||||
- Feature: Show link in chat if the saved media file contains one
|
||||
- Improve: Verification badge for groups
|
||||
- Improve: Huge reduction in app size
|
||||
- Fix: Crash on older devices when compressing a video
|
||||
- Fix: Problem with decrypting messages fixed
|
||||
|
||||
## 0.0.93
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ android {
|
|||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
|
@ -72,4 +72,5 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
implementation 'com.otaliastudios:transcoder:0.11.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:label="twonly"
|
||||
android:name="${applicationName}"
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
|
|
|||
|
|
@ -1,27 +1,14 @@
|
|||
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 android.view.KeyEvent
|
||||
import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownPlugin.eventSink
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||
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() {
|
||||
|
||||
private val MEDIA_STORE_CHANNEL = "eu.twonly/mediaStore"
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
||||
eventSink!!.success(true)
|
||||
|
|
@ -37,80 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MEDIA_STORE_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 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
|
||||
MediaStoreChannel.configure(flutterEngine, applicationContext)
|
||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal file
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
android/app/src/main/kotlin/eu/twonly/MyApplication.kt
Normal file
13
android/app/src/main/kotlin/eu/twonly/MyApplication.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package eu.twonly
|
||||
|
||||
class MyMediaStorageProxy {
|
||||
|
||||
|
||||
}
|
||||
107
android/app/src/main/kotlin/eu/twonly/VideoCompressionChannel.kt
Normal file
107
android/app/src/main/kotlin/eu/twonly/VideoCompressionChannel.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
assets/animated_icons/birthday-cake.json
Normal file
1
assets/animated_icons/birthday-cake.json
Normal file
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
|
||||
|
|
@ -141,20 +141,20 @@ struct PushNotification: Sendable {
|
|||
var kind: PushKind = .reaction
|
||||
|
||||
var messageID: String {
|
||||
get {return _messageID ?? String()}
|
||||
get {_messageID ?? String()}
|
||||
set {_messageID = newValue}
|
||||
}
|
||||
/// 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.
|
||||
mutating func clearMessageID() {self._messageID = nil}
|
||||
|
||||
var additionalContent: String {
|
||||
get {return _additionalContent ?? String()}
|
||||
get {_additionalContent ?? String()}
|
||||
set {_additionalContent = newValue}
|
||||
}
|
||||
/// 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.
|
||||
mutating func clearAdditionalContent() {self._additionalContent = nil}
|
||||
|
||||
|
|
@ -190,11 +190,11 @@ struct PushUser: Sendable {
|
|||
var blocked: Bool = false
|
||||
|
||||
var lastMessageID: String {
|
||||
get {return _lastMessageID ?? String()}
|
||||
get {_lastMessageID ?? String()}
|
||||
set {_lastMessageID = newValue}
|
||||
}
|
||||
/// 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.
|
||||
mutating func clearLastMessageID() {self._lastMessageID = nil}
|
||||
|
||||
|
|
|
|||
110
ios/Podfile.lock
110
ios/Podfile.lock
|
|
@ -49,55 +49,55 @@ PODS:
|
|||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase (12.8.0):
|
||||
- Firebase/Core (= 12.8.0)
|
||||
- Firebase/Core (12.8.0):
|
||||
- Firebase (12.9.0):
|
||||
- Firebase/Core (= 12.9.0)
|
||||
- Firebase/Core (12.9.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 12.8.0)
|
||||
- Firebase/CoreOnly (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- Firebase/Messaging (12.8.0):
|
||||
- FirebaseAnalytics (~> 12.9.0)
|
||||
- Firebase/CoreOnly (12.9.0):
|
||||
- FirebaseCore (~> 12.9.0)
|
||||
- Firebase/Messaging (12.9.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.8.0)
|
||||
- firebase_core (4.4.0):
|
||||
- Firebase/CoreOnly (= 12.8.0)
|
||||
- FirebaseMessaging (~> 12.9.0)
|
||||
- firebase_core (4.5.0):
|
||||
- Firebase/CoreOnly (= 12.9.0)
|
||||
- Flutter
|
||||
- firebase_messaging (16.1.1):
|
||||
- Firebase/Messaging (= 12.8.0)
|
||||
- firebase_messaging (16.1.2):
|
||||
- Firebase/Messaging (= 12.9.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (12.8.0):
|
||||
- FirebaseAnalytics/Default (= 12.8.0)
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
- FirebaseAnalytics (12.9.0):
|
||||
- FirebaseAnalytics/Default (= 12.9.0)
|
||||
- FirebaseCore (~> 12.9.0)
|
||||
- FirebaseInstallations (~> 12.9.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/Default (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
- GoogleAppMeasurement/Default (= 12.8.0)
|
||||
- FirebaseAnalytics/Default (12.9.0):
|
||||
- FirebaseCore (~> 12.9.0)
|
||||
- FirebaseInstallations (~> 12.9.0)
|
||||
- GoogleAppMeasurement/Default (= 12.9.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (12.8.0):
|
||||
- FirebaseCoreInternal (~> 12.8.0)
|
||||
- FirebaseCore (12.9.0):
|
||||
- FirebaseCoreInternal (~> 12.9.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.8.0):
|
||||
- FirebaseCoreInternal (12.9.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (12.9.0):
|
||||
- FirebaseCore (~> 12.9.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.8.0):
|
||||
- FirebaseCore (~> 12.8.0)
|
||||
- FirebaseInstallations (~> 12.8.0)
|
||||
- FirebaseMessaging (12.9.0):
|
||||
- FirebaseCore (~> 12.9.0)
|
||||
- FirebaseInstallations (~> 12.9.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
|
|
@ -140,23 +140,23 @@ PODS:
|
|||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Core (12.8.0):
|
||||
- GoogleAppMeasurement/Core (12.9.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Default (12.8.0):
|
||||
- GoogleAppMeasurement/Default (12.9.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.2.0)
|
||||
- GoogleAppMeasurement/Core (= 12.8.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.8.0)
|
||||
- GoogleAppMeasurement/Core (= 12.9.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.9.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.8.0):
|
||||
- GoogleAppMeasurement/Core (= 12.8.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.9.0):
|
||||
- GoogleAppMeasurement/Core (= 12.9.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
|
|
@ -267,7 +267,7 @@ PODS:
|
|||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- no_screenshot (0.3.2-beta.3):
|
||||
- no_screenshot (0.10.0):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
|
|
@ -276,7 +276,7 @@ PODS:
|
|||
- pro_video_editor (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- restart_app (0.0.1):
|
||||
- restart_app (1.7.3):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.6):
|
||||
- SDWebImage/Core (= 5.21.6)
|
||||
|
|
@ -285,7 +285,7 @@ PODS:
|
|||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- Sentry/HybridSDK (8.56.2)
|
||||
- sentry_flutter (9.13.0):
|
||||
- sentry_flutter (9.14.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.56.2)
|
||||
|
|
@ -326,11 +326,11 @@ PODS:
|
|||
- SwiftyGif (5.4.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_compress (0.3.0):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- workmanager_apple (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
|
|
@ -375,8 +375,8 @@ DEPENDENCIES:
|
|||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- SwiftProtobuf
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_compress (from `.symlinks/plugins/video_compress/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
|
|
@ -484,10 +484,10 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_compress:
|
||||
:path: ".symlinks/plugins/video_compress/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
workmanager_apple:
|
||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
||||
|
|
@ -501,14 +501,14 @@ SPEC CHECKSUMS:
|
|||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
|
||||
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
|
||||
firebase_messaging: 343de01a8d3e18b60df0c6d37f7174c44ae38e02
|
||||
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc
|
||||
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
|
||||
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
|
||||
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
|
||||
FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c
|
||||
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
|
||||
firebase_core: afac1aac13c931e0401c7e74ed1276112030efab
|
||||
firebase_messaging: 7cb2727feb789751fc6936bcc8e08408970e2820
|
||||
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
|
||||
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
|
||||
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
|
||||
FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
|
||||
FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
|
||||
|
|
@ -521,7 +521,7 @@ SPEC CHECKSUMS:
|
|||
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
|
||||
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
|
||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
|
||||
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
|
|
@ -538,16 +538,16 @@ SPEC CHECKSUMS:
|
|||
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
|
||||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
no_screenshot: 5e345998c43ffcad5d6834f249590483fcc037bd
|
||||
no_screenshot: 03c8ac6586f9652cd45e3d12d74e5992256403ac
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
|
||||
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
|
||||
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
|
||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
|
||||
sentry_flutter: dbed9a62ae39716b685a80140705c330d200d941
|
||||
sentry_flutter: 841fa2fe08dc72eb95e2320b76e3f751f3400cf5
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
|
@ -556,8 +556,8 @@ SPEC CHECKSUMS:
|
|||
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
/* 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; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
|
@ -235,6 +237,7 @@
|
|||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */,
|
||||
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
|
||||
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
|
||||
D2265DD42D920142000D99BB /* Runner.entitlements */,
|
||||
|
|
@ -624,6 +627,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */,
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import UIKit
|
||||
import UserNotifications
|
||||
import flutter_sharing_intent
|
||||
import workmanager_apple
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
|
|
@ -12,6 +13,22 @@ import flutter_sharing_intent
|
|||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,27 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<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>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
|
@ -43,6 +24,17 @@
|
|||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<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>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
|
||||
|
|
@ -51,10 +43,10 @@
|
|||
<false/>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
|
|
@ -63,12 +55,39 @@
|
|||
<string>Use your microphone to enable audio when making videos.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<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>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</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>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
|
@ -89,19 +108,5 @@
|
|||
</array>
|
||||
<key>firebase_performance_collection_deactivated</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
|
|
|||
253
ios/Runner/VideoCompressionChannel.swift
Normal file
253
ios/Runner/VideoCompressionChannel.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -188,7 +187,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
if (_showDatabaseMigration) {
|
||||
child = const Center(child: Text('Please reinstall twonly.'));
|
||||
} else if (_isUserCreated) {
|
||||
if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) {
|
||||
if (gUser.twonlySafeBackup == null && !_skipBackup) {
|
||||
child = SetupBackupView(
|
||||
callBack: () {
|
||||
_skipBackup = true;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
|||
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
|
||||
|
||||
bool globalIsAppInBackground = true;
|
||||
bool globalIsInBackgroundTask = false;
|
||||
bool globalAllowErrorTrackingViaSentry = false;
|
||||
bool globalGotMessageFromServer = false;
|
||||
|
||||
late String globalApplicationCacheDirectory;
|
||||
late String globalApplicationSupportDirectory;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.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/media_background.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/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/notifications/fcm.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/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -27,6 +29,10 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
void main() async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
||||
await initFCMService();
|
||||
|
||||
final user = await getUser();
|
||||
|
|
@ -45,12 +51,12 @@ void main() async {
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
final settingsController = SettingsChangeProvider();
|
||||
|
|
|
|||
45
lib/src/channels/video_compression.channel.dart
Normal file
45
lib/src/channels/video_compression.channel.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
lib/src/constants/keyvalue.keys.dart
Normal file
4
lib/src/constants/keyvalue.keys.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class KeyValueKeys {
|
||||
static const String lastPeriodicTaskExecution =
|
||||
'last_periodic_task_execution';
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
return query.map((row) => row.read(count)).watchSingle();
|
||||
}
|
||||
|
||||
Stream<int?> watchContactsRequested() {
|
||||
Stream<int?> watchContactsRequestedCount() {
|
||||
final count = contacts.requested.count(distinct: true);
|
||||
final query = selectOnly(contacts)
|
||||
..where(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
GroupsDao(super.db);
|
||||
|
||||
Future<bool> isContactInGroup(int contactId, String groupId) async {
|
||||
final entry = await (select(groupMembers)..where(
|
||||
// ignore: require_trailing_commas
|
||||
(t) => t.contactId.equals(contactId) & t.groupId.equals(groupId)))
|
||||
.getSingleOrNull();
|
||||
final entry =
|
||||
await (select(groupMembers)..where(
|
||||
(t) => t.contactId.equals(contactId) & t.groupId.equals(groupId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
return entry != null;
|
||||
}
|
||||
|
||||
|
|
@ -38,30 +39,31 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
String groupId,
|
||||
GroupsCompanion updates,
|
||||
) async {
|
||||
await (update(groups)..where((c) => c.groupId.equals(groupId)))
|
||||
.write(updates);
|
||||
await (update(
|
||||
groups,
|
||||
)..where((c) => c.groupId.equals(groupId))).write(updates);
|
||||
}
|
||||
|
||||
Future<List<GroupMember>> getGroupNonLeftMembers(String groupId) async {
|
||||
return (select(groupMembers)
|
||||
..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.memberState.equals(MemberState.leftGroup.name).not() |
|
||||
t.memberState.isNull()),
|
||||
))
|
||||
return (select(groupMembers)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.memberState.equals(MemberState.leftGroup.name).not() |
|
||||
t.memberState.isNull()),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<GroupMember>> getAllGroupMembers(String groupId) async {
|
||||
return (select(groupMembers)..where((t) => t.groupId.equals(groupId)))
|
||||
.get();
|
||||
return (select(
|
||||
groupMembers,
|
||||
)..where((t) => t.groupId.equals(groupId))).get();
|
||||
}
|
||||
|
||||
Future<GroupMember?> getGroupMemberByPublicKey(Uint8List publicKey) async {
|
||||
return (select(groupMembers)
|
||||
..where((t) => t.groupPublicKey.equals(publicKey)))
|
||||
.getSingleOrNull();
|
||||
return (select(
|
||||
groupMembers,
|
||||
)..where((t) => t.groupPublicKey.equals(publicKey))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Group?> createNewGroup(GroupsCompanion group) async {
|
||||
|
|
@ -94,18 +96,16 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
int contactId,
|
||||
GroupMembersCompanion updates,
|
||||
) async {
|
||||
await (update(groupMembers)
|
||||
..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
|
||||
))
|
||||
await (update(groupMembers)..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> removeMember(String groupId, int contactId) async {
|
||||
await (delete(groupMembers)
|
||||
..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
|
||||
))
|
||||
await (delete(groupMembers)..where(
|
||||
(c) => c.groupId.equals(groupId) & c.contactId.equals(contactId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +138,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
Future<Group?> _insertGroup(GroupsCompanion group) async {
|
||||
try {
|
||||
await into(groups).insert(group);
|
||||
return await (select(groups)
|
||||
..where((t) => t.groupId.equals(group.groupId.value)))
|
||||
.getSingle();
|
||||
return await (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
|
||||
} catch (e) {
|
||||
Log.error('Could not insert group: $e');
|
||||
return null;
|
||||
|
|
@ -148,69 +148,71 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
}
|
||||
|
||||
Future<List<Contact>> getGroupContact(String groupId) async {
|
||||
final query = (select(contacts).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.contactId.equalsExp(contacts.userId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
final query =
|
||||
(select(contacts).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.contactId.equalsExp(contacts.userId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
return query.map((row) => row.readTable(contacts)).get();
|
||||
}
|
||||
|
||||
Stream<List<Contact>> watchGroupContact(String groupId) {
|
||||
final query = (select(contacts).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.contactId.equalsExp(contacts.userId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
final query =
|
||||
(select(contacts).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.contactId.equalsExp(contacts.userId),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
return query.map((row) => row.readTable(contacts)).watch();
|
||||
}
|
||||
|
||||
Stream<List<(Contact, GroupMember)>> watchGroupMembers(String groupId) {
|
||||
final query =
|
||||
(select(groupMembers)..where((t) => t.groupId.equals(groupId))).join([
|
||||
leftOuterJoin(
|
||||
contacts,
|
||||
contacts.userId.equalsExp(groupMembers.contactId),
|
||||
),
|
||||
]);
|
||||
leftOuterJoin(
|
||||
contacts,
|
||||
contacts.userId.equalsExp(groupMembers.contactId),
|
||||
),
|
||||
]);
|
||||
return query
|
||||
.map((row) => (row.readTable(contacts), row.readTable(groupMembers)))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchGroupsForShareImage() {
|
||||
return (select(groups)
|
||||
..where(
|
||||
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
|
||||
))
|
||||
return (select(groups)..where(
|
||||
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<GroupMember>> watchContactGroupMember(int contactId) {
|
||||
return (select(groupMembers)
|
||||
..where(
|
||||
(g) => g.contactId.equals(contactId),
|
||||
))
|
||||
return (select(groupMembers)..where(
|
||||
(g) => g.contactId.equals(contactId),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<Group?> watchGroup(String groupId) {
|
||||
return (select(groups)..where((t) => t.groupId.equals(groupId)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<Group?> watchDirectChat(int contactId) {
|
||||
final groupId = getUUIDforDirectChat(contactId, gUser.userId);
|
||||
return (select(groups)..where((t) => t.groupId.equals(groupId)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchGroupsForChatList() {
|
||||
|
|
@ -228,18 +230,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
}
|
||||
|
||||
Future<Group?> getGroup(String groupId) {
|
||||
return (select(groups)..where((t) => t.groupId.equals(groupId)))
|
||||
.getSingleOrNull();
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(String groupId) {
|
||||
return (select(groups)
|
||||
..where(
|
||||
(u) =>
|
||||
u.groupId.equals(groupId) &
|
||||
u.lastMessageReceived.isNotNull() &
|
||||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
return (select(groups)..where(
|
||||
(u) =>
|
||||
u.groupId.equals(groupId) &
|
||||
u.lastMessageReceived.isNotNull() &
|
||||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingleOrNull()
|
||||
.asyncMap(getFlameCounterFromGroup);
|
||||
}
|
||||
|
|
@ -248,25 +250,28 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
return (select(groups)..where((t) => t.isDirectChat.equals(true))).get();
|
||||
}
|
||||
|
||||
Future<List<Group>> getAllGroups() {
|
||||
return select(groups).get();
|
||||
}
|
||||
|
||||
Future<List<Group>> getAllNotJoinedGroups() {
|
||||
return (select(groups)
|
||||
..where(
|
||||
(t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false),
|
||||
))
|
||||
return (select(groups)..where(
|
||||
(t) => t.joinedGroup.equals(false) & t.isDirectChat.equals(false),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<GroupMember>> getAllGroupMemberWithoutPublicKey() async {
|
||||
try {
|
||||
final query = ((select(groupMembers)
|
||||
..where((t) => t.groupPublicKey.isNull()))
|
||||
.join([
|
||||
leftOuterJoin(
|
||||
groups,
|
||||
groups.groupId.equalsExp(groupMembers.groupId),
|
||||
),
|
||||
])
|
||||
..where(groups.isDirectChat.isNull()));
|
||||
final query =
|
||||
((select(groupMembers)..where((t) => t.groupPublicKey.isNull())).join(
|
||||
[
|
||||
leftOuterJoin(
|
||||
groups,
|
||||
groups.groupId.equalsExp(groupMembers.groupId),
|
||||
),
|
||||
],
|
||||
)..where(groups.isDirectChat.isNull()));
|
||||
return query.map((row) => row.readTable(groupMembers)).get();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
|
|
@ -277,12 +282,11 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
Future<Group?> getDirectChat(int userId) async {
|
||||
final query =
|
||||
((select(groups)..where((t) => t.isDirectChat.equals(true))).join([
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.groupId.equalsExp(groups.groupId),
|
||||
),
|
||||
])
|
||||
..where(groupMembers.contactId.equals(userId)));
|
||||
leftOuterJoin(
|
||||
groupMembers,
|
||||
groupMembers.groupId.equalsExp(groups.groupId),
|
||||
),
|
||||
])..where(groupMembers.contactId.equals(userId)));
|
||||
|
||||
return query.map((row) => row.readTable(groups)).getSingleOrNull();
|
||||
}
|
||||
|
|
@ -300,12 +304,11 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
String groupId,
|
||||
DateTime newLastMessage,
|
||||
) async {
|
||||
await (update(groups)
|
||||
..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)),
|
||||
))
|
||||
await (update(groups)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)),
|
||||
))
|
||||
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,5 +22,7 @@ class GroupsDaoManager {
|
|||
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
|
||||
$$GroupHistoriesTableTableManager get groupHistories =>
|
||||
$$GroupHistoriesTableTableManager(
|
||||
_db.attachedDatabase, _db.groupHistories);
|
||||
_db.attachedDatabase,
|
||||
_db.groupHistories,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
..where(
|
||||
(t) => (t.uploadState.equals(UploadState.initialized.name) |
|
||||
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
||||
t.uploadState.equals(UploadState.uploading.name) |
|
||||
t.uploadState.equals(UploadState.preprocessing.name)),
|
||||
))
|
||||
.get();
|
||||
|
|
|
|||
|
|
@ -44,20 +44,23 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||
final query = select(messages).join([
|
||||
leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)),
|
||||
])
|
||||
..where(
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
mediaFiles.type.equals(MediaType.audio.name).not() &
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
messages.mediaId.isNotNull() &
|
||||
messages.senderId.isNotNull() &
|
||||
messages.type.equals(MessageType.media.name),
|
||||
);
|
||||
final query =
|
||||
select(messages).join([
|
||||
leftOuterJoin(
|
||||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])..where(
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
mediaFiles.type.equals(MediaType.audio.name).not() &
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
messages.mediaId.isNotNull() &
|
||||
messages.senderId.isNotNull() &
|
||||
messages.type.equals(MessageType.media.name),
|
||||
);
|
||||
return query.map((row) => row.readTable(messages)).watch();
|
||||
}
|
||||
|
||||
|
|
@ -70,8 +73,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Stream<List<Message>> watchByGroupId(String groupId) {
|
||||
return ((select(messages)
|
||||
..where(
|
||||
return ((select(messages)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.isDeletedFromSender.equals(true) |
|
||||
|
|
@ -92,21 +94,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
contacts,
|
||||
contacts.userId.equalsExp(groupMembers.contactId),
|
||||
),
|
||||
])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
])..where(groupMembers.groupId.equals(groupId)));
|
||||
return query
|
||||
.map((row) => (row.readTable(groupMembers), row.readTable(contacts)))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<MessageAction>> watchMessageActionChanges(String messageId) {
|
||||
return (select(messageActions)..where((t) => t.messageId.equals(messageId)))
|
||||
.watch();
|
||||
return (select(
|
||||
messageActions,
|
||||
)..where((t) => t.messageId.equals(messageId))).watch();
|
||||
}
|
||||
|
||||
Stream<Message?> watchMessageById(String messageId) {
|
||||
return (select(messages)..where((t) => t.messageId.equals(messageId)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
messages,
|
||||
)..where((t) => t.messageId.equals(messageId))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> purgeMessageTable() async {
|
||||
|
|
@ -114,35 +117,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
|
||||
for (final group in allGroups) {
|
||||
final deletionTime = clock.now().subtract(
|
||||
Duration(
|
||||
milliseconds: group.deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
);
|
||||
await (delete(messages)
|
||||
..where(
|
||||
(m) =>
|
||||
m.groupId.equals(group.groupId) &
|
||||
(m.mediaStored.equals(true) &
|
||||
m.isDeletedFromSender.equals(true) |
|
||||
m.mediaStored.equals(false)) &
|
||||
(m.openedAt.isSmallerThanValue(deletionTime) |
|
||||
(m.isDeletedFromSender.equals(true) &
|
||||
m.createdAt.isSmallerThanValue(deletionTime))),
|
||||
))
|
||||
Duration(
|
||||
milliseconds: group.deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
);
|
||||
await (delete(messages)..where(
|
||||
(m) =>
|
||||
m.groupId.equals(group.groupId) &
|
||||
(m.mediaStored.equals(true) &
|
||||
m.isDeletedFromSender.equals(true) |
|
||||
m.mediaStored.equals(false)) &
|
||||
(m.openedAt.isSmallerThanValue(deletionTime) |
|
||||
(m.isDeletedFromSender.equals(true) &
|
||||
m.createdAt.isSmallerThanValue(deletionTime))),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openedAllTextMessages(String groupId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(clock.now()));
|
||||
return (update(messages)
|
||||
..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
t.senderId.isNotNull() &
|
||||
t.openedAt.isNull() &
|
||||
t.type.equals(MessageType.text.name),
|
||||
))
|
||||
return (update(messages)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
t.senderId.isNotNull() &
|
||||
t.openedAt.isNull() &
|
||||
t.type.equals(MessageType.text.name),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
|
|
@ -158,29 +159,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
if (msg.mediaId != null && contactId != null) {
|
||||
// contactId -> When a image is send to multiple and one message is delete the image should be still available...
|
||||
await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!)))
|
||||
.go();
|
||||
await (delete(
|
||||
mediaFiles,
|
||||
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
|
||||
|
||||
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
|
||||
if (mediaService != null) {
|
||||
mediaService.fullMediaRemoval();
|
||||
}
|
||||
}
|
||||
await (delete(messageHistories)
|
||||
..where((t) => t.messageId.equals(messageId)))
|
||||
.go();
|
||||
await (delete(
|
||||
messageHistories,
|
||||
)..where((t) => t.messageId.equals(messageId))).go();
|
||||
|
||||
await (update(messages)
|
||||
..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
await (update(messages)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
.write(
|
||||
const MessagesCompanion(
|
||||
isDeletedFromSender: Value(true),
|
||||
content: Value(null),
|
||||
mediaId: Value(null),
|
||||
),
|
||||
);
|
||||
const MessagesCompanion(
|
||||
isDeletedFromSender: Value(true),
|
||||
content: Value(null),
|
||||
mediaId: Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleTextEdit(
|
||||
|
|
@ -200,16 +201,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
createdAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
await (update(messages)
|
||||
..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
await (update(messages)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
.write(
|
||||
MessagesCompanion(
|
||||
content: Value(text),
|
||||
modifiedAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
MessagesCompanion(
|
||||
content: Value(text),
|
||||
modifiedAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleMessagesOpened(
|
||||
|
|
@ -232,8 +232,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
for (final messageId in messageIds) {
|
||||
final isOpenedByAll =
|
||||
await haveAllMembers(messageId, MessageActionType.openedAt);
|
||||
final isOpenedByAll = await haveAllMembers(
|
||||
messageId,
|
||||
MessageActionType.openedAt,
|
||||
);
|
||||
final now = clock.now();
|
||||
|
||||
batch.update(
|
||||
|
|
@ -271,17 +273,19 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
MessageActionType action,
|
||||
) async {
|
||||
final message =
|
||||
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null) return true;
|
||||
final members =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId);
|
||||
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||
message.groupId,
|
||||
);
|
||||
|
||||
final actions = await (select(messageActions)
|
||||
..where(
|
||||
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
|
||||
))
|
||||
.get();
|
||||
final actions =
|
||||
await (select(messageActions)..where(
|
||||
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
|
||||
))
|
||||
.get();
|
||||
|
||||
return members.length == actions.length;
|
||||
}
|
||||
|
|
@ -290,16 +294,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
MessagesCompanion updatedValues,
|
||||
) async {
|
||||
await (update(messages)..where((c) => c.messageId.equals(messageId)))
|
||||
.write(updatedValues);
|
||||
await (update(
|
||||
messages,
|
||||
)..where((c) => c.messageId.equals(messageId))).write(updatedValues);
|
||||
}
|
||||
|
||||
Future<void> updateMessagesByMediaId(
|
||||
String mediaId,
|
||||
MessagesCompanion updatedValues,
|
||||
) {
|
||||
return (update(messages)..where((c) => c.mediaId.equals(mediaId)))
|
||||
.write(updatedValues);
|
||||
return (update(
|
||||
messages,
|
||||
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
|
||||
}
|
||||
|
||||
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)))
|
||||
.getSingle();
|
||||
return await (select(
|
||||
messages,
|
||||
)..where((t) => t.rowId.equals(rowId))).getSingle();
|
||||
} catch (e) {
|
||||
Log.error('Could not insert message: $e');
|
||||
return null;
|
||||
|
|
@ -342,11 +349,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Future<MessageAction?> getLastMessageAction(String messageId) async {
|
||||
return (((select(messageActions)
|
||||
..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
|
||||
return (((select(messageActions)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
|
@ -373,8 +379,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
contacts,
|
||||
contacts.userId.equalsExp(messageActions.contactId),
|
||||
),
|
||||
])
|
||||
..where(messageActions.messageId.equals(messageId)));
|
||||
])..where(messageActions.messageId.equals(messageId)));
|
||||
return query
|
||||
.map((row) => (row.readTable(messageActions), row.readTable(contacts)))
|
||||
.watch();
|
||||
|
|
|
|||
|
|
@ -31,10 +31,14 @@ class MessagesDaoManager {
|
|||
$$ReactionsTableTableManager(_db.attachedDatabase, _db.reactions);
|
||||
$$MessageHistoriesTableTableManager get messageHistories =>
|
||||
$$MessageHistoriesTableTableManager(
|
||||
_db.attachedDatabase, _db.messageHistories);
|
||||
_db.attachedDatabase,
|
||||
_db.messageHistories,
|
||||
);
|
||||
$$GroupMembersTableTableManager get groupMembers =>
|
||||
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
|
||||
$$MessageActionsTableTableManager get messageActions =>
|
||||
$$MessageActionsTableTableManager(
|
||||
_db.attachedDatabase, _db.messageActions);
|
||||
_db.attachedDatabase,
|
||||
_db.messageActions,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.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/receipts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
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 {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -16,12 +20,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
ReceiptsDao(super.db);
|
||||
|
||||
Future<void> confirmReceipt(String receiptId, int fromUserId) async {
|
||||
final receipt = await (select(receipts)
|
||||
..where(
|
||||
(t) =>
|
||||
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
final receipt =
|
||||
await (select(receipts)..where(
|
||||
(t) =>
|
||||
t.receiptId.equals(receiptId) &
|
||||
t.contactId.equals(fromUserId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
|
||||
if (receipt == null) return;
|
||||
|
||||
|
|
@ -33,34 +38,42 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
type: const Value(MessageActionType.ackByUserAt),
|
||||
),
|
||||
);
|
||||
await handleMediaRelatedResponseFromReceiver(receipt.messageId!);
|
||||
}
|
||||
|
||||
await (delete(receipts)
|
||||
..where(
|
||||
(t) =>
|
||||
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
|
||||
))
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> deleteReceipt(String receiptId) async {
|
||||
await (delete(receipts)
|
||||
..where(
|
||||
(t) => t.receiptId.equals(receiptId),
|
||||
))
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.receiptId.equals(receiptId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> purgeReceivedReceipts() async {
|
||||
await (delete(receivedReceipts)
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
await (delete(receivedReceipts)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.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 {
|
||||
|
|
@ -72,8 +85,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
);
|
||||
}
|
||||
final id = await into(receipts).insert(insertEntry);
|
||||
return await (select(receipts)..where((t) => t.rowId.equals(id)))
|
||||
.getSingle();
|
||||
return await (select(
|
||||
receipts,
|
||||
)..where((t) => t.rowId.equals(id))).getSingle();
|
||||
} catch (e) {
|
||||
// ignore error, receipts is already in the database...
|
||||
return null;
|
||||
|
|
@ -82,10 +96,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
|
||||
Future<Receipt?> getReceiptById(String receiptId) async {
|
||||
try {
|
||||
return await (select(receipts)
|
||||
..where(
|
||||
(t) => t.receiptId.equals(receiptId),
|
||||
))
|
||||
return await (select(receipts)..where(
|
||||
(t) => t.receiptId.equals(receiptId),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
|
|
@ -95,19 +108,20 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
|
||||
Future<List<Receipt>> getReceiptsForRetransmission() async {
|
||||
final markedRetriesTime = clock.now().subtract(
|
||||
const Duration(
|
||||
// give the server time to transmit all messages to the client
|
||||
seconds: 20,
|
||||
),
|
||||
);
|
||||
return (select(receipts)
|
||||
..where(
|
||||
(t) =>
|
||||
t.ackByServerAt.isNull() |
|
||||
t.markForRetry.isSmallerThanValue(markedRetriesTime) |
|
||||
t.markForRetryAfterAccepted
|
||||
.isSmallerThanValue(markedRetriesTime),
|
||||
))
|
||||
const Duration(
|
||||
// give the server time to transmit all messages to the client
|
||||
seconds: 20,
|
||||
),
|
||||
);
|
||||
return (select(receipts)..where(
|
||||
(t) =>
|
||||
(t.ackByServerAt.isNull() |
|
||||
t.markForRetry.isSmallerThanValue(markedRetriesTime) |
|
||||
t.markForRetryAfterAccepted.isSmallerThanValue(
|
||||
markedRetriesTime,
|
||||
)) &
|
||||
t.willBeRetriedByMediaUpload.equals(false),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
|
|
@ -119,8 +133,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
String receiptId,
|
||||
ReceiptsCompanion updates,
|
||||
) async {
|
||||
await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
|
||||
.write(updates);
|
||||
await (update(
|
||||
receipts,
|
||||
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
|
||||
}
|
||||
|
||||
Future<void> updateReceiptWidthUserId(
|
||||
|
|
@ -128,31 +143,35 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
String receiptId,
|
||||
ReceiptsCompanion updates,
|
||||
) async {
|
||||
await (update(receipts)
|
||||
..where(
|
||||
(c) =>
|
||||
c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
|
||||
))
|
||||
await (update(receipts)..where(
|
||||
(c) => c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> markMessagesForRetry(int contactId) async {
|
||||
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write(
|
||||
ReceiptsCompanion(
|
||||
markForRetry: Value(clock.now()),
|
||||
),
|
||||
);
|
||||
await (update(receipts)..where(
|
||||
(c) =>
|
||||
c.contactId.equals(contactId) &
|
||||
c.willBeRetriedByMediaUpload.equals(false),
|
||||
))
|
||||
.write(
|
||||
ReceiptsCompanion(
|
||||
markForRetry: Value(clock.now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isDuplicated(String receiptId) async {
|
||||
return await (select(receivedReceipts)
|
||||
..where((t) => t.receiptId.equals(receiptId)))
|
||||
.getSingleOrNull() !=
|
||||
return await (select(
|
||||
receivedReceipts,
|
||||
)..where((t) => t.receiptId.equals(receiptId))).getSingleOrNull() !=
|
||||
null;
|
||||
}
|
||||
|
||||
Future<void> gotReceipt(String receiptId) async {
|
||||
await into(receivedReceipts)
|
||||
.insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
|
||||
await into(
|
||||
receivedReceipts,
|
||||
).insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,12 @@ class ReceiptsDaoManager {
|
|||
$$ReceiptsTableTableManager(_db.attachedDatabase, _db.receipts);
|
||||
$$MessageActionsTableTableManager get messageActions =>
|
||||
$$MessageActionsTableTableManager(
|
||||
_db.attachedDatabase, _db.messageActions);
|
||||
_db.attachedDatabase,
|
||||
_db.messageActions,
|
||||
);
|
||||
$$ReceivedReceiptsTableTableManager get receivedReceipts =>
|
||||
$$ReceivedReceiptsTableTableManager(
|
||||
_db.attachedDatabase, _db.receivedReceipts);
|
||||
_db.attachedDatabase,
|
||||
_db.receivedReceipts,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
2075
lib/src/database/schemas/twonly_db/drift_schema_v10.json
Normal file
2075
lib/src/database/schemas/twonly_db/drift_schema_v10.json
Normal file
File diff suppressed because it is too large
Load diff
2061
lib/src/database/schemas/twonly_db/drift_schema_v9.json
Normal file
2061
lib/src/database/schemas/twonly_db/drift_schema_v9.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,9 @@ enum UploadState {
|
|||
uploaded,
|
||||
|
||||
uploadLimitReached,
|
||||
|
||||
// File is to big to be uploaded
|
||||
fileLimitReached,
|
||||
// readyToUpload,
|
||||
// uploadTaskStarted,
|
||||
// receiverNotified,
|
||||
|
|
@ -48,6 +51,8 @@ class MediaFiles extends Table {
|
|||
BoolColumn get stored => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get preProgressingProcess => integer().nullable()();
|
||||
|
||||
TextColumn get reuploadRequestedBy =>
|
||||
text().map(IntListTypeConverter()).nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -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/mediafiles.table.dart';
|
||||
|
||||
enum MessageType { media, text, contacts }
|
||||
enum MessageType { media, text, contacts, restoreFlameCounter }
|
||||
|
||||
@DataClassName('Message')
|
||||
class Messages extends Table {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ class Receipts extends Table {
|
|||
BoolColumn get contactWillSendsReceipt =>
|
||||
boolean().withDefault(const Constant(true))();
|
||||
|
||||
BoolColumn get willBeRetriedByMediaUpload =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
DateTimeColumn get markForRetry => dateTime().nullable()();
|
||||
DateTimeColumn get markForRetryAfterAccepted => dateTime().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -54,15 +54,15 @@ part 'twonly.db.g.dart';
|
|||
)
|
||||
class TwonlyDB extends _$TwonlyDB {
|
||||
TwonlyDB([QueryExecutor? e])
|
||||
: super(
|
||||
e ?? _openConnection(),
|
||||
);
|
||||
: super(
|
||||
e ?? _openConnection(),
|
||||
);
|
||||
|
||||
// ignore: matching_super_parameters
|
||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 8;
|
||||
int get schemaVersion => 10;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
|
|
@ -131,6 +131,18 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
// ignore: experimental_member_use
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
|
@ -154,21 +166,20 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
}
|
||||
|
||||
Future<void> deleteDataForTwonlySafe() async {
|
||||
await (delete(messages)
|
||||
..where(
|
||||
(t) => (t.mediaStored.equals(false) &
|
||||
t.isDeletedFromSender.equals(false)),
|
||||
))
|
||||
await (delete(messages)..where(
|
||||
(t) =>
|
||||
(t.mediaStored.equals(false) &
|
||||
t.isDeletedFromSender.equals(false)),
|
||||
))
|
||||
.go();
|
||||
await update(messages).write(
|
||||
const MessagesCompanion(
|
||||
downloadToken: Value(null),
|
||||
),
|
||||
);
|
||||
await (delete(mediaFiles)
|
||||
..where(
|
||||
(t) => (t.stored.equals(false)),
|
||||
))
|
||||
await (delete(mediaFiles)..where(
|
||||
(t) => (t.stored.equals(false)),
|
||||
))
|
||||
.go();
|
||||
await delete(receipts).go();
|
||||
await delete(receivedReceipts).go();
|
||||
|
|
@ -178,14 +189,13 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
senderProfileCounter: Value(0),
|
||||
),
|
||||
);
|
||||
await (delete(signalPreKeyStores)
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
await (delete(signalPreKeyStores)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -64,7 +64,7 @@ import 'app_localizations_sv.dart';
|
|||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
|
|
@ -87,17 +87,17 @@ abstract class AppLocalizations {
|
|||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('sv')
|
||||
Locale('sv'),
|
||||
];
|
||||
|
||||
/// No description provided for @registerTitle.
|
||||
|
|
@ -685,7 +685,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @settingsPrivacy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Privacy'**
|
||||
/// **'Privacy & Security'**
|
||||
String get settingsPrivacy;
|
||||
|
||||
/// 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.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3033,6 +3039,24 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Unknown contact whose identity has not yet been verified.'**
|
||||
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
|
||||
|
|
@ -3064,8 +3088,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
|||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.');
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get settingsAppearance => 'Erscheinungsbild';
|
||||
|
||||
@override
|
||||
String get settingsPrivacy => 'Datenschutz';
|
||||
String get settingsPrivacy => 'Datenschutz & Sicherheit';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsers => 'Benutzer blockieren';
|
||||
|
|
@ -1116,6 +1116,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get uploadLimitReached =>
|
||||
'Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.';
|
||||
|
||||
@override
|
||||
String get fileLimitReached => 'Maximale Dateigröße\nerreicht';
|
||||
|
||||
@override
|
||||
String get retransmissionRequested => 'Wird erneut versucht.';
|
||||
|
||||
|
|
@ -1695,4 +1698,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get verificationBadgeRedDesc =>
|
||||
'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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get settingsAppearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get settingsPrivacy => 'Privacy';
|
||||
String get settingsPrivacy => 'Privacy & Security';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsers => 'Block users';
|
||||
|
|
@ -1109,6 +1109,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get uploadLimitReached =>
|
||||
'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.';
|
||||
|
||||
@override
|
||||
String get fileLimitReached => 'Maximum file size\nexceeded';
|
||||
|
||||
@override
|
||||
String get retransmissionRequested => 'Retransmission requested';
|
||||
|
||||
|
|
@ -1683,4 +1686,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get verificationBadgeRedDesc =>
|
||||
'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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
String get settingsAppearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get settingsPrivacy => 'Privacy';
|
||||
String get settingsPrivacy => 'Privacy & Security';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsers => 'Block users';
|
||||
|
|
@ -1109,6 +1109,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
String get uploadLimitReached =>
|
||||
'The upload limit has\nbeen reached. Upgrade to Pro\nor wait until tomorrow.';
|
||||
|
||||
@override
|
||||
String get fileLimitReached => 'Maximum file size\nexceeded';
|
||||
|
||||
@override
|
||||
String get retransmissionRequested => 'Retransmission requested';
|
||||
|
||||
|
|
@ -1683,4 +1686,17 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get verificationBadgeRedDesc =>
|
||||
'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
|
||||
|
|
@ -8,14 +8,16 @@ part of 'signal_identity.dart';
|
|||
|
||||
SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) =>
|
||||
SignalIdentity(
|
||||
identityKeyPairU8List: const Uint8ListConverter()
|
||||
.fromJson(json['identityKeyPairU8List'] as String),
|
||||
identityKeyPairU8List: const Uint8ListConverter().fromJson(
|
||||
json['identityKeyPairU8List'] as String,
|
||||
),
|
||||
registrationId: (json['registrationId'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) =>
|
||||
<String, dynamic>{
|
||||
'registrationId': instance.registrationId,
|
||||
'identityKeyPairU8List':
|
||||
const Uint8ListConverter().toJson(instance.identityKeyPairU8List),
|
||||
'identityKeyPairU8List': const Uint8ListConverter().toJson(
|
||||
instance.identityKeyPairU8List,
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ part of 'userdata.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
||||
userId: (json['userId'] as num).toInt(),
|
||||
username: json['username'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||
)
|
||||
UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||
UserData(
|
||||
userId: (json['userId'] as num).toInt(),
|
||||
username: json['username'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
|
||||
|
|
@ -25,7 +26,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
|
||||
..themeMode =
|
||||
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
ThemeMode.system
|
||||
ThemeMode.system
|
||||
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
|
||||
..requestedAudioPermission =
|
||||
json['requestedAudioPermission'] as bool? ?? false
|
||||
|
|
@ -38,9 +39,11 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
.toList()
|
||||
..autoDownloadOptions =
|
||||
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) =>
|
||||
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()),
|
||||
)
|
||||
(k, e) => MapEntry(
|
||||
k,
|
||||
(e as List<dynamic>).map((e) => e as String).toList(),
|
||||
),
|
||||
)
|
||||
..storeMediaFilesInGallery =
|
||||
json['storeMediaFilesInGallery'] as bool? ?? false
|
||||
..autoStoreAllSendUnlimitedMediaFiles =
|
||||
|
|
@ -53,8 +56,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..myBestFriendGroupId = json['myBestFriendGroupId'] as String?
|
||||
..signalLastSignedPreKeyUpdated =
|
||||
json['signalLastSignedPreKeyUpdated'] == null
|
||||
? null
|
||||
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
|
||||
? null
|
||||
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
|
||||
..allowErrorTrackingViaSentry =
|
||||
json['allowErrorTrackingViaSentry'] as bool? ?? false
|
||||
..currentPreKeyIndexStart =
|
||||
|
|
@ -75,7 +78,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..twonlySafeBackup = json['twonlySafeBackup'] == null
|
||||
? null
|
||||
: TwonlySafeBackup.fromJson(
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>)
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>,
|
||||
)
|
||||
..askedForUserStudyPermission =
|
||||
json['askedForUserStudyPermission'] as bool? ?? false
|
||||
..userStudyParticipantsToken =
|
||||
|
|
@ -85,52 +89,51 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
: DateTime.parse(json['lastUserStudyDataUpload'] as String);
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'appVersion': instance.appVersion,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
'deviceId': instance.deviceId,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
'todaysImageCounter': instance.todaysImageCounter,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'requestedAudioPermission': instance.requestedAudioPermission,
|
||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||
'showShowImagePreviewWhenSending':
|
||||
instance.showShowImagePreviewWhenSending,
|
||||
'startWithCameraOpen': instance.startWithCameraOpen,
|
||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||
'autoDownloadOptions': instance.autoDownloadOptions,
|
||||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||
'autoStoreAllSendUnlimitedMediaFiles':
|
||||
instance.autoStoreAllSendUnlimitedMediaFiles,
|
||||
'lastPlanBallance': instance.lastPlanBallance,
|
||||
'additionalUserInvites': instance.additionalUserInvites,
|
||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||
'myBestFriendGroupId': instance.myBestFriendGroupId,
|
||||
'signalLastSignedPreKeyUpdated':
|
||||
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
|
||||
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
|
||||
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
|
||||
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
|
||||
'lastChangeLogHash': instance.lastChangeLogHash,
|
||||
'hideChangeLog': instance.hideChangeLog,
|
||||
'updateFCMToken': instance.updateFCMToken,
|
||||
'nextTimeToShowBackupNotice':
|
||||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
|
||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||
'lastUserStudyDataUpload':
|
||||
instance.lastUserStudyDataUpload?.toIso8601String(),
|
||||
};
|
||||
'userId': instance.userId,
|
||||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'appVersion': instance.appVersion,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
'deviceId': instance.deviceId,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
'todaysImageCounter': instance.todaysImageCounter,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'requestedAudioPermission': instance.requestedAudioPermission,
|
||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
|
||||
'startWithCameraOpen': instance.startWithCameraOpen,
|
||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||
'autoDownloadOptions': instance.autoDownloadOptions,
|
||||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||
'autoStoreAllSendUnlimitedMediaFiles':
|
||||
instance.autoStoreAllSendUnlimitedMediaFiles,
|
||||
'lastPlanBallance': instance.lastPlanBallance,
|
||||
'additionalUserInvites': instance.additionalUserInvites,
|
||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||
'myBestFriendGroupId': instance.myBestFriendGroupId,
|
||||
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
|
||||
?.toIso8601String(),
|
||||
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
|
||||
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
|
||||
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
|
||||
'lastChangeLogHash': instance.lastChangeLogHash,
|
||||
'hideChangeLog': instance.hideChangeLog,
|
||||
'updateFCMToken': instance.updateFCMToken,
|
||||
'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice
|
||||
?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
|
||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
|
||||
?.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
ThemeMode.system: 'system',
|
||||
|
|
@ -140,16 +143,18 @@ const _$ThemeModeEnumMap = {
|
|||
|
||||
TwonlySafeBackup _$TwonlySafeBackupFromJson(Map<String, dynamic> json) =>
|
||||
TwonlySafeBackup(
|
||||
backupId: (json['backupId'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
encryptionKey: (json['encryptionKey'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
)
|
||||
backupId: (json['backupId'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
encryptionKey: (json['encryptionKey'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
)
|
||||
..lastBackupSize = (json['lastBackupSize'] as num).toInt()
|
||||
..backupUploadState =
|
||||
$enumDecode(_$LastBackupUploadStateEnumMap, json['backupUploadState'])
|
||||
..backupUploadState = $enumDecode(
|
||||
_$LastBackupUploadStateEnumMap,
|
||||
json['backupUploadState'],
|
||||
)
|
||||
..lastBackupDone = json['lastBackupDone'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastBackupDone'] as String);
|
||||
|
|
@ -172,10 +177,10 @@ const _$LastBackupUploadStateEnumMap = {
|
|||
};
|
||||
|
||||
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
|
||||
serverUrl: json['serverUrl'] as String,
|
||||
retentionDays: (json['retentionDays'] as num).toInt(),
|
||||
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
|
||||
);
|
||||
serverUrl: json['serverUrl'] as String,
|
||||
retentionDays: (json['retentionDays'] as num).toInt(),
|
||||
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BackupServerToJson(BackupServer instance) =>
|
||||
<String, dynamic>{
|
||||
|
|
|
|||
|
|
@ -536,12 +536,14 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? authToken,
|
||||
$core.String? appVersion,
|
||||
$fixnum.Int64? deviceId,
|
||||
$core.bool? inBackground,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
if (authToken != null) result.authToken = authToken;
|
||||
if (appVersion != null) result.appVersion = appVersion;
|
||||
if (deviceId != null) result.deviceId = deviceId;
|
||||
if (inBackground != null) result.inBackground = inBackground;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -564,6 +566,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage {
|
|||
2, _omitFieldNames ? '' : 'authToken', $pb.PbFieldType.OY)
|
||||
..aOS(3, _omitFieldNames ? '' : 'appVersion')
|
||||
..aInt64(4, _omitFieldNames ? '' : 'deviceId')
|
||||
..aOB(5, _omitFieldNames ? '' : 'inBackground')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$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);
|
||||
@$pb.TagNumber(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 {
|
||||
|
|
|
|||
|
|
@ -229,10 +229,20 @@ const Handshake_Authenticate$json = {
|
|||
'10': 'deviceId',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'in_background',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 8,
|
||||
'9': 2,
|
||||
'10': 'inBackground',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_app_version'},
|
||||
{'1': '_device_id'},
|
||||
{'1': '_in_background'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -254,10 +264,11 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
|
|||
'QSFQoGaXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIK'
|
||||
'DXByb29mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrQg4KDF9pbnZpdGVfY29kZRoSChBHZX'
|
||||
'RBdXRoQ2hhbGxlbmdlGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQS'
|
||||
'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGqwBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB'
|
||||
'GgoIcmVzcG9uc2UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZB'
|
||||
'gBIAEoA1IGdXNlcklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVy'
|
||||
'c2lvbhgDIAEoCUgAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2'
|
||||
'VJZIgBAUIOCgxfYXBwX3ZlcnNpb25CDAoKX2RldmljZV9pZEILCglIYW5kc2hha2U=');
|
||||
'VJZIgBARIoCg1pbl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBw'
|
||||
'X3ZlcnNpb25CDAoKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZEILCglIYW5kc2hha2U=');
|
||||
|
||||
@$core.Deprecated('Use applicationDataDescriptor instead')
|
||||
const ApplicationData$json = {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
ErrorCode._(1034, _omitEnumNames ? '' : 'IPAPaymentExpired');
|
||||
static const ErrorCode UserIsNotInFreePlan =
|
||||
ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan');
|
||||
static const ErrorCode ForegroundSessionConnected =
|
||||
ErrorCode._(1036, _omitEnumNames ? '' : 'ForegroundSessionConnected');
|
||||
|
||||
static const $core.List<ErrorCode> values = <ErrorCode>[
|
||||
Unknown,
|
||||
|
|
@ -131,6 +133,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
RegistrationDisabled,
|
||||
IPAPaymentExpired,
|
||||
UserIsNotInFreePlan,
|
||||
ForegroundSessionConnected,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ErrorCode> _byValue =
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const ErrorCode$json = {
|
|||
{'1': 'RegistrationDisabled', '2': 1033},
|
||||
{'1': 'IPAPaymentExpired', '2': 1034},
|
||||
{'1': 'UserIsNotInFreePlan', '2': 1035},
|
||||
{'1': 'ForegroundSessionConnected', '2': 1036},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -77,4 +78,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
|||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
||||
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
|
||||
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
|
||||
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsI');
|
||||
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsIEh8KGkZvcmVncm'
|
||||
'91bmRTZXNzaW9uQ29ubmVjdGVkEIwI');
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ message AdditionalMessageData {
|
|||
enum Type {
|
||||
LINK = 0;
|
||||
CONTACTS = 1;
|
||||
RESTORED_FLAME_COUNTER = 2;
|
||||
}
|
||||
Type type = 1;
|
||||
|
||||
optional string link = 2;
|
||||
repeated SharedContact contacts = 3;
|
||||
optional int64 restored_flame_counter = 4;
|
||||
}
|
||||
|
|
@ -106,11 +106,14 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
AdditionalMessageData_Type? type,
|
||||
$core.String? link,
|
||||
$core.Iterable<SharedContact>? contacts,
|
||||
$fixnum.Int64? restoredFlameCounter,
|
||||
}) {
|
||||
final result = create();
|
||||
if (type != null) result.type = type;
|
||||
if (link != null) result.link = link;
|
||||
if (contacts != null) result.contacts.addAll(contacts);
|
||||
if (restoredFlameCounter != null)
|
||||
result.restoredFlameCounter = restoredFlameCounter;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +138,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
..pc<SharedContact>(
|
||||
3, _omitFieldNames ? '' : 'contacts', $pb.PbFieldType.PM,
|
||||
subBuilder: SharedContact.create)
|
||||
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -180,6 +184,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
|
||||
@$pb.TagNumber(3)
|
||||
$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 =
|
||||
|
|
|
|||
|
|
@ -19,15 +19,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
|
|||
AdditionalMessageData_Type._(0, _omitEnumNames ? '' : 'LINK');
|
||||
static const AdditionalMessageData_Type 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 =
|
||||
<AdditionalMessageData_Type>[
|
||||
LINK,
|
||||
CONTACTS,
|
||||
RESTORED_FLAME_COUNTER,
|
||||
];
|
||||
|
||||
static final $core.List<AdditionalMessageData_Type?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 1);
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 2);
|
||||
static AdditionalMessageData_Type? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,20 @@ const AdditionalMessageData$json = {
|
|||
'6': '.SharedContact',
|
||||
'10': 'contacts'
|
||||
},
|
||||
{
|
||||
'1': 'restored_flame_counter',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'9': 1,
|
||||
'10': 'restoredFlameCounter',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'4': [AdditionalMessageData_Type$json],
|
||||
'8': [
|
||||
{'1': '_link'},
|
||||
{'1': '_restored_flame_counter'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -70,6 +80,7 @@ const AdditionalMessageData_Type$json = {
|
|||
'2': [
|
||||
{'1': 'LINK', '2': 0},
|
||||
{'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(
|
||||
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
|
||||
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
|
||||
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzIh4KBFR5cGUSCAoETElOSxAAEgwKCENPTl'
|
||||
'RBQ1RTEAFCBwoFX2xpbms=');
|
||||
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
|
||||
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI'
|
||||
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3'
|
||||
'RvcmVkX2ZsYW1lX2NvdW50ZXI=');
|
||||
|
|
|
|||
|
|
@ -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/server_messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.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/utils.signal.dart';
|
||||
|
|
@ -46,6 +46,7 @@ import 'package:web_socket_channel/io.dart';
|
|||
|
||||
final lockConnecting = Mutex();
|
||||
final lockRetransStore = Mutex();
|
||||
final lockAuthentication = Mutex();
|
||||
|
||||
/// The ApiProvider is responsible for communicating with the server.
|
||||
/// 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
|
||||
Future<void> onAuthenticated() async {
|
||||
isAuthenticated = true;
|
||||
await initFCMAfterAuthenticated();
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
|
||||
if (!globalIsAppInBackground) {
|
||||
if (globalIsInBackgroundTask) {
|
||||
await retransmitRawBytes();
|
||||
await tryTransmitMessages();
|
||||
await tryDownloadAllMediaFiles();
|
||||
} else if (!globalIsAppInBackground) {
|
||||
unawaited(retransmitRawBytes());
|
||||
unawaited(tryTransmitMessages());
|
||||
unawaited(tryDownloadAllMediaFiles());
|
||||
|
|
@ -124,6 +128,7 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> startReconnectionTimer() async {
|
||||
if (globalIsInBackgroundTask) return;
|
||||
if (reconnectionTimer?.isActive ?? false) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -152,8 +157,9 @@ class ApiService {
|
|||
if (connectivitySubscription != null) {
|
||||
return;
|
||||
}
|
||||
connectivitySubscription =
|
||||
Connectivity().onConnectivityChanged.listen((result) async {
|
||||
connectivitySubscription = Connectivity().onConnectivityChanged.listen((
|
||||
result,
|
||||
) async {
|
||||
if (!result.contains(ConnectivityResult.none)) {
|
||||
await connect();
|
||||
}
|
||||
|
|
@ -330,7 +336,9 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
if (res.isError) {
|
||||
Log.warn('Got error from server: ${res.error}');
|
||||
if (res.error != ErrorCode.ForegroundSessionConnected) {
|
||||
Log.warn('Got error from server: ${res.error}');
|
||||
}
|
||||
if (res.error == ErrorCode.AppVersionOutdated) {
|
||||
globalCallbackAppIsOutdated();
|
||||
Log.warn('App Version is OUTDATED.');
|
||||
|
|
@ -348,7 +356,6 @@ class ApiService {
|
|||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
if (res.error == ErrorCode.SessionNotAuthenticated) {
|
||||
isAuthenticated = false;
|
||||
if (authenticated) {
|
||||
await authenticate();
|
||||
if (isAuthenticated) {
|
||||
|
|
@ -380,8 +387,9 @@ class ApiService {
|
|||
|
||||
Future<bool> tryAuthenticateWithToken(int userId) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final apiAuthToken =
|
||||
await storage.read(key: SecureStorageKeys.apiAuthToken);
|
||||
final apiAuthToken = await storage.read(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
final user = await getUser();
|
||||
|
||||
if (apiAuthToken != null && user != null) {
|
||||
|
|
@ -395,6 +403,7 @@ class ApiService {
|
|||
..userId = Int64(userId)
|
||||
..appVersion = (await PackageInfo.fromPlatform()).version
|
||||
..deviceId = Int64(user.deviceId)
|
||||
..inBackground = globalIsInBackgroundTask
|
||||
..authToken = base64Decode(apiAuthToken);
|
||||
|
||||
final handshake = Handshake()..authenticate = authenticate;
|
||||
|
|
@ -404,12 +413,20 @@ class ApiService {
|
|||
|
||||
if (result.isSuccess) {
|
||||
Log.info('websocket is authenticated');
|
||||
unawaited(onAuthenticated());
|
||||
isAuthenticated = true;
|
||||
if (globalIsInBackgroundTask) {
|
||||
await onAuthenticated();
|
||||
} else {
|
||||
unawaited(onAuthenticated());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (result.isError) {
|
||||
if (result.error != ErrorCode.AuthTokenNotValid) {
|
||||
Log.error('got error while authenticating to the server: $result');
|
||||
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||
Log.error(
|
||||
'got error while authenticating to the server: ${result.error}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -418,60 +435,66 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> authenticate() async {
|
||||
if (isAuthenticated) return;
|
||||
if (await getSignalIdentity() == null) {
|
||||
return;
|
||||
}
|
||||
return lockAuthentication.protect(() async {
|
||||
if (isAuthenticated) return;
|
||||
if (await getSignalIdentity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final userData = await getUser();
|
||||
if (userData == null) return;
|
||||
final userData = await getUser();
|
||||
if (userData == null) return;
|
||||
|
||||
if (await tryAuthenticateWithToken(userData.userId)) {
|
||||
return;
|
||||
}
|
||||
if (await tryAuthenticateWithToken(userData.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final handshake = Handshake()
|
||||
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
final handshake = Handshake()
|
||||
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.warn('could not request auth challenge', result);
|
||||
return;
|
||||
}
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.warn('could not request auth challenge', result);
|
||||
return;
|
||||
}
|
||||
|
||||
final challenge = result.value.authchallenge;
|
||||
final challenge = result.value.authchallenge;
|
||||
|
||||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
||||
if (privKey == null) return;
|
||||
final random = getRandomUint8List(32);
|
||||
final signature = sign(privKey.serialize(), challenge as Uint8List, random);
|
||||
privKey = null;
|
||||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
||||
if (privKey == null) return;
|
||||
final random = getRandomUint8List(32);
|
||||
final signature = sign(
|
||||
privKey.serialize(),
|
||||
challenge as Uint8List,
|
||||
random,
|
||||
);
|
||||
privKey = null;
|
||||
|
||||
final getAuthToken = Handshake_GetAuthToken()
|
||||
..response = signature
|
||||
..userId = Int64(userData.userId);
|
||||
final getAuthToken = Handshake_GetAuthToken()
|
||||
..response = signature
|
||||
..userId = Int64(userData.userId);
|
||||
|
||||
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
|
||||
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
|
||||
|
||||
final req2 = createClientToServerFromHandshake(getauthtoken);
|
||||
final req2 = createClientToServerFromHandshake(getauthtoken);
|
||||
|
||||
final result2 = await sendRequestSync(req2, authenticated: false);
|
||||
if (result2.isError) {
|
||||
Log.error('could not send auth response: ${result2.error}');
|
||||
return;
|
||||
}
|
||||
final result2 = await sendRequestSync(req2, authenticated: false);
|
||||
if (result2.isError) {
|
||||
Log.error('could not send auth response: ${result2.error}');
|
||||
return;
|
||||
}
|
||||
|
||||
final apiAuthToken = result2.value.authtoken as Uint8List;
|
||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
final apiAuthToken = result2.value.authtoken as Uint8List;
|
||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result> register(
|
||||
|
|
@ -490,8 +513,9 @@ class ApiService {
|
|||
|
||||
final register = Handshake_Register()
|
||||
..username = username
|
||||
..publicIdentityKey =
|
||||
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
|
||||
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
|
||||
.getPublicKey()
|
||||
.serialize()
|
||||
..registrationId = Int64(signalIdentity.registrationId)
|
||||
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
||||
..signedPrekeySignature = signedPreKey.signature
|
||||
|
|
@ -511,8 +535,10 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> checkForDeletedUsernames() async {
|
||||
final users = await twonlyDB.contactsDao
|
||||
.getContactsByUsername('[deleted]', username2: '[Unknown]');
|
||||
final users = await twonlyDB.contactsDao.getContactsByUsername(
|
||||
'[deleted]',
|
||||
username2: '[Unknown]',
|
||||
);
|
||||
for (final user in users) {
|
||||
final userData = await getUserById(user.userId);
|
||||
if (userData != null) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,15 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
|||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
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.
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
|
|
@ -50,6 +58,28 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<void> handleContactAccept(int fromUserId) async {
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
const ContactsCompanion(
|
||||
requested: Value(false),
|
||||
accepted: Value(true),
|
||||
deletedByUser: Value(false),
|
||||
),
|
||||
);
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
if (contact != null) {
|
||||
await twonlyDB.groupsDao.createNewDirectChat(
|
||||
fromUserId,
|
||||
GroupsCompanion(
|
||||
groupName: Value(getContactDisplayName(contact)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> handleContactRequest(
|
||||
int fromUserId,
|
||||
EncryptedContent_ContactRequest contactRequest,
|
||||
|
|
@ -60,25 +90,7 @@ Future<bool> handleContactRequest(
|
|||
return handleNewContactRequest(fromUserId);
|
||||
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
||||
Log.info('Got a contact accept from $fromUserId');
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
const ContactsCompanion(
|
||||
requested: Value(false),
|
||||
accepted: Value(true),
|
||||
deletedByUser: Value(false),
|
||||
),
|
||||
);
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
if (contact != null) {
|
||||
await twonlyDB.groupsDao.createNewDirectChat(
|
||||
fromUserId,
|
||||
GroupsCompanion(
|
||||
groupName: Value(getContactDisplayName(contact)),
|
||||
),
|
||||
);
|
||||
}
|
||||
await handleContactAccept(fromUserId);
|
||||
case EncryptedContent_ContactRequest_Type.REJECT:
|
||||
Log.info('Got a contact reject from $fromUserId');
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
|
|
@ -125,12 +137,12 @@ Future<void> handleContactUpdate(
|
|||
}
|
||||
|
||||
Future<void> handleFlameSync(
|
||||
int contactId,
|
||||
String groupId,
|
||||
EncryptedContent_FlameSync flameSync,
|
||||
) 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;
|
||||
|
||||
var updates = GroupsCompanion(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/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/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
|
|
@ -31,7 +32,7 @@ Future<void> handleMedia(
|
|||
message.senderId != fromUserId ||
|
||||
message.mediaId == null) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -53,8 +54,9 @@ Future<void> handleMedia(
|
|||
),
|
||||
);
|
||||
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
|
||||
if (mediaFile != null) {
|
||||
unawaited(startDownloadMedia(mediaFile, false));
|
||||
|
|
@ -89,56 +91,64 @@ Future<void> handleMedia(
|
|||
}
|
||||
}
|
||||
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
type: Value(mediaType),
|
||||
requiresAuthentication: Value(media.requiresAuthentication),
|
||||
displayLimitInMilliseconds: Value(
|
||||
displayLimitInMilliseconds,
|
||||
),
|
||||
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
|
||||
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
|
||||
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
|
||||
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
late MediaFile? mediaFile;
|
||||
late Message? message;
|
||||
|
||||
if (mediaFile == null) {
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
await twonlyDB.transaction(() async {
|
||||
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
type: Value(mediaType),
|
||||
requiresAuthentication: Value(media.requiresAuthentication),
|
||||
displayLimitInMilliseconds: Value(
|
||||
displayLimitInMilliseconds,
|
||||
),
|
||||
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
|
||||
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
|
||||
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
|
||||
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
|
||||
final message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
messageId: Value(media.senderMessageId),
|
||||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile.mediaId),
|
||||
type: Value(MessageType.media.name),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
media.hasAdditionalMessageData()
|
||||
? Uint8List.fromList(media.additionalMessageData)
|
||||
: null,
|
||||
if (mediaFile == null) {
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
|
||||
message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
messageId: Value(media.senderMessageId),
|
||||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile!.mediaId),
|
||||
type: Value(MessageType.media.name),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
media.hasAdditionalMessageData()
|
||||
? Uint8List.fromList(media.additionalMessageData)
|
||||
: null,
|
||||
),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
if (message != null) {
|
||||
await twonlyDB.groupsDao
|
||||
.increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp));
|
||||
Log.info('Inserted a new media message with ID: ${message.messageId}');
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(
|
||||
groupId,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
Log.info('Inserted a new media message with ID: ${message!.messageId}');
|
||||
await incFlameCounter(
|
||||
message.groupId,
|
||||
message!.groupId,
|
||||
true,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
|
||||
unawaited(startDownloadMedia(mediaFile, false));
|
||||
unawaited(startDownloadMedia(mediaFile!, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +173,9 @@ Future<void> handleMediaUpdate(
|
|||
);
|
||||
return;
|
||||
}
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'Got media file update, but media file was not found ${message.mediaId}',
|
||||
|
|
@ -203,8 +214,9 @@ Future<void> handleMediaUpdate(
|
|||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||
),
|
||||
);
|
||||
final mediaFileUpdated =
|
||||
await MediaFileService.fromMediaId(mediaFile.mediaId);
|
||||
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
if (mediaFileUpdated != null) {
|
||||
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
||||
mediaFileUpdated.uploadRequestPath.deleteSync();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:twonly/globals.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';
|
||||
|
||||
Future<void> handleReaction(
|
||||
|
|
@ -17,6 +18,8 @@ Future<void> handleReaction(
|
|||
reaction.remove,
|
||||
);
|
||||
|
||||
await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId);
|
||||
|
||||
if (!reaction.remove) {
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
|
||||
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
||||
// This is called when WebSocket is newly connected, so allow all downloads to be restarted.
|
||||
final mediaFiles =
|
||||
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload();
|
||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesPendingDownload();
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
if (await canMediaFileBeDownloaded(mediaFile)) {
|
||||
|
|
@ -30,8 +30,9 @@ Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
|||
}
|
||||
|
||||
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(mediaFile.mediaId);
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
|
||||
// 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.
|
||||
|
|
@ -56,8 +57,9 @@ Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
|
|||
return false;
|
||||
}
|
||||
|
||||
final contact =
|
||||
await twonlyDB.contactsDao.getContactById(messages.first.senderId!);
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
messages.first.senderId!,
|
||||
);
|
||||
|
||||
if (contact == null || contact.accountDeleted) {
|
||||
Log.info(
|
||||
|
|
@ -98,23 +100,27 @@ Future<bool> isAllowedToDownload(MediaType type) async {
|
|||
|
||||
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.video.name)) {
|
||||
if (options[ConnectivityResult.mobile.name]!.contains(
|
||||
DownloadMediaTypes.video.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.mobile.name]!
|
||||
.contains(DownloadMediaTypes.image.name)) {
|
||||
} else if (options[ConnectivityResult.mobile.name]!.contains(
|
||||
DownloadMediaTypes.image.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.wifi.name]!
|
||||
.contains(DownloadMediaTypes.video.name)) {
|
||||
if (options[ConnectivityResult.wifi.name]!.contains(
|
||||
DownloadMediaTypes.video.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.wifi.name]!
|
||||
.contains(DownloadMediaTypes.image.name)) {
|
||||
} else if (options[ConnectivityResult.wifi.name]!.contains(
|
||||
DownloadMediaTypes.image.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -230,8 +236,9 @@ Future<void> downloadFileFast(
|
|||
String apiUrl,
|
||||
File filePath,
|
||||
) async {
|
||||
final response =
|
||||
await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10));
|
||||
final response = await http
|
||||
.get(Uri.parse(apiUrl))
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
await filePath.writeAsBytes(response.bodyBytes);
|
||||
|
|
@ -308,8 +315,10 @@ Future<void> handleEncryptedFile(String mediaId) async {
|
|||
mac: Mac(mediaService.mediaFile.encryptionMac!),
|
||||
);
|
||||
|
||||
final plaintextBytes =
|
||||
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
|
||||
final plaintextBytes = await chacha20.decrypt(
|
||||
secretBox,
|
||||
secretKey: secretKeyData,
|
||||
);
|
||||
|
||||
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
|
||||
|
||||
|
|
@ -337,8 +346,8 @@ Future<void> handleEncryptedFile(String mediaId) async {
|
|||
}
|
||||
|
||||
Future<void> makeMigrationToVersion91() async {
|
||||
final messages =
|
||||
await twonlyDB.mediaFilesDao.getAllMediaFilesReuploadRequested();
|
||||
final messages = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesReuploadRequested();
|
||||
for (final message in messages) {
|
||||
await requestMediaReupload(message.mediaId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.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/services/api/mediafiles/download.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/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> initFileDownloader() async {
|
||||
|
|
@ -74,30 +74,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
if (update.status == TaskStatus.complete) {
|
||||
if (update.responseStatusCode == 200) {
|
||||
Log.info('Upload of ${media.mediaId} success!');
|
||||
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploaded),
|
||||
),
|
||||
);
|
||||
|
||||
/// As the messages where send in a bulk acknowledge all messages.
|
||||
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
|
||||
for (final message in messages) {
|
||||
final contacts =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId);
|
||||
for (final contact in contacts) {
|
||||
await twonlyDB.messagesDao.handleMessageAckByServer(
|
||||
contact.contactId,
|
||||
message.messageId,
|
||||
clock.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await markUploadAsSuccessful(media);
|
||||
return;
|
||||
}
|
||||
Log.error(
|
||||
|
|
@ -122,6 +99,20 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
'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 ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error(
|
||||
|
|
@ -129,8 +120,11 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
);
|
||||
final mediaService = MediaFileService(media);
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
// in case the media file is already uploaded to not reqtry
|
||||
if (mediaService.mediaFile.uploadState != UploadState.uploaded) {
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
||||
|
||||
Future<void> finishStartedPreprocessing() async {
|
||||
final mediaFiles =
|
||||
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload();
|
||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesPendingUpload();
|
||||
|
||||
Log.info('There are ${mediaFiles.length} media files pending');
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
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(
|
||||
MediaType type,
|
||||
int? displayLimitInMilliseconds, {
|
||||
|
|
@ -103,8 +150,9 @@ Future<void> insertMediaFileInMessagesTable(
|
|||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaService.mediaFile.mediaId),
|
||||
type: Value(MessageType.media.name),
|
||||
additionalMessageData:
|
||||
Value.absentIfNull(additionalData?.writeToBuffer()),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
additionalData?.writeToBuffer(),
|
||||
),
|
||||
),
|
||||
);
|
||||
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));
|
||||
|
||||
mediaService.encryptedPath
|
||||
.writeAsBytesSync(Uint8List.fromList(secretBox.cipherText));
|
||||
mediaService.encryptedPath.writeAsBytesSync(
|
||||
Uint8List.fromList(secretBox.cipherText),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createUploadRequest(MediaFileService media) async {
|
||||
|
|
@ -201,8 +250,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
|
||||
final messagesOnSuccess = <TextMessage>[];
|
||||
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId);
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||
media.mediaFile.mediaId,
|
||||
);
|
||||
|
||||
if (messages.isEmpty) {
|
||||
// There where no user selected who should receive the image, so waiting with this step...
|
||||
|
|
@ -210,8 +260,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
}
|
||||
|
||||
for (final message in messages) {
|
||||
final groupMembers =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId);
|
||||
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||
message.groupId,
|
||||
);
|
||||
|
||||
if (media.mediaFile.reuploadRequestedBy == null) {
|
||||
await incFlameCounter(message.groupId, false, message.createdAt);
|
||||
|
|
@ -220,8 +271,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
for (final groupMember in groupMembers) {
|
||||
/// only send the upload to the users
|
||||
if (media.mediaFile.reuploadRequestedBy != null) {
|
||||
if (!media.mediaFile.reuploadRequestedBy!
|
||||
.contains(groupMember.contactId)) {
|
||||
if (!media.mediaFile.reuploadRequestedBy!.contains(
|
||||
groupMember.contactId,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -260,8 +312,9 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
);
|
||||
|
||||
if (media.mediaFile.displayLimitInMilliseconds != null) {
|
||||
notEncryptedContent.media.displayLimitInMilliseconds =
|
||||
Int64(media.mediaFile.displayLimitInMilliseconds!);
|
||||
notEncryptedContent.media.displayLimitInMilliseconds = Int64(
|
||||
media.mediaFile.displayLimitInMilliseconds!,
|
||||
);
|
||||
}
|
||||
|
||||
final cipherText = await sendCipherText(
|
||||
|
|
@ -299,6 +352,19 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -306,8 +372,9 @@ Mutex protectUpload = Mutex();
|
|||
|
||||
Future<void> _uploadUploadRequest(MediaFileService media) async {
|
||||
await protectUpload.protect(() async {
|
||||
final currentMedia =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId);
|
||||
final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
media.mediaFile.mediaId,
|
||||
);
|
||||
|
||||
if (currentMedia == null ||
|
||||
currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) {
|
||||
|
|
@ -315,8 +382,9 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
|
|||
return null;
|
||||
}
|
||||
|
||||
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
||||
.read(key: SecureStorageKeys.apiAuthToken);
|
||||
final apiAuthTokenRaw = await const FlutterSecureStorage().read(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
|
||||
if (apiAuthTokenRaw == null) {
|
||||
Log.error('api auth token not defined.');
|
||||
|
|
@ -344,8 +412,9 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
|
|||
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
if (!connectivityResult.contains(ConnectivityResult.mobile) &&
|
||||
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
if (globalIsInBackgroundTask ||
|
||||
!connectivityResult.contains(ConnectivityResult.mobile) &&
|
||||
!connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
// no internet, directly put it into the background...
|
||||
await FileDownloader().enqueue(task);
|
||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
||||
|
|
@ -376,15 +445,30 @@ Future<void> uploadFileFastOrEnqueue(
|
|||
);
|
||||
|
||||
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}');
|
||||
final response =
|
||||
await requestMultipart.send().timeout(const Duration(seconds: 8));
|
||||
|
||||
final response = await requestMultipart.send();
|
||||
var status = TaskStatus.failed;
|
||||
if (response.statusCode == 200) {
|
||||
status = TaskStatus.complete;
|
||||
} else if (response.statusCode == 404) {
|
||||
status = TaskStatus.notFound;
|
||||
}
|
||||
|
||||
await Workmanager().cancelByUniqueName(workmanagerUniqueName);
|
||||
|
||||
await handleUploadStatusUpdate(
|
||||
TaskStatusUpdate(
|
||||
task,
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
// ignore: parameter_assignments
|
||||
receiptId = receipt.receiptId;
|
||||
|
||||
final contact =
|
||||
await twonlyDB.contactsDao.getContactById(receipt.contactId);
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
receipt.contactId,
|
||||
);
|
||||
if (contact == null || contact.accountDeleted) {
|
||||
Log.warn('Will not send message again as user does not exist anymore.');
|
||||
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
|
||||
|
|
@ -99,8 +100,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
final message = pb.Message.fromBuffer(receipt.message)
|
||||
..receiptId = receiptId;
|
||||
|
||||
final encryptedContent =
|
||||
pb.EncryptedContent.fromBuffer(message.encryptedContent);
|
||||
final encryptedContent = pb.EncryptedContent.fromBuffer(
|
||||
message.encryptedContent,
|
||||
);
|
||||
|
||||
final pushNotification = await getPushNotificationFromEncryptedContent(
|
||||
receipt.contactId,
|
||||
|
|
@ -111,8 +113,10 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
Uint8List? pushData;
|
||||
if (pushNotification != null && receipt.retryCount <= 3) {
|
||||
/// In case the message has to be resend more than three times, do not show a notification again...
|
||||
pushData =
|
||||
await encryptPushNotification(receipt.contactId, pushNotification);
|
||||
pushData = await encryptPushNotification(
|
||||
receipt.contactId,
|
||||
pushNotification,
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
|
||||
|
|
@ -331,7 +335,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
contactId: Value(contactId),
|
||||
message: Value(response.writeToBuffer()),
|
||||
messageId: Value(messageId),
|
||||
ackByServerAt: Value(onlyReturnEncryptedData ? clock.now() : null),
|
||||
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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/group.services.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/misc.dart';
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
..response = response;
|
||||
|
||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
globalGotMessageFromServer = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +96,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
|||
var retry = false;
|
||||
if (message.hasPlaintextContent()) {
|
||||
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(
|
||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
||||
);
|
||||
|
|
@ -251,11 +258,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasFlameSync()) {
|
||||
await handleFlameSync(fromUserId, content.flameSync);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasPushKeys()) {
|
||||
await handlePushKey(fromUserId, content.pushKeys);
|
||||
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()) {
|
||||
await handleGroupUpdate(
|
||||
fromUserId,
|
||||
|
|
|
|||
|
|
@ -81,22 +81,27 @@ Future<void> handleMediaError(MediaFile media) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> importSignalContactAndCreateRequest(
|
||||
Future<bool> importSignalContactAndCreateRequest(
|
||||
server.Response_UserData userdata,
|
||||
) async {
|
||||
if (await processSignalUserData(userdata)) {
|
||||
// 1. Setup notifications keys with the other user
|
||||
await setupNotificationWithUsers(
|
||||
forceContact: userdata.userId.toInt(),
|
||||
);
|
||||
// 2. Then send user request
|
||||
await sendCipherText(
|
||||
userdata.userId.toInt(),
|
||||
EncryptedContent(
|
||||
contactRequest: EncryptedContent_ContactRequest(
|
||||
type: EncryptedContent_ContactRequest_Type.REQUEST,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!await processSignalUserData(userdata)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Setup notifications keys with the other user
|
||||
await setupNotificationWithUsers(
|
||||
forceContact: userdata.userId.toInt(),
|
||||
);
|
||||
|
||||
// 2. Then send user request
|
||||
await sendCipherText(
|
||||
userdata.userId.toInt(),
|
||||
EncryptedContent(
|
||||
contactRequest: EncryptedContent_ContactRequest(
|
||||
type: EncryptedContent_ContactRequest_Type.REQUEST,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
130
lib/src/services/background/callback_dispatcher.background.dart
Normal file
130
lib/src/services/background/callback_dispatcher.background.dart
Normal 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}.');
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import 'package:hashlib/hashlib.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/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/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -10,13 +10,12 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.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/misc.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!');
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final baseDir = globalApplicationSupportDirectory;
|
||||
|
||||
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
|
||||
await backupDir.create(recursive: true);
|
||||
|
||||
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
|
||||
|
||||
final backupDatabaseFileCleaned =
|
||||
File(join(backupDir.path, 'twonly.backup.cleaned.sqlite'));
|
||||
final backupDatabaseFileCleaned = File(
|
||||
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
|
||||
);
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
|
|
@ -70,8 +70,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
await backupDB.deleteDataForTwonlySafe();
|
||||
|
||||
await backupDB
|
||||
.customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]);
|
||||
await backupDB.customStatement('VACUUM INTO ?', [
|
||||
backupDatabaseFileCleaned.path,
|
||||
]);
|
||||
|
||||
await backupDB.printTableSizes();
|
||||
|
||||
|
|
@ -80,10 +81,11 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
// ignore: inference_failure_on_collection_literal
|
||||
final secureStorageBackup = {};
|
||||
const storage = FlutterSecureStorage();
|
||||
secureStorageBackup[SecureStorageKeys.signalIdentity] =
|
||||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
|
||||
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage
|
||||
.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
|
||||
final userBackup = await getUser();
|
||||
if (userBackup == null) return;
|
||||
|
|
@ -117,13 +119,15 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
||||
|
||||
if (gUser.twonlySafeBackup!.lastBackupDone == null ||
|
||||
gUser.twonlySafeBackup!.lastBackupDone!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 90)))) {
|
||||
gUser.twonlySafeBackup!.lastBackupDone!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 90)),
|
||||
)) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
final lastHash =
|
||||
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
|
||||
final lastHash = await storage.read(
|
||||
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
||||
);
|
||||
|
||||
if (lastHash != null && !force) {
|
||||
if (backupHash == lastHash) {
|
||||
|
|
@ -155,8 +159,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
Log.info('Backup files created.');
|
||||
|
||||
final encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, 'twonly_safe.backup'));
|
||||
final encryptedBackupBytesFile = File(
|
||||
join(backupDir.path, 'twonly_safe.backup'),
|
||||
);
|
||||
|
||||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
|
|
@ -8,23 +8,25 @@ import 'package:drift/drift.dart';
|
|||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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/model/json/userdata.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/storage.dart';
|
||||
|
||||
Future<void> recoverTwonlySafe(
|
||||
Future<void> recoverBackup(
|
||||
String username,
|
||||
String password,
|
||||
BackupServer? server,
|
||||
) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, username);
|
||||
|
||||
final backupServerUrl =
|
||||
await getTwonlySafeBackupUrlFromServer(backupId, server);
|
||||
final backupServerUrl = await getTwonlySafeBackupUrlFromServer(
|
||||
backupId,
|
||||
server,
|
||||
);
|
||||
|
||||
if (backupServerUrl == null) {
|
||||
Log.error('Could not create backup url');
|
||||
|
|
@ -87,8 +89,9 @@ Future<void> handleBackupData(
|
|||
plaintextBytes,
|
||||
);
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
final originalDatabase = File(
|
||||
join(globalApplicationSupportDirectory, 'twonly.sqlite'),
|
||||
);
|
||||
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
|
|
@ -10,7 +10,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||
final groups = await twonlyDB.groupsDao.getAllDirectChats();
|
||||
final groups = await twonlyDB.groupsDao.getAllGroups();
|
||||
if (groups.isEmpty) return;
|
||||
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
|
||||
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
|
||||
if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue;
|
||||
|
||||
final groupMembers =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
|
||||
if (groupMembers.length != 1) {
|
||||
continue; // flame sync is only done for groups of two
|
||||
}
|
||||
|
||||
await sendCipherText(
|
||||
groupMembers.first.contactId,
|
||||
await sendCipherTextToGroup(
|
||||
group.groupId,
|
||||
EncryptedContent(
|
||||
flameSync: EncryptedContent_FlameSync(
|
||||
flameCounter: Int64(flameCounter),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:pro_video_editor/pro_video_editor.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/twonly.db.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
Future<void> compressImage(
|
||||
File sourceFile,
|
||||
|
|
@ -16,8 +17,6 @@ Future<void> compressImage(
|
|||
) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// // ffmpeg -i input.png -vcodec libwebp -lossless 1 -preset default output.webp
|
||||
|
||||
try {
|
||||
var compressedBytes = await FlutterImageCompress.compressWithFile(
|
||||
sourceFile.path,
|
||||
|
|
@ -74,37 +73,53 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
|
|||
try {
|
||||
final task = VideoRenderData(
|
||||
video: EditorVideo.file(media.originalPath),
|
||||
// qualityPreset: VideoQualityPreset.p720High,
|
||||
imageBytes: media.overlayImagePath.readAsBytesSync(),
|
||||
enableAudio: !media.removeAudio,
|
||||
);
|
||||
|
||||
final result = await ProVideoEditor.instance.renderVideo(task);
|
||||
media.ffmpegOutputPath.writeAsBytesSync(result);
|
||||
await ProVideoEditor.instance
|
||||
.renderVideoToFile(media.ffmpegOutputPath.path, task);
|
||||
|
||||
MediaInfo? mediaInfo;
|
||||
try {
|
||||
mediaInfo = await VideoCompress.compressVideo(
|
||||
media.ffmpegOutputPath.path,
|
||||
quality: VideoQuality.Res640x480Quality,
|
||||
includeAudio: true,
|
||||
);
|
||||
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
|
||||
} catch (e) {
|
||||
Log.error('during video compression: $e');
|
||||
}
|
||||
if (Platform.isIOS ||
|
||||
media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
|
||||
!kReleaseMode) {
|
||||
String? compressedPath;
|
||||
try {
|
||||
compressedPath = await VideoCompressionChannel.compressVideo(
|
||||
inputPath: media.ffmpegOutputPath.path,
|
||||
outputPath: media.tempPath.path,
|
||||
onProgress: (progress) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
preProgressingProcess: Value((progress * 100).toInt()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('during video compression: $e');
|
||||
}
|
||||
|
||||
if (mediaInfo == null) {
|
||||
Log.error('Could not compress video using original video.');
|
||||
// as a fall back use the non compressed version
|
||||
media.ffmpegOutputPath.copySync(media.tempPath.path);
|
||||
if (compressedPath == null) {
|
||||
Log.error('Could not compress video using original video.');
|
||||
// as a fall back use the non compressed version
|
||||
media.ffmpegOutputPath.copySync(media.tempPath.path);
|
||||
}
|
||||
} 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();
|
||||
|
||||
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
|
||||
.toStringAsFixed(2);
|
||||
final sizeTo =
|
||||
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2);
|
||||
|
||||
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) {
|
||||
Log.error(e);
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:twonly/globals.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/utils/log.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
|
||||
|
||||
|
|
@ -111,8 +112,15 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|||
initLogger();
|
||||
// Log.info('Handling a background message: ${message.messageId}');
|
||||
await handleRemoteMessage(message);
|
||||
// make sure every thing run...
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (await initBackgroundExecution()) {
|
||||
await handlePeriodicTask();
|
||||
}
|
||||
} else {
|
||||
// make sure every thing run...
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class KeyValueStore {
|
||||
static Future<File> _getFilePath(String key) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return File('${directory.path}/keyvalue/$key.json');
|
||||
return File('$globalApplicationSupportDirectory/keyvalue/$key.json');
|
||||
}
|
||||
|
||||
static Future<void> delete(String key) async {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/foundation.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:twonly/globals.dart';
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ void initLogger() {
|
|||
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) async {
|
||||
await _writeLogToFile(record);
|
||||
unawaited(_writeLogToFile(record));
|
||||
if (!kReleaseMode) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
|
|
@ -66,77 +67,123 @@ class Log {
|
|||
}
|
||||
|
||||
Future<String> loadLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
return logFile.readAsString();
|
||||
} else {
|
||||
return 'Log file does not exist.';
|
||||
}
|
||||
if (logFile.existsSync()) {
|
||||
return logFile.readAsString();
|
||||
} else {
|
||||
return 'Log file does not exist.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> readLast1000Lines() async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final file = File('${dir.path}/app.log');
|
||||
if (!file.existsSync()) return '';
|
||||
final all = await file.readAsLines();
|
||||
final start = all.length > 1000 ? all.length - 1000 : 0;
|
||||
return all.sublist(start).join('\n');
|
||||
return _protectFileAccess(() async {
|
||||
final file = File('$globalApplicationSupportDirectory/app.log');
|
||||
if (!file.existsSync()) return '';
|
||||
final all = await file.readAsLines();
|
||||
final start = all.length > 1000 ? all.length - 1000 : 0;
|
||||
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 {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
final logMessage =
|
||||
'${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
|
||||
|
||||
final raf = await logFile.open(mode: FileMode.writeOnlyAppend);
|
||||
|
||||
try {
|
||||
// Use FileLock.blockingExclusive to wait until the lock is available
|
||||
await raf.lock(FileLock.blockingExclusive);
|
||||
await raf.writeString(logMessage);
|
||||
await raf.flush();
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('Error during file access: $e');
|
||||
} finally {
|
||||
await raf.unlock();
|
||||
await raf.close();
|
||||
}
|
||||
return _protectFileAccess(() async {
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
final raf = await logFile.open(mode: FileMode.writeOnlyAppend);
|
||||
try {
|
||||
await raf.writeString(logMessage);
|
||||
await raf.flush();
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('Error during file access: $e');
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cleanLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
final lines = await logFile.readAsLines();
|
||||
if (logFile.existsSync()) {
|
||||
final lines = await logFile.readAsLines();
|
||||
|
||||
if (lines.length <= 10000) return;
|
||||
if (lines.length <= 10000) return;
|
||||
|
||||
final removeCount = lines.length - 10000;
|
||||
final remaining = lines.sublist(removeCount, lines.length);
|
||||
final removeCount = lines.length - 10000;
|
||||
final remaining = lines.sublist(removeCount, lines.length);
|
||||
|
||||
final sink = logFile.openWrite()..writeAll(remaining, '\n');
|
||||
await sink.close();
|
||||
}
|
||||
final sink = logFile.openWrite()..writeAll(remaining, '\n');
|
||||
await sink.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> deleteLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
await logFile.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (logFile.existsSync()) {
|
||||
await logFile.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
String _getCallerSourceCodeFilename() {
|
||||
|
|
@ -152,8 +199,11 @@ String _getCallerSourceCodeFilename() {
|
|||
lineNumber = parts.last.split(':')[1]; // Extract the line number
|
||||
} else {
|
||||
final firstLine = stackTraceString.split('\n')[0];
|
||||
fileName =
|
||||
firstLine.split('/').last.split(':').first; // Extract the file name
|
||||
fileName = firstLine
|
||||
.split('/')
|
||||
.last
|
||||
.split(':')
|
||||
.first; // Extract the file name
|
||||
lineNumber = firstLine.split(':')[1]; // Extract the line number
|
||||
}
|
||||
lineNumber = lineNumber.replaceAll(')', '');
|
||||
|
|
|
|||
|
|
@ -302,7 +302,9 @@ Color getMessageColorFromType(
|
|||
) {
|
||||
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;
|
||||
} else if (mediaFile != null) {
|
||||
if (mediaFile.requiresAuthentication) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ PublicProfile? parseQrCodeData(Uint8List rawBytes) {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<void> addNewContactFromPublicProfile(PublicProfile profile) async {
|
||||
Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
|
||||
final userdata = Response_UserData(
|
||||
userId: profile.userId,
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ Future<bool> isUserCreated() async {
|
|||
}
|
||||
|
||||
Future<UserData?> getUser() async {
|
||||
final userJson =
|
||||
await const FlutterSecureStorage().read(key: SecureStorageKeys.userData);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final userJson = await const FlutterSecureStorage()
|
||||
.read(key: SecureStorageKeys.userData);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
|
||||
final user = UserData.fromJson(userMap);
|
||||
return user;
|
||||
|
|
|
|||
|
|
@ -849,7 +849,21 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
onTap: () async {
|
||||
c.isLoading = true;
|
||||
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(
|
||||
padding: const EdgeInsets.all(12),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ enum FaceFilterType {
|
|||
none,
|
||||
dogBrown,
|
||||
beardUpperLipGreen,
|
||||
beardUpperLip,
|
||||
}
|
||||
|
||||
extension FaceFilterTypeExtension on FaceFilterType {
|
||||
|
|
@ -27,8 +26,6 @@ extension FaceFilterTypeExtension on FaceFilterType {
|
|||
return Container();
|
||||
case FaceFilterType.dogBrown:
|
||||
return DogFilterPainter.getPreview();
|
||||
case FaceFilterType.beardUpperLip:
|
||||
return BeardFilterPainter.getPreview(this);
|
||||
case FaceFilterType.beardUpperLipGreen:
|
||||
return BeardFilterPainter.getPreview(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
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';
|
||||
|
||||
class ScannedVerifiedContact {
|
||||
ScannedVerifiedContact({
|
||||
required this.contact,
|
||||
required this.verificationOk,
|
||||
});
|
||||
ScannedVerifiedContact({required this.contact, required this.verificationOk});
|
||||
Contact contact;
|
||||
bool verificationOk;
|
||||
}
|
||||
|
||||
class ScannedNewProfile {
|
||||
ScannedNewProfile({
|
||||
required this.profile,
|
||||
this.isLoading = false,
|
||||
});
|
||||
ScannedNewProfile({required this.profile, this.isLoading = false});
|
||||
PublicProfile profile;
|
||||
bool isLoading;
|
||||
}
|
||||
|
|
@ -53,14 +48,15 @@ class MainCameraController {
|
|||
String? scannedUrl;
|
||||
GlobalKey zoomButtonKey = GlobalKey();
|
||||
GlobalKey cameraPreviewKey = GlobalKey();
|
||||
bool isSelectingFaceFilters = false;
|
||||
|
||||
bool isSelectingFaceFilters = false;
|
||||
bool isSharePreviewIsShown = false;
|
||||
bool isVideoRecording = false;
|
||||
DateTime? timeSharedLinkWasSetWithQr;
|
||||
|
||||
Uri? sharedLinkForPreview;
|
||||
|
||||
void setSharedLinkForPreview(Uri url) {
|
||||
void setSharedLinkForPreview(Uri? url) {
|
||||
sharedLinkForPreview = url;
|
||||
setState();
|
||||
}
|
||||
|
|
@ -92,9 +88,8 @@ class MainCameraController {
|
|||
scannedUrl = null;
|
||||
try {
|
||||
await cameraController?.stopImageStream();
|
||||
} catch (e) {
|
||||
Log.warn(e);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
final cameraControllerTemp = cameraController;
|
||||
cameraController = null;
|
||||
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
|
||||
|
|
@ -159,8 +154,9 @@ class MainCameraController {
|
|||
}
|
||||
}
|
||||
|
||||
await cameraController
|
||||
?.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
||||
await cameraController?.lockCaptureOrientation(
|
||||
DeviceOrientation.portraitUp,
|
||||
);
|
||||
await cameraController?.setFlashMode(
|
||||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
||||
);
|
||||
|
|
@ -169,7 +165,8 @@ class MainCameraController {
|
|||
selectedCameraDetails.minAvailableZoom =
|
||||
await cameraController?.getMinZoomLevel() ?? 1;
|
||||
selectedCameraDetails
|
||||
..isZoomAble = selectedCameraDetails.maxAvailableZoom !=
|
||||
..isZoomAble =
|
||||
selectedCameraDetails.maxAvailableZoom !=
|
||||
selectedCameraDetails.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = cameraId;
|
||||
|
|
@ -325,11 +322,23 @@ class MainCameraController {
|
|||
);
|
||||
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) {
|
||||
if (barcode.displayValue != null) {
|
||||
if (barcode.displayValue!.startsWith('http://') ||
|
||||
barcode.displayValue!.startsWith('https://')) {
|
||||
scannedUrl = barcode.displayValue;
|
||||
if (sharedLinkForPreview == null) {
|
||||
timeSharedLinkWasSetWithQr = clock.now();
|
||||
setSharedLinkForPreview(Uri.parse(scannedUrl!));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (barcode.rawBytes == null) continue;
|
||||
|
|
@ -338,16 +347,19 @@ class MainCameraController {
|
|||
|
||||
if (profile == null) continue;
|
||||
|
||||
final contact =
|
||||
await twonlyDB.contactsDao.getContactById(profile.userId.toInt());
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
profile.userId.toInt(),
|
||||
);
|
||||
|
||||
if (contact != null && contact.accepted) {
|
||||
if (contactsVerified[contact.userId] == null) {
|
||||
final storedPublicKey =
|
||||
await getPublicKeyFromContact(contact.userId);
|
||||
final storedPublicKey = await getPublicKeyFromContact(
|
||||
contact.userId,
|
||||
);
|
||||
if (storedPublicKey != null) {
|
||||
final verificationOk =
|
||||
profile.publicIdentityKey.equals(storedPublicKey.toList());
|
||||
final verificationOk = profile.publicIdentityKey.equals(
|
||||
storedPublicKey.toList(),
|
||||
);
|
||||
contactsVerified[contact.userId] = ScannedVerifiedContact(
|
||||
contact: contact,
|
||||
verificationOk: verificationOk,
|
||||
|
|
@ -365,8 +377,8 @@ class MainCameraController {
|
|||
content: Text(
|
||||
globalRootScaffoldMessengerKey.currentContext?.lang
|
||||
.verifiedPublicKey(
|
||||
getContactDisplayName(contact),
|
||||
) ??
|
||||
getContactDisplayName(contact),
|
||||
) ??
|
||||
'',
|
||||
),
|
||||
duration: const Duration(seconds: 6),
|
||||
|
|
@ -408,7 +420,6 @@ class MainCameraController {
|
|||
inputImage.metadata!.rotation,
|
||||
cameraController!.description.lensDirection,
|
||||
);
|
||||
case FaceFilterType.beardUpperLip:
|
||||
case FaceFilterType.beardUpperLipGreen:
|
||||
painter = BeardFilterPainter(
|
||||
_currentFilterType,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ class BeardFilterPainter extends FaceFilterPainter {
|
|||
|
||||
static String getAssetPath(FaceFilterType beardType) {
|
||||
switch (beardType) {
|
||||
case FaceFilterType.beardUpperLip:
|
||||
return 'assets/filters/beard_upper_lip.webp';
|
||||
case FaceFilterType.beardUpperLipGreen:
|
||||
return 'assets/filters/beard_upper_lip_green.webp';
|
||||
case FaceFilterType.dogBrown:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class FilterSkeleton extends StatelessWidget {
|
|||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: Container()),
|
||||
if (child != null) child!,
|
||||
?child,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -89,10 +89,11 @@ class _FilterLayerState extends State<FilterLayer> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final stickers = (await getStickerIndex())
|
||||
.where((x) => x.imageSrc.contains('/imagefilter/'))
|
||||
.toList()
|
||||
..sortBy((x) => x.imageSrc);
|
||||
final stickers =
|
||||
(await getStickerIndex())
|
||||
.where((x) => x.imageSrc.contains('/imagefilter/'))
|
||||
.toList()
|
||||
..sortBy((x) => x.imageSrc);
|
||||
|
||||
for (final sticker in stickers) {
|
||||
pages.insert(pages.length - 1, ImageFilter(imagePath: sticker.imageSrc));
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
actions: [
|
||||
const FeedbackIconButton(),
|
||||
StreamBuilder(
|
||||
stream: twonlyDB.contactsDao.watchContactsRequested(),
|
||||
stream: twonlyDB.contactsDao.watchContactsRequestedCount(),
|
||||
builder: (context, snapshot) {
|
||||
var count = 0;
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,15 +26,17 @@ class _ShareAdditionalViewState extends State<ShareAdditionalView> {
|
|||
}
|
||||
|
||||
Future<void> openShareContactView() async {
|
||||
final selectedContacts = await context.navPush(
|
||||
SelectContactsView(
|
||||
text: SelectedContactViewText(
|
||||
title: context.lang.shareContactsTitle,
|
||||
submitButton: (_, __) => context.lang.shareContactsSubmit,
|
||||
submitIcon: FontAwesomeIcons.shareNodes,
|
||||
),
|
||||
),
|
||||
) as List<int>?;
|
||||
final selectedContacts =
|
||||
await context.navPush(
|
||||
SelectContactsView(
|
||||
text: SelectedContactViewText(
|
||||
title: context.lang.shareContactsTitle,
|
||||
submitButton: (_, _) => context.lang.shareContactsSubmit,
|
||||
submitIcon: FontAwesomeIcons.shareNodes,
|
||||
),
|
||||
),
|
||||
)
|
||||
as List<int>?;
|
||||
if (selectedContacts != null && selectedContacts.isNotEmpty) {
|
||||
await insertAndSendContactShareMessage(
|
||||
widget.group.groupId,
|
||||
|
|
|
|||
|
|
@ -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/model/memory_item.model.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/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_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_text_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) {
|
||||
mediaService = MediaFileService(mediaFiles);
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -101,8 +101,9 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
|
||||
final mediaFile = message.mediaId == null
|
||||
? null
|
||||
: widget.mediaFiles
|
||||
.firstWhereOrNull((t) => t.mediaId == message.mediaId);
|
||||
: widget.mediaFiles.firstWhereOrNull(
|
||||
(t) => t.mediaId == message.mediaId,
|
||||
);
|
||||
|
||||
final color = getMessageColorFromType(message, mediaFile, context);
|
||||
|
||||
|
|
@ -144,8 +145,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
}
|
||||
}
|
||||
case MessageSendState.send:
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
|
||||
icon = FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane,
|
||||
size: 12,
|
||||
color: color,
|
||||
);
|
||||
text = context.lang.messageSendState_Send;
|
||||
case MessageSendState.sending:
|
||||
icon = getLoaderIcon(color);
|
||||
|
|
@ -163,13 +167,20 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
context.lang.uploadLimitReached,
|
||||
style: const TextStyle(fontSize: 9),
|
||||
);
|
||||
|
||||
onTap = () => context.push(Routes.settingsSubscription);
|
||||
}
|
||||
if (mediaFile.uploadState == UploadState.preprocessing ||
|
||||
mediaFile.uploadState == UploadState.initialized) {
|
||||
|
||||
if (mediaFile.uploadState == UploadState.initialized) {
|
||||
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;
|
||||
|
|
@ -191,13 +202,28 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
}
|
||||
|
||||
if (mediaFile.downloadState == DownloadState.reuploadRequested) {
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
||||
icon = FaIcon(
|
||||
FontAwesomeIcons.clockRotateLeft,
|
||||
size: 12,
|
||||
color: color,
|
||||
);
|
||||
textWidget = Text(
|
||||
context.lang.retransmissionRequested,
|
||||
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) {
|
||||
|
|
@ -220,10 +246,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
if (!widget.messages.any((t) => t.openedAt == null)) {
|
||||
if (widget.lastReaction != null) {
|
||||
/// No messages are still open, so check if the reaction is the last message received.
|
||||
if (!widget.messages
|
||||
.any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) {
|
||||
if (EmojiAnimation.animatedIcons
|
||||
.containsKey(widget.lastReaction!.emoji)) {
|
||||
if (!widget.messages.any(
|
||||
(m) => m.createdAt.isAfter(widget.lastReaction!.createdAt),
|
||||
)) {
|
||||
if (EmojiAnimation.animatedIcons.containsKey(
|
||||
widget.lastReaction!.emoji,
|
||||
)) {
|
||||
icons = [
|
||||
SizedBox(
|
||||
height: 18,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ class EmojiAnimation extends StatelessWidget {
|
|||
'💯': '100.lottie',
|
||||
'🎉': 'party-popper.lottie',
|
||||
'🎊': 'confetti-ball.lottie',
|
||||
'🎂': 'birthday-cake.json',
|
||||
'🧡': 'orange-heart.lottie',
|
||||
'💛': 'yellow-heart.lottie',
|
||||
'💚': 'green-heart.lottie',
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// ignore: strict_top_level_inference
|
||||
Widget errorBuilder(_, __, ___) {
|
||||
Widget errorBuilder(_, _, _) {
|
||||
return const SvgPicture(
|
||||
AssetBytesLoader('assets/images/default_avatar.svg.vec'),
|
||||
);
|
||||
|
|
@ -81,20 +80,20 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
groupStream = twonlyDB.groupsDao
|
||||
.watchGroupContact(widget.group!.groupId)
|
||||
.listen((contacts) {
|
||||
_avatarContacts = [];
|
||||
if (contacts.length == 1) {
|
||||
if (contacts.first.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contacts.first);
|
||||
}
|
||||
} else {
|
||||
for (final contact in contacts) {
|
||||
if (contact.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contact);
|
||||
_avatarContacts = [];
|
||||
if (contacts.length == 1) {
|
||||
if (contacts.first.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contacts.first);
|
||||
}
|
||||
} else {
|
||||
for (final contact in contacts) {
|
||||
if (contact.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
setState(() {});
|
||||
});
|
||||
} else if (widget.myAvatar) {
|
||||
_globalUserDataCallBackId = 'avatar_${getRandomString(10)}';
|
||||
globalUserDataChangedCallBack[_globalUserDataCallBackId!] = () {
|
||||
|
|
@ -113,11 +112,11 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
contactStream = twonlyDB.contactsDao
|
||||
.watchContact(widget.contactId!)
|
||||
.listen((contact) {
|
||||
if (contact != null && contact.avatarSvgCompressed != null) {
|
||||
_avatarContacts = [contact];
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
if (contact != null && contact.avatarSvgCompressed != null) {
|
||||
_avatarContacts = [contact];
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,11 +44,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
|
||||
groupId = group?.groupId;
|
||||
} else if (groupId != null) {
|
||||
// do not display the flame counter for groups
|
||||
group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (!(group?.isDirectChat ?? false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (groupId != null && group != null) {
|
||||
isBestFriend =
|
||||
|
|
@ -67,19 +63,30 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (flameCounter < 1) return Container();
|
||||
|
||||
var flameEmoji = '🔥';
|
||||
|
||||
if (isBestFriend) flameEmoji = '❤️🔥';
|
||||
if (flameCounter == 100) flameEmoji = '💯';
|
||||
|
||||
if (flameCounter >= 365 && flameCounter % 365 == 0) {
|
||||
flameEmoji = '🎂';
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.prefix) const SizedBox(width: 5),
|
||||
if (widget.prefix) const Text('•'),
|
||||
if (widget.prefix) const SizedBox(width: 5),
|
||||
Text(
|
||||
flameCounter.toString(),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
if (flameCounter != 100)
|
||||
Text(
|
||||
flameCounter.toString(),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
child: EmojiAnimation(
|
||||
emoji: isBestFriend ? '❤️🔥' : '🔥',
|
||||
emoji: flameEmoji,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class _ThreeRotatingDotsState extends State<ThreeRotatingDots>
|
|||
height: size,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (_, __) => Transform.translate(
|
||||
builder: (_, _) => Transform.translate(
|
||||
offset: Offset(0, size / 12),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
|
|
@ -110,7 +110,6 @@ class _ThreeRotatingDotsState extends State<ThreeRotatingDots>
|
|||
),
|
||||
|
||||
/// Next 3 dots
|
||||
|
||||
_BuildDot.second(
|
||||
controller: _animationController,
|
||||
beginAngle: 0,
|
||||
|
|
@ -217,9 +216,9 @@ class DrawDot extends StatelessWidget {
|
|||
required double dotSize,
|
||||
required this.color,
|
||||
super.key,
|
||||
}) : width = dotSize,
|
||||
height = dotSize,
|
||||
circular = true;
|
||||
}) : width = dotSize,
|
||||
height = dotSize,
|
||||
circular = true;
|
||||
|
||||
const DrawDot.elliptical({
|
||||
required this.width,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import 'dart:async';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.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/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/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -46,7 +53,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
|||
}
|
||||
|
||||
Future<void> _restoreFlames() async {
|
||||
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) {
|
||||
if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames) &&
|
||||
kReleaseMode) {
|
||||
await context.push(Routes.settingsSubscription);
|
||||
return;
|
||||
}
|
||||
|
|
@ -60,7 +68,40 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
|
|||
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 sendCipherTextToGroup(
|
||||
_groupId,
|
||||
encryptedContent,
|
||||
messageId: message.messageId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class ContactView extends StatefulWidget {
|
|||
|
||||
class _ContactViewState extends State<ContactView> {
|
||||
Contact? _contact;
|
||||
bool _contactIsStillAGroupMember = true;
|
||||
List<GroupMember> _memberOfGroups = [];
|
||||
|
||||
late StreamSubscription<Contact?> _contactSub;
|
||||
late StreamSubscription<List<GroupMember>> _groupMemberSub;
|
||||
|
|
@ -44,10 +44,8 @@ class _ContactViewState extends State<ContactView> {
|
|||
});
|
||||
_groupMemberSub = twonlyDB.groupsDao
|
||||
.watchContactGroupMember(widget.userId)
|
||||
.listen((update) {
|
||||
setState(() {
|
||||
_contactIsStillAGroupMember = update.isNotEmpty;
|
||||
});
|
||||
.listen((groups) async {
|
||||
_memberOfGroups = groups;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
|
@ -60,7 +58,18 @@ class _ContactViewState extends State<ContactView> {
|
|||
}
|
||||
|
||||
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(
|
||||
SnackBar(
|
||||
content: Text(context.lang.deleteUserErrorMessage),
|
||||
|
|
@ -211,26 +220,6 @@ class _ContactViewState extends State<ContactView> {
|
|||
setState(() {});
|
||||
},
|
||||
),
|
||||
// BetterListTile(
|
||||
// icon: FontAwesomeIcons.eraser,
|
||||
// iconSize: 16,
|
||||
// text: context.lang.deleteAllContactMessages,
|
||||
// onTap: () async {
|
||||
// final block = await showAlertDialog(
|
||||
// context,
|
||||
// context.lang.deleteAllContactMessages,
|
||||
// context.lang.deleteAllContactMessagesBody(
|
||||
// getContactDisplayName(contact),
|
||||
// ),
|
||||
// );
|
||||
// if (block) {
|
||||
// if (context.mounted) {
|
||||
// await twonlyDB.messagesDao
|
||||
// .deleteMessagesByContactId(contact.userId);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.flag,
|
||||
text: context.lang.reportUser,
|
||||
|
|
|
|||
|
|
@ -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/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||
|
|
@ -181,6 +182,10 @@ class _GroupViewState extends State<GroupView> {
|
|||
substringBy(_group!.groupName, 25),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
FlameCounterWidget(
|
||||
groupId: _group?.groupId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
|
|
|
|||
|
|
@ -93,7 +93,9 @@ class GroupMemberContextMenu extends StatelessWidget {
|
|||
await twonlyDB.contactsDao.updateContact(
|
||||
member.contactId,
|
||||
const ContactsCompanion(
|
||||
requested: Value(true),
|
||||
accepted: Value(false),
|
||||
requested: Value(false),
|
||||
deletedByUser: Value(false),
|
||||
),
|
||||
);
|
||||
await sendCipherText(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.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/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
|
|
@ -29,7 +29,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
});
|
||||
|
||||
try {
|
||||
await recoverTwonlySafe(
|
||||
await recoverBackup(
|
||||
usernameCtrl.text,
|
||||
passwordCtrl.text,
|
||||
backupServer,
|
||||
|
|
@ -38,6 +38,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
await Restart.restartApp(
|
||||
notificationTitle: 'Backup successfully recovered.',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
forceKill: true,
|
||||
);
|
||||
} catch (e) {
|
||||
// in case something was already written from the backup...
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ class _AccountViewState extends State<AccountView> {
|
|||
await Restart.restartApp(
|
||||
notificationTitle: 'Account successfully deleted',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
forceKill: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.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';
|
||||
|
||||
void Function() gUpdateBackupView = () {};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/services.dart' show rootBundle;
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.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/views/components/alert_dialog.dart';
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue