mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
working native push notification
This commit is contained in:
parent
6a711431aa
commit
f534089dcf
329 changed files with 8615 additions and 2216 deletions
|
|
@ -73,6 +73,9 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
implementation platform('com.google.firebase:firebase-bom:34.9.0')
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
implementation 'androidx.work:work-runtime:2.10.2'
|
||||
implementation 'com.otaliastudios:transcoder:0.11.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,25 @@
|
|||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||
tools:node="remove">
|
||||
</service>
|
||||
<service
|
||||
android:name=".notifications.TwonlyFirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!-- Twonly owns background FCM delivery natively. Keep the Flutter
|
||||
plugin for foreground permission/token APIs, but never allow it
|
||||
to launch a Dart background isolate. -->
|
||||
<service
|
||||
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
|
||||
tools:node="remove" />
|
||||
<service
|
||||
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingBackgroundService"
|
||||
tools:node="remove" />
|
||||
<receiver
|
||||
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingReceiver"
|
||||
tools:node="remove" />
|
||||
<meta-data
|
||||
android:name="eu.twonly.service.TWONLY_LOGO"
|
||||
android:resource="@drawable/ic_launcher_foreground" />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
|||
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.crates.keyring.Keyring
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import android.os.Bundle
|
||||
|
|
@ -16,6 +17,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import eu.twonly.notifications.NotificationTapChannel
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
private val CHANNEL = "eu.twonly/photo_picker"
|
||||
|
|
@ -25,6 +27,10 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
// Buffer a notification tap before the Flutter engine exists so the
|
||||
// cold-start route is not lost.
|
||||
NotificationTapChannel.handleIntent(intent)
|
||||
|
||||
pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
|
||||
if (uris.isNotEmpty()) {
|
||||
|
|
@ -39,6 +45,12 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
NotificationTapChannel.handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
||||
eventSink!!.success(true)
|
||||
|
|
@ -58,6 +70,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||
|
||||
NotificationTapChannel.configure(flutterEngine, applicationContext)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"pickImages" -> {
|
||||
|
|
@ -88,4 +102,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
NotificationTapChannel.detach()
|
||||
super.cleanUpFlutterEngine(flutterEngine)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package eu.twonly.notifications
|
||||
|
||||
internal object NativeNotificationBridge {
|
||||
init {
|
||||
System.loadLibrary("rust_lib_twonly")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun process(
|
||||
databaseDirectory: String,
|
||||
dataDirectory: String,
|
||||
locale: String,
|
||||
deadlineMs: Long,
|
||||
): String
|
||||
|
||||
@JvmStatic
|
||||
external fun acknowledge(eventIdsJson: String): String
|
||||
|
||||
@JvmStatic
|
||||
external fun storeFcmToken(
|
||||
databaseDirectory: String,
|
||||
dataDirectory: String,
|
||||
token: String,
|
||||
): String
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package eu.twonly.notifications
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
internal data class NativeNotificationPresentation(
|
||||
val title: String,
|
||||
val body: String,
|
||||
)
|
||||
|
||||
internal data class NativeNotificationAddition(
|
||||
val eventId: String,
|
||||
val notificationId: String,
|
||||
val conversationId: String?,
|
||||
val senderId: Long,
|
||||
val senderName: String,
|
||||
val title: String,
|
||||
val body: String,
|
||||
val createdAt: Long,
|
||||
val avatarPath: String?,
|
||||
)
|
||||
|
||||
internal data class NativeNotificationBatch(
|
||||
val additions: List<NativeNotificationAddition>,
|
||||
val removals: List<String>,
|
||||
val badgeCount: Long,
|
||||
val completed: Boolean,
|
||||
)
|
||||
|
||||
internal data class NativeNotificationResponse(
|
||||
val ok: Boolean,
|
||||
val batch: NativeNotificationBatch?,
|
||||
val fallback: NativeNotificationPresentation?,
|
||||
) {
|
||||
companion object {
|
||||
fun parse(json: String): NativeNotificationResponse {
|
||||
val root = JSONObject(json)
|
||||
val batchJson = root.optJSONObject("batch")
|
||||
val additions = batchJson?.optJSONArray("additions")?.let { array ->
|
||||
List(array.length()) { index ->
|
||||
val item = array.getJSONObject(index)
|
||||
NativeNotificationAddition(
|
||||
eventId = item.getString("event_id"),
|
||||
notificationId = item.getString("notification_id"),
|
||||
conversationId = item.nullableString("conversation_id"),
|
||||
senderId = item.getLong("sender_id"),
|
||||
senderName = item.getString("sender_name"),
|
||||
title = item.getString("title"),
|
||||
body = item.getString("body"),
|
||||
createdAt = item.getLong("created_at"),
|
||||
avatarPath = item.nullableString("avatar_path"),
|
||||
)
|
||||
}
|
||||
}.orEmpty()
|
||||
val removals = batchJson?.optJSONArray("removals")?.let { array ->
|
||||
List(array.length()) { index -> array.getString(index) }
|
||||
}.orEmpty()
|
||||
val batch = batchJson?.let {
|
||||
NativeNotificationBatch(
|
||||
additions = additions,
|
||||
removals = removals,
|
||||
badgeCount = it.optLong("badge_count"),
|
||||
completed = it.optBoolean("completed"),
|
||||
)
|
||||
}
|
||||
val fallback = root.optJSONObject("fallback")?.let {
|
||||
NativeNotificationPresentation(
|
||||
title = it.getString("title"),
|
||||
body = it.getString("body"),
|
||||
)
|
||||
}
|
||||
return NativeNotificationResponse(root.optBoolean("ok"), batch, fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.nullableString(key: String): String? =
|
||||
if (isNull(key)) null else optString(key).takeIf(String::isNotEmpty)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package eu.twonly.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
internal fun nativeNotificationId(value: String): Int = value.hashCode() and Int.MAX_VALUE
|
||||
|
||||
/**
|
||||
* Forwards taps on natively rendered notifications into Flutter.
|
||||
*
|
||||
* Only the opaque conversation identifier travels across this channel; the
|
||||
* route itself is built in Dart so Kotlin never duplicates Flutter routing.
|
||||
*/
|
||||
object NotificationTapChannel {
|
||||
private const val CHANNEL = "eu.twonly/notificationTap"
|
||||
const val EXTRA_CONVERSATION_ID = "conversation_id"
|
||||
|
||||
private var channel: MethodChannel? = null
|
||||
private var pendingConversationId: String? = null
|
||||
private var pendingLaunch = false
|
||||
|
||||
fun configure(flutterEngine: FlutterEngine, context: Context) {
|
||||
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumeInitialNotification" -> {
|
||||
val launched = pendingLaunch
|
||||
val conversationId = pendingConversationId
|
||||
pendingLaunch = false
|
||||
pendingConversationId = null
|
||||
result.success(
|
||||
if (launched) {
|
||||
mapOf(EXTRA_CONVERSATION_ID to conversationId)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
"cancelNotifications" -> {
|
||||
val notificationIds = call.argument<List<String>>("notification_ids").orEmpty()
|
||||
val manager = NotificationManagerCompat.from(context.applicationContext)
|
||||
notificationIds.forEach { manager.cancel(nativeNotificationId(it)) }
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
this.channel = channel
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
channel?.setMethodCallHandler(null)
|
||||
channel = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the launch intent before Flutter attaches. Called for both the
|
||||
* cold-start intent and every `onNewIntent`; a live channel is notified
|
||||
* immediately, otherwise the tap is buffered for `consumeInitialNotification`.
|
||||
*/
|
||||
fun handleIntent(intent: Intent?) {
|
||||
if (intent?.hasExtra(EXTRA_CONVERSATION_ID) != true) return
|
||||
val conversationId =
|
||||
intent.getStringExtra(EXTRA_CONVERSATION_ID)?.takeIf(String::isNotEmpty)
|
||||
// A tap must only route once, even if the activity is recreated with
|
||||
// the same intent after a configuration change.
|
||||
intent.removeExtra(EXTRA_CONVERSATION_ID)
|
||||
|
||||
val channel = this.channel
|
||||
if (channel == null) {
|
||||
pendingLaunch = true
|
||||
pendingConversationId = conversationId
|
||||
return
|
||||
}
|
||||
channel.invokeMethod(
|
||||
"onNotificationTapped",
|
||||
mapOf(EXTRA_CONVERSATION_ID to conversationId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package eu.twonly.notifications
|
||||
|
||||
import android.util.Log
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkManager
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
|
||||
class TwonlyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
if (message.data["kind"] != "message_wakeup" || message.data["version"] != "1") {
|
||||
Log.w(TAG, "Ignoring unsupported opaque FCM payload")
|
||||
return
|
||||
}
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<TwonlyNotificationWorker>()
|
||||
.setInputData(Data.Builder().putString(INPUT_MESSAGE_ID, message.messageId).build())
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
WorkManager.getInstance(applicationContext).enqueueUniqueWork(
|
||||
UNIQUE_WORK,
|
||||
ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
try {
|
||||
val directory = applicationContext.filesDir.absolutePath
|
||||
NativeNotificationBridge.storeFcmToken(directory, directory, token)
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "Could not persist refreshed FCM token in Rust", error)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "TwonlyFCM"
|
||||
const val UNIQUE_WORK = "twonly-native-notification-drain"
|
||||
const val INPUT_MESSAGE_ID = "fcm_message_id"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package eu.twonly.notifications
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.twonly.MainActivity
|
||||
import eu.twonly.R
|
||||
import java.util.Locale
|
||||
import org.json.JSONArray
|
||||
|
||||
class TwonlyNotificationWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters,
|
||||
) : Worker(appContext, params) {
|
||||
override fun doWork(): Result {
|
||||
return try {
|
||||
val directory = applicationContext.filesDir.absolutePath
|
||||
val response = NativeNotificationResponse.parse(
|
||||
NativeNotificationBridge.process(
|
||||
directory,
|
||||
directory,
|
||||
Locale.getDefault().toLanguageTag(),
|
||||
RUST_DEADLINE_MS,
|
||||
),
|
||||
)
|
||||
ensureChannel()
|
||||
val batch = response.batch
|
||||
if (!response.ok || batch == null) {
|
||||
// Rust could not reach the mailbox. Show the generic alert so a
|
||||
// high-priority wake-up still produces a notification, and retry.
|
||||
response.fallback?.let(::showFallback)
|
||||
return if (runAttemptCount < MAX_RETRIES) Result.retry() else Result.success()
|
||||
}
|
||||
|
||||
val manager = NotificationManagerCompat.from(applicationContext)
|
||||
batch.removals.forEach { manager.cancel(nativeNotificationId(it)) }
|
||||
val delivered = batch.additions.filter { addition ->
|
||||
showAddition(manager, addition)
|
||||
}.map(NativeNotificationAddition::eventId)
|
||||
if (delivered.isNotEmpty()) {
|
||||
NativeNotificationBridge.acknowledge(JSONArray(delivered).toString())
|
||||
// Real notifications supersede a placeholder from an earlier attempt.
|
||||
manager.cancel(FALLBACK_ID)
|
||||
}
|
||||
|
||||
// An empty batch is the normal outcome of a duplicate wake-up or of
|
||||
// traffic that is not user visible, so it must not retry. Only an
|
||||
// undrained mailbox is worth another attempt.
|
||||
if (!batch.completed && runAttemptCount < MAX_RETRIES) {
|
||||
Result.retry()
|
||||
} else {
|
||||
Result.success()
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "Native notification processing failed", error)
|
||||
if (runAttemptCount < MAX_RETRIES) Result.retry() else Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddition(
|
||||
manager: NotificationManagerCompat,
|
||||
addition: NativeNotificationAddition,
|
||||
): Boolean {
|
||||
val avatar = addition.avatarPath
|
||||
?.let(BitmapFactory::decodeFile)
|
||||
?.let(IconCompat::createWithBitmap)
|
||||
val sender = Person.Builder()
|
||||
.setName(addition.senderName)
|
||||
.setKey(addition.senderId.toString())
|
||||
.setIcon(avatar)
|
||||
.build()
|
||||
val user = Person.Builder().setName(applicationLabel()).setKey("twonly-user").build()
|
||||
val style = NotificationCompat.MessagingStyle(user)
|
||||
.addMessage(addition.body, addition.createdAt * 1_000, sender)
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
// Only opaque identifiers cross into the activity; Dart owns routing.
|
||||
putExtra(
|
||||
NotificationTapChannel.EXTRA_CONVERSATION_ID,
|
||||
addition.conversationId.orEmpty(),
|
||||
)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
nativeNotificationId(addition.notificationId),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(addition.title)
|
||||
.setContentText(addition.body)
|
||||
.setStyle(style)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setGroup(addition.conversationId ?: addition.senderId.toString())
|
||||
.build()
|
||||
return try {
|
||||
manager.notify(nativeNotificationId(addition.notificationId), notification)
|
||||
true
|
||||
} catch (error: SecurityException) {
|
||||
Log.w(TAG, "Notification permission is unavailable", error)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFallback(presentation: NativeNotificationPresentation) {
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(presentation.title)
|
||||
.setContentText(presentation.body)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
try {
|
||||
NotificationManagerCompat.from(applicationContext).notify(FALLBACK_ID, notification)
|
||||
} catch (error: SecurityException) {
|
||||
Log.w(TAG, "Notification permission is unavailable", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
applicationLabel(),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
)
|
||||
applicationContext.getSystemService(NotificationManager::class.java)
|
||||
.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun applicationLabel(): String =
|
||||
applicationContext.applicationInfo.loadLabel(applicationContext.packageManager).toString()
|
||||
|
||||
private companion object {
|
||||
const val TAG = "TwonlyNotification"
|
||||
const val CHANNEL_ID = "twonly_messages_v2"
|
||||
const val FALLBACK_ID = 0x74776F
|
||||
const val MAX_RETRIES = 2
|
||||
const val RUST_DEADLINE_MS = 25_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,14 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.usernotifications.filtering</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.runtime</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
||||
|
|
|
|||
|
|
@ -1,326 +1,273 @@
|
|||
import CryptoKit
|
||||
import Foundation
|
||||
import Security
|
||||
import Intents
|
||||
import UserNotifications
|
||||
import rust_lib_twonly
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(
|
||||
_ request: UNNotificationRequest,
|
||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||
) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
// Store the current timestamp in Keychain for iOS FCM messaging tracking
|
||||
let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000)
|
||||
writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs)
|
||||
NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)")
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
|
||||
guard bestAttemptContent.userInfo as? [String: Any] != nil,
|
||||
let push_data = bestAttemptContent.userInfo["push_data"] as? String
|
||||
else {
|
||||
return contentHandler(bestAttemptContent)
|
||||
}
|
||||
|
||||
let data = getPushNotificationData(pushData: push_data)
|
||||
|
||||
if data != nil {
|
||||
if data!.title == "blocked" {
|
||||
NSLog("Block message because user is blocked!")
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering
|
||||
return contentHandler(UNNotificationContent())
|
||||
}
|
||||
bestAttemptContent.title = data!.title
|
||||
bestAttemptContent.body = data!.body
|
||||
bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId)
|
||||
} else {
|
||||
NSLog("Could not decrypt message. Show default.")
|
||||
bestAttemptContent.title = "\(bestAttemptContent.title)"
|
||||
}
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
private let runtimeAppGroup = "group.eu.twonly.runtime"
|
||||
|
||||
private struct NativeNotificationResponse: Decodable {
|
||||
let ok: Bool
|
||||
let batch: NativeNotificationBatch?
|
||||
let error: String?
|
||||
}
|
||||
|
||||
func getPushNotificationData(pushData: String) -> (
|
||||
title: String, body: String, notificationId: Int64
|
||||
)? {
|
||||
private struct NativeNotificationBatch: Decodable {
|
||||
let additions: [NativeNotificationAddition]
|
||||
let removals: [String]
|
||||
let badgeCount: Int64
|
||||
let completed: Bool
|
||||
|
||||
guard let data = Data(base64Encoded: pushData) else {
|
||||
NSLog("Failed to decode base64 string")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let pushData = try EncryptedPushNotification(serializedBytes: data)
|
||||
|
||||
var pushNotification: PushNotification?
|
||||
var pushUser: PushUser?
|
||||
|
||||
// Check the keyId
|
||||
if pushData.keyID == 0 {
|
||||
let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!
|
||||
pushNotification = tryDecryptMessage(key: key, pushData: pushData)
|
||||
} else {
|
||||
let pushUsers = getPushUsers()
|
||||
if pushUsers != nil {
|
||||
for tryPushUser in pushUsers! {
|
||||
for pushKey in tryPushUser.pushKeys {
|
||||
if pushKey.id == pushData.keyID {
|
||||
pushNotification = tryDecryptMessage(
|
||||
key: pushKey.key, pushData: pushData)
|
||||
if pushNotification != nil {
|
||||
pushUser = tryPushUser
|
||||
if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID)
|
||||
{
|
||||
//return ("blocked", "blocked", 0)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if pushUser != nil { break }
|
||||
}
|
||||
} else {
|
||||
NSLog("pushKeys are empty")
|
||||
}
|
||||
}
|
||||
|
||||
if pushUser?.blocked == true {
|
||||
return ("blocked", "blocked", 0)
|
||||
}
|
||||
|
||||
// Handle the push notification based on the pushKind
|
||||
if let pushNotification = pushNotification {
|
||||
|
||||
if pushNotification.kind == .testNotification {
|
||||
return ("Test Notification", "This is a test notification.", 0)
|
||||
} else if pushUser != nil {
|
||||
return (
|
||||
pushUser!.displayName,
|
||||
getPushNotificationText(pushNotification: pushNotification, userKnown: true).0, pushUser!.userID
|
||||
)
|
||||
} else {
|
||||
let content = getPushNotificationText(pushNotification: pushNotification, userKnown: false)
|
||||
return (
|
||||
content.1, content.0, 1
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
NSLog("Failed to decrypt message or pushKind is nil")
|
||||
}
|
||||
return nil
|
||||
} catch {
|
||||
NSLog("Error decoding JSON: \(error)")
|
||||
return nil
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case additions, removals, completed
|
||||
case badgeCount = "badge_count"
|
||||
}
|
||||
}
|
||||
|
||||
func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool {
|
||||
guard uuid1.count >= 8, uuid2.count >= 8 else { return true }
|
||||
let hex1 = String(uuid1.prefix(8))
|
||||
let hex2 = String(uuid2.prefix(8))
|
||||
guard let timestamp1 = UInt32(hex1, radix: 16),
|
||||
let timestamp2 = UInt32(hex2, radix: 16)
|
||||
else { return true }
|
||||
return timestamp1 > timestamp2
|
||||
private struct NativeNotificationAddition: Decodable {
|
||||
let eventId: String
|
||||
let notificationId: String
|
||||
let conversationId: String?
|
||||
let senderId: Int64
|
||||
let senderName: String
|
||||
let title: String
|
||||
let body: String
|
||||
let conversationName: String?
|
||||
let isGroup: Bool
|
||||
let messageId: String?
|
||||
let kind: String
|
||||
let content: String?
|
||||
let createdAt: Int64
|
||||
let avatarPath: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case kind, content, title, body
|
||||
case eventId = "event_id"
|
||||
case notificationId = "notification_id"
|
||||
case conversationId = "conversation_id"
|
||||
case senderId = "sender_id"
|
||||
case senderName = "sender_name"
|
||||
case conversationName = "conversation_name"
|
||||
case isGroup = "is_group"
|
||||
case messageId = "message_id"
|
||||
case createdAt = "created_at"
|
||||
case avatarPath = "avatar_path"
|
||||
}
|
||||
}
|
||||
|
||||
func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNotification? {
|
||||
final class NotificationService: UNNotificationServiceExtension {
|
||||
private let finishLock = NSLock()
|
||||
private var hasFinished = false
|
||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
|
||||
do {
|
||||
// Create a nonce for ChaChaPoly
|
||||
let nonce = try ChaChaPoly.Nonce(data: pushData.nonce)
|
||||
override func didReceive(
|
||||
_ request: UNNotificationRequest,
|
||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||
) {
|
||||
self.contentHandler = contentHandler
|
||||
|
||||
// Create a sealed box for ChaChaPoly
|
||||
let sealedBox = try ChaChaPoly.SealedBox(
|
||||
nonce: nonce,
|
||||
ciphertext: pushData.ciphertext,
|
||||
tag: pushData.mac
|
||||
)
|
||||
|
||||
// Decrypt the data using the key
|
||||
let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: key))
|
||||
|
||||
// Here you can determine the PushKind based on the decrypted message
|
||||
return try PushNotification(serializedBytes: decryptedData)
|
||||
} catch {
|
||||
NSLog("Decryption failed: \(error)")
|
||||
guard let runtimeDirectory = Self.runtimeDirectory() else {
|
||||
suppress(reason: "shared runtime directory is unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPushUsers() -> [PushUser]? {
|
||||
// Retrieve the data from secure storage (Keychain)
|
||||
guard let pushUsersB64 = readFromKeychain(key: "push_keys_receiving") else {
|
||||
NSLog("No data found for key: push_keys_receiving")
|
||||
return nil
|
||||
}
|
||||
guard let pushUsersBytes = Data(base64Encoded: pushUsersB64) else {
|
||||
NSLog("Failed to decode base64 push users")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let pushUsers = try PushUsers(serializedBytes: pushUsersBytes)
|
||||
return pushUsers.users
|
||||
} catch {
|
||||
NSLog("Error decoding JSON: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to read from Keychain
|
||||
func readFromKeychain(key: String) -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrService as String: "flutter_secure_storage_service",
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group
|
||||
]
|
||||
|
||||
var dataTypeRef: AnyObject? = nil
|
||||
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
||||
|
||||
if status == errSecSuccess {
|
||||
if let data = dataTypeRef as? Data {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to write to Keychain
|
||||
func writeToKeychain(key: String, value: String) {
|
||||
guard let data = value.data(using: .utf8) else {
|
||||
NSLog("Failed to convert value to data for keychain key: \(key)")
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
guard let response = Self.processWakeup(runtimeDirectory: runtimeDirectory) else {
|
||||
self.suppress(reason: "notification worker returned no response")
|
||||
return
|
||||
}
|
||||
guard response.ok,
|
||||
let batch = response.batch,
|
||||
!batch.additions.isEmpty
|
||||
else {
|
||||
self.suppress(reason: response.error ?? "notification worker returned no messages")
|
||||
return
|
||||
}
|
||||
self.render(batch: batch, original: request.content)
|
||||
}
|
||||
}
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrService as String: "flutter_secure_storage_service",
|
||||
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared"
|
||||
]
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
suppress(reason: "notification service extension timed out")
|
||||
}
|
||||
|
||||
// Delete existing item first to ensure a clean overwrite
|
||||
SecItemDelete(query as CFDictionary)
|
||||
private func render(batch: NativeNotificationBatch, original: UNNotificationContent) {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let group = DispatchGroup()
|
||||
let stateLock = NSLock()
|
||||
var deliveredEventIds: [String] = []
|
||||
var finalContent: UNNotificationContent = original
|
||||
|
||||
// Add the new item with background-compatible accessibility
|
||||
var addQuery = query
|
||||
addQuery[kSecValueData as String] = data
|
||||
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
|
||||
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
||||
if status != errSecSuccess {
|
||||
NSLog("Failed to write keychain item for key \(key): \(status)")
|
||||
} else {
|
||||
NSLog("Successfully wrote keychain item for key: \(key)")
|
||||
}
|
||||
}
|
||||
|
||||
func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
|
||||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||
|
||||
var pushNotificationText: [PushKind: String] = [:]
|
||||
var title = "You"
|
||||
var noTranslationFoundTitle = "You have a new message."
|
||||
var noTranslationFoundBody = "Open twonly to learn more."
|
||||
|
||||
// Define the messages based on the system language
|
||||
if systemLanguage.contains("de") { // German
|
||||
title = "Du"
|
||||
noTranslationFoundTitle = "Du hast eine neue Nachricht."
|
||||
noTranslationFoundBody = "Öffne twonly um mehr zu erfahren."
|
||||
if (userKnown) {
|
||||
pushNotificationText = [
|
||||
.text: "hat eine Nachricht{inGroup} gesendet.",
|
||||
.twonly: "hat ein twonly{inGroup} gesendet.",
|
||||
.video: "hat ein Video{inGroup} gesendet.",
|
||||
.image: "hat ein Bild{inGroup} gesendet.",
|
||||
.audio: "hat eine Sprachnachricht{inGroup} gesendet.",
|
||||
.contactRequest: "möchte sich mit dir vernetzen.",
|
||||
.acceptRequest: "ist jetzt mit dir vernetzt.",
|
||||
.storedMediaFile: "hat dein Bild gespeichert.",
|
||||
.reaction: "hat auf dein Bild reagiert.",
|
||||
.testNotification: "Das ist eine Testbenachrichtigung.",
|
||||
.reopenedMedia: "hat dein Bild erneut geöffnet.",
|
||||
.reactionToVideo: "hat mit {{content}} auf dein Video reagiert.",
|
||||
.reactionToText: "hat mit {{content}} auf deinen Text reagiert.",
|
||||
.reactionToImage: "hat mit {{content}} auf dein Bild reagiert.",
|
||||
.reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.",
|
||||
.response: "hat dir{inGroup} geantwortet.",
|
||||
.addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.",
|
||||
]
|
||||
} else {
|
||||
pushNotificationText = [
|
||||
.contactRequest: "hast eine neue Kontaktanfrage erhalten.",
|
||||
]
|
||||
for (index, addition) in batch.additions.enumerated() {
|
||||
group.enter()
|
||||
communicationContent(for: addition, badgeCount: batch.badgeCount, original: original) {
|
||||
content in
|
||||
let isFinal = index == batch.additions.count - 1
|
||||
if isFinal {
|
||||
stateLock.lock()
|
||||
finalContent = content
|
||||
deliveredEventIds.append(addition.eventId)
|
||||
stateLock.unlock()
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (userKnown) {
|
||||
pushNotificationText = [
|
||||
.text: "sent a message{inGroup}.",
|
||||
.twonly: "sent a twonly{inGroup}.",
|
||||
.video: "sent a video{inGroup}.",
|
||||
.image: "sent an image{inGroup}.",
|
||||
.audio: "sent a voice message{inGroup}.",
|
||||
.contactRequest: "wants to connect with you.",
|
||||
.acceptRequest: "is now connected with you.",
|
||||
.storedMediaFile: "has stored your image.",
|
||||
.reaction: "has reacted to your image.",
|
||||
.testNotification: "This is a test notification.",
|
||||
.reopenedMedia: "has reopened your image.",
|
||||
.reactionToVideo: "has reacted with {{content}} to your video.",
|
||||
.reactionToText: "has reacted with {{content}} to your text.",
|
||||
.reactionToImage: "has reacted with {{content}} to your image.",
|
||||
.reactionToAudio: "has reacted with {{content}} to your voice message.",
|
||||
.response: "has responded{inGroup}.",
|
||||
.addedToGroup: "has added you to \"{{content}}\"",
|
||||
]
|
||||
} else {
|
||||
pushNotificationText = [
|
||||
.contactRequest: "have received a new contact request.",
|
||||
]
|
||||
|
||||
let request = UNNotificationRequest(
|
||||
identifier: addition.notificationId,
|
||||
content: content,
|
||||
trigger: nil
|
||||
)
|
||||
center.add(request) { error in
|
||||
if let error {
|
||||
NSLog("Could not schedule Twonly notification: \(error)")
|
||||
} else {
|
||||
stateLock.lock()
|
||||
deliveredEventIds.append(addition.eventId)
|
||||
stateLock.unlock()
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var content = pushNotificationText[pushNotification.kind] ?? ""
|
||||
if (content == "") {
|
||||
title = noTranslationFoundTitle
|
||||
content = noTranslationFoundBody
|
||||
group.notify(queue: .global(qos: .userInitiated)) { [weak self] in
|
||||
stateLock.lock()
|
||||
let eventIds = deliveredEventIds
|
||||
let content = finalContent
|
||||
stateLock.unlock()
|
||||
Self.acknowledge(eventIds: eventIds)
|
||||
self?.finish(with: content)
|
||||
}
|
||||
}
|
||||
|
||||
if pushNotification.hasAdditionalContent {
|
||||
content.replace("{{content}}", with: pushNotification.additionalContent)
|
||||
content.replace("{inGroup}", with: " in {inGroup}")
|
||||
content.replace("{inGroup}", with: pushNotification.additionalContent)
|
||||
} else {
|
||||
content.replace("{inGroup}", with: "")
|
||||
private func communicationContent(
|
||||
for addition: NativeNotificationAddition,
|
||||
badgeCount: Int64,
|
||||
original: UNNotificationContent,
|
||||
completion: @escaping (UNNotificationContent) -> Void
|
||||
) {
|
||||
let mutable = (original.mutableCopy() as? UNMutableNotificationContent)
|
||||
?? UNMutableNotificationContent()
|
||||
mutable.title = addition.title
|
||||
mutable.body = addition.body
|
||||
mutable.threadIdentifier = addition.conversationId ?? String(addition.senderId)
|
||||
mutable.badge = NSNumber(value: badgeCount)
|
||||
mutable.sound = .default
|
||||
var userInfo = mutable.userInfo
|
||||
if let conversationId = addition.conversationId {
|
||||
userInfo["conversation_id"] = conversationId
|
||||
}
|
||||
userInfo["notification_id"] = addition.notificationId
|
||||
mutable.userInfo = userInfo
|
||||
|
||||
let avatar = addition.avatarPath
|
||||
.flatMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) }
|
||||
.map(INImage.init(imageData:))
|
||||
let sender = INPerson(
|
||||
personHandle: INPersonHandle(value: String(addition.senderId), type: .unknown),
|
||||
nameComponents: nil,
|
||||
displayName: addition.senderName,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: String(addition.senderId)
|
||||
)
|
||||
let groupName = addition.isGroup
|
||||
? addition.conversationName.map(INSpeakableString.init(spokenPhrase:))
|
||||
: nil
|
||||
let intent = INSendMessageIntent(
|
||||
recipients: nil,
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: addition.body,
|
||||
speakableGroupName: groupName,
|
||||
conversationIdentifier: addition.conversationId ?? String(addition.senderId),
|
||||
serviceName: "Twonly",
|
||||
sender: sender,
|
||||
attachments: nil
|
||||
)
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
interaction.donate { error in
|
||||
if let error {
|
||||
NSLog("Could not donate Twonly communication intent: \(error)")
|
||||
completion(mutable)
|
||||
return
|
||||
}
|
||||
do {
|
||||
completion(try mutable.updating(from: intent))
|
||||
} catch {
|
||||
NSLog("Could not create Twonly communication notification: \(error)")
|
||||
completion(mutable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(with content: UNNotificationContent) {
|
||||
finishLock.lock()
|
||||
guard !hasFinished else {
|
||||
finishLock.unlock()
|
||||
return
|
||||
}
|
||||
hasFinished = true
|
||||
let handler = contentHandler
|
||||
contentHandler = nil
|
||||
finishLock.unlock()
|
||||
handler?(content)
|
||||
}
|
||||
|
||||
private func suppress(reason: String) {
|
||||
NSLog("Suppressing Twonly wake-up notification: \(reason)")
|
||||
finish(with: UNNotificationContent())
|
||||
}
|
||||
|
||||
private static func runtimeDirectory() -> String? {
|
||||
guard
|
||||
let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: runtimeAppGroup
|
||||
)
|
||||
else { return nil }
|
||||
let directory = container.appendingPathComponent("runtime", isDirectory: true)
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
try (directory as NSURL).setResourceValue(
|
||||
URLFileProtection.completeUntilFirstUserAuthentication,
|
||||
forKey: .fileProtectionKey
|
||||
)
|
||||
return directory.path
|
||||
} catch {
|
||||
NSLog("Could not open Twonly runtime directory: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func processWakeup(runtimeDirectory: String) -> NativeNotificationResponse? {
|
||||
let locale = Locale.current.identifier
|
||||
let pointer = runtimeDirectory.withCString { databaseDirectory in
|
||||
runtimeDirectory.withCString { dataDirectory in
|
||||
locale.withCString { locale in
|
||||
twonly_notification_process(databaseDirectory, dataDirectory, locale, 24_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard let pointer else { return nil }
|
||||
defer { twonly_notification_string_free(pointer) }
|
||||
let json = String(cString: pointer)
|
||||
do {
|
||||
return try JSONDecoder().decode(
|
||||
NativeNotificationResponse.self,
|
||||
from: Data(json.utf8)
|
||||
)
|
||||
} catch {
|
||||
NSLog("Could not decode Twonly notification worker response: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func acknowledge(eventIds: [String]) {
|
||||
guard !eventIds.isEmpty, let data = try? JSONEncoder().encode(eventIds),
|
||||
let json = String(data: data, encoding: .utf8)
|
||||
else { return }
|
||||
let pointer = json.withCString { twonly_notification_acknowledge($0) }
|
||||
guard let pointer else { return }
|
||||
twonly_notification_string_free(pointer)
|
||||
}
|
||||
|
||||
// Return the corresponding message or an empty string if not found
|
||||
return (content, title)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,447 +0,0 @@
|
|||
// DO NOT EDIT.
|
||||
// swift-format-ignore-file
|
||||
// swiftlint:disable all
|
||||
//
|
||||
// Generated by the Swift generator plugin for the protocol buffer compiler.
|
||||
// Source: push_notification.proto
|
||||
//
|
||||
// For information on using the generated types, please see the documentation:
|
||||
// https://github.com/apple/swift-protobuf/
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
import FoundationEssentials
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
import SwiftProtobuf
|
||||
|
||||
// If the compiler emits an error on this type, it is because this file
|
||||
// was generated by a version of the `protoc` Swift plug-in that is
|
||||
// incompatible with the version of SwiftProtobuf to which you are linking.
|
||||
// Please ensure that you are building against the same version of the API
|
||||
// that was used to generate this file.
|
||||
fileprivate nonisolated struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
|
||||
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
|
||||
typealias Version = _2
|
||||
}
|
||||
|
||||
nonisolated enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable {
|
||||
typealias RawValue = Int
|
||||
case reaction // = 0
|
||||
case response // = 1
|
||||
case text // = 2
|
||||
case video // = 3
|
||||
case twonly // = 4
|
||||
case image // = 5
|
||||
case contactRequest // = 6
|
||||
case acceptRequest // = 7
|
||||
case storedMediaFile // = 8
|
||||
case testNotification // = 9
|
||||
case reopenedMedia // = 10
|
||||
case reactionToVideo // = 11
|
||||
case reactionToText // = 12
|
||||
case reactionToImage // = 13
|
||||
case reactionToAudio // = 14
|
||||
case addedToGroup // = 15
|
||||
case audio // = 16
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
self = .reaction
|
||||
}
|
||||
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .reaction
|
||||
case 1: self = .response
|
||||
case 2: self = .text
|
||||
case 3: self = .video
|
||||
case 4: self = .twonly
|
||||
case 5: self = .image
|
||||
case 6: self = .contactRequest
|
||||
case 7: self = .acceptRequest
|
||||
case 8: self = .storedMediaFile
|
||||
case 9: self = .testNotification
|
||||
case 10: self = .reopenedMedia
|
||||
case 11: self = .reactionToVideo
|
||||
case 12: self = .reactionToText
|
||||
case 13: self = .reactionToImage
|
||||
case 14: self = .reactionToAudio
|
||||
case 15: self = .addedToGroup
|
||||
case 16: self = .audio
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .reaction: return 0
|
||||
case .response: return 1
|
||||
case .text: return 2
|
||||
case .video: return 3
|
||||
case .twonly: return 4
|
||||
case .image: return 5
|
||||
case .contactRequest: return 6
|
||||
case .acceptRequest: return 7
|
||||
case .storedMediaFile: return 8
|
||||
case .testNotification: return 9
|
||||
case .reopenedMedia: return 10
|
||||
case .reactionToVideo: return 11
|
||||
case .reactionToText: return 12
|
||||
case .reactionToImage: return 13
|
||||
case .reactionToAudio: return 14
|
||||
case .addedToGroup: return 15
|
||||
case .audio: return 16
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
||||
// The compiler won't synthesize support with the UNRECOGNIZED case.
|
||||
static let allCases: [PushKind] = [
|
||||
.reaction,
|
||||
.response,
|
||||
.text,
|
||||
.video,
|
||||
.twonly,
|
||||
.image,
|
||||
.contactRequest,
|
||||
.acceptRequest,
|
||||
.storedMediaFile,
|
||||
.testNotification,
|
||||
.reopenedMedia,
|
||||
.reactionToVideo,
|
||||
.reactionToText,
|
||||
.reactionToImage,
|
||||
.reactionToAudio,
|
||||
.addedToGroup,
|
||||
.audio,
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
nonisolated struct EncryptedPushNotification: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var keyID: Int64 = 0
|
||||
|
||||
var nonce: Data = Data()
|
||||
|
||||
var ciphertext: Data = Data()
|
||||
|
||||
var mac: Data = Data()
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
}
|
||||
|
||||
nonisolated struct PushNotification: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var kind: PushKind = .reaction
|
||||
|
||||
var messageID: String {
|
||||
get {_messageID ?? String()}
|
||||
set {_messageID = newValue}
|
||||
}
|
||||
/// Returns true if `messageID` has been explicitly set.
|
||||
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 {_additionalContent ?? String()}
|
||||
set {_additionalContent = newValue}
|
||||
}
|
||||
/// Returns true if `additionalContent` has been explicitly set.
|
||||
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}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _messageID: String? = nil
|
||||
fileprivate var _additionalContent: String? = nil
|
||||
}
|
||||
|
||||
nonisolated struct PushUsers: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var users: [PushUser] = []
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
}
|
||||
|
||||
nonisolated struct PushUser: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var userID: Int64 = 0
|
||||
|
||||
var displayName: String = String()
|
||||
|
||||
var blocked: Bool = false
|
||||
|
||||
var lastMessageID: String {
|
||||
get {_lastMessageID ?? String()}
|
||||
set {_lastMessageID = newValue}
|
||||
}
|
||||
/// Returns true if `lastMessageID` has been explicitly set.
|
||||
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}
|
||||
|
||||
var pushKeys: [PushKey] = []
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _lastMessageID: String? = nil
|
||||
}
|
||||
|
||||
nonisolated struct PushKey: Sendable {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
var id: Int64 = 0
|
||||
|
||||
var key: Data = Data()
|
||||
|
||||
var createdAtUnixTimestamp: Int64 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
}
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
nonisolated extension PushKind: SwiftProtobuf._ProtoNameProviding {
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0REACTION\0\u{1}RESPONSE\0\u{1}TEXT\0\u{1}VIDEO\0\u{1}TWONLY\0\u{1}IMAGE\0\u{1}CONTACT_REQUEST\0\u{1}ACCEPT_REQUEST\0\u{1}STORED_MEDIA_FILE\0\u{1}TEST_NOTIFICATION\0\u{1}REOPENED_MEDIA\0\u{1}REACTION_TO_VIDEO\0\u{1}REACTION_TO_TEXT\0\u{1}REACTION_TO_IMAGE\0\u{1}REACTION_TO_AUDIO\0\u{1}ADDED_TO_GROUP\0\u{1}AUDIO\0")
|
||||
}
|
||||
|
||||
nonisolated extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "EncryptedPushNotification"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularInt64Field(value: &self.keyID) }()
|
||||
case 2: try { try decoder.decodeSingularBytesField(value: &self.nonce) }()
|
||||
case 3: try { try decoder.decodeSingularBytesField(value: &self.ciphertext) }()
|
||||
case 4: try { try decoder.decodeSingularBytesField(value: &self.mac) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.keyID != 0 {
|
||||
try visitor.visitSingularInt64Field(value: self.keyID, fieldNumber: 1)
|
||||
}
|
||||
if !self.nonce.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.nonce, fieldNumber: 2)
|
||||
}
|
||||
if !self.ciphertext.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.ciphertext, fieldNumber: 3)
|
||||
}
|
||||
if !self.mac.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.mac, fieldNumber: 4)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: EncryptedPushNotification, rhs: EncryptedPushNotification) -> Bool {
|
||||
if lhs.keyID != rhs.keyID {return false}
|
||||
if lhs.nonce != rhs.nonce {return false}
|
||||
if lhs.ciphertext != rhs.ciphertext {return false}
|
||||
if lhs.mac != rhs.mac {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "PushNotification"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &self._additionalContent) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if self.kind != .reaction {
|
||||
try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1)
|
||||
}
|
||||
try { if let v = self._messageID {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
} }()
|
||||
try { if let v = self._additionalContent {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
} }()
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: PushNotification, rhs: PushNotification) -> Bool {
|
||||
if lhs.kind != rhs.kind {return false}
|
||||
if lhs._messageID != rhs._messageID {return false}
|
||||
if lhs._additionalContent != rhs._additionalContent {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "PushUsers"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}users\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.users) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if !self.users.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.users, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: PushUsers, rhs: PushUsers) -> Bool {
|
||||
if lhs.users != rhs.users {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "PushUser"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }()
|
||||
case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }()
|
||||
case 4: try { try decoder.decodeSingularStringField(value: &self._lastMessageID) }()
|
||||
case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if self.userID != 0 {
|
||||
try visitor.visitSingularInt64Field(value: self.userID, fieldNumber: 1)
|
||||
}
|
||||
if !self.displayName.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.displayName, fieldNumber: 2)
|
||||
}
|
||||
if self.blocked != false {
|
||||
try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3)
|
||||
}
|
||||
try { if let v = self._lastMessageID {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||
} }()
|
||||
if !self.pushKeys.isEmpty {
|
||||
try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: PushUser, rhs: PushUser) -> Bool {
|
||||
if lhs.userID != rhs.userID {return false}
|
||||
if lhs.displayName != rhs.displayName {return false}
|
||||
if lhs.blocked != rhs.blocked {return false}
|
||||
if lhs._lastMessageID != rhs._lastMessageID {return false}
|
||||
if lhs.pushKeys != rhs.pushKeys {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "PushKey"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularInt64Field(value: &self.id) }()
|
||||
case 2: try { try decoder.decodeSingularBytesField(value: &self.key) }()
|
||||
case 3: try { try decoder.decodeSingularInt64Field(value: &self.createdAtUnixTimestamp) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if self.id != 0 {
|
||||
try visitor.visitSingularInt64Field(value: self.id, fieldNumber: 1)
|
||||
}
|
||||
if !self.key.isEmpty {
|
||||
try visitor.visitSingularBytesField(value: self.key, fieldNumber: 2)
|
||||
}
|
||||
if self.createdAtUnixTimestamp != 0 {
|
||||
try visitor.visitSingularInt64Field(value: self.createdAtUnixTimestamp, fieldNumber: 3)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: PushKey, rhs: PushKey) -> Bool {
|
||||
if lhs.id != rhs.id {return false}
|
||||
if lhs.key != rhs.key {return false}
|
||||
if lhs.createdAtUnixTimestamp != rhs.createdAtUnixTimestamp {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +97,5 @@ post_install do |installer|
|
|||
end
|
||||
|
||||
target 'NotificationService' do
|
||||
pod 'SwiftProtobuf'
|
||||
# pod 'Firebase/Messaging'
|
||||
pod 'rust_lib_twonly', :path => '.symlinks/plugins/rust_lib_twonly/ios'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -93,8 +93,7 @@ PODS:
|
|||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- rust_lib_twonly (0.0.1):
|
||||
- Flutter
|
||||
- rust_lib_twonly (0.0.1)
|
||||
- screen_protector (1.5.1):
|
||||
- Flutter
|
||||
- ScreenProtectorKit (= 1.5.1)
|
||||
|
|
@ -199,7 +198,7 @@ SPEC CHECKSUMS:
|
|||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||
rust_lib_twonly: 6586fdf02e31cd8a3ad9f1ee84796da6bbf289bb
|
||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
|
|
@ -207,6 +206,6 @@ SPEC CHECKSUMS:
|
|||
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
|
||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||
|
||||
PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf
|
||||
PODFILE CHECKSUM: f83bbaaed0b8c29b006472e50864d40e54617e24
|
||||
|
||||
COCOAPODS: 1.17.0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
|
|
@ -23,7 +24,6 @@
|
|||
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 */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -91,6 +91,7 @@
|
|||
70E8A5E1DA4031C0E3F86C77 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7FC59147CD9A45BFAC98EA05 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
|
|
@ -115,7 +116,6 @@
|
|||
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>"; };
|
||||
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
|
@ -311,9 +311,6 @@
|
|||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
|
|
@ -335,6 +332,9 @@
|
|||
D25D4D792EFF41DB0029F805 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
|
@ -385,9 +385,6 @@
|
|||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
|
|
@ -419,6 +416,9 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -1319,12 +1319,14 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
||||
"version" : "11.2.0"
|
||||
"revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
|
||||
"version" : "11.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
||||
"version" : "12.14.0"
|
||||
"revision" : "346daa9f46316aa372b35b317e18224acc2e9063",
|
||||
"version" : "12.18.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
||||
"version" : "3.6.0"
|
||||
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
|
||||
"version" : "3.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -50,8 +50,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
||||
"version" : "12.14.0"
|
||||
"revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5",
|
||||
"version" : "12.18.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
"revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974",
|
||||
"version" : "10.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -68,8 +68,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
||||
"version" : "8.1.0"
|
||||
"revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3",
|
||||
"version" : "8.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -86,8 +86,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
"revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc",
|
||||
"version" : "5.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -113,8 +113,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version" : "2.30910.0"
|
||||
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||
"version" : "2.30910.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -122,8 +122,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version" : "2.4.0"
|
||||
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||
"version" : "2.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import workmanager_apple
|
|||
|
||||
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
|
||||
GeneratedPluginRegistrant.register(with: registry)
|
||||
// Background tasks call AppEnvironment.init() too, so this engine needs
|
||||
// the runtime storage channel just as much as the implicit one.
|
||||
RuntimeStorageChannel.register(with: registry)
|
||||
}
|
||||
|
||||
WorkmanagerPlugin.registerPeriodicTask(
|
||||
|
|
@ -51,6 +54,8 @@ import workmanager_apple
|
|||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
|
||||
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
|
||||
override func userNotificationCenter(
|
||||
|
|
@ -70,23 +75,139 @@ import workmanager_apple
|
|||
) {
|
||||
NSLog("userNotificationCenter:willPresent")
|
||||
|
||||
/*
|
||||
debugging NotificationService
|
||||
let pushKeys = getPushKey();
|
||||
print(pushKeys)
|
||||
|
||||
let bestAttemptContent = notification.request.content
|
||||
|
||||
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
||||
let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
|
||||
return completionHandler([.alert, .sound])
|
||||
}
|
||||
|
||||
let data = getPushNotificationData(pushDataJson: push_data)
|
||||
print(data)
|
||||
*/
|
||||
|
||||
completionHandler([.alert, .sound])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Withdraws only the native notifications whose message IDs Dart reports as
|
||||
/// opened. The notification service extension's final alert keeps APNs' request
|
||||
/// identifier, so both the identifier and our `notification_id` user-info field
|
||||
/// have to be considered.
|
||||
class NativeNotificationChannel {
|
||||
private static let channelName = "eu.twonly/notificationTap"
|
||||
private static let notificationIdsKey = "notification_ids"
|
||||
|
||||
static func register(with registry: FlutterPluginRegistry) {
|
||||
guard let registrar = registry.registrar(forPlugin: "TwonlyNativeNotifications") else {
|
||||
return
|
||||
}
|
||||
let channel = FlutterMethodChannel(
|
||||
name: channelName,
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
channel.setMethodCallHandler { call, result in
|
||||
guard call.method == "cancelNotifications" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
guard
|
||||
let arguments = call.arguments as? [String: Any],
|
||||
let values = arguments[notificationIdsKey] as? [String]
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "invalid_notification_ids",
|
||||
message: "notification_ids must be a list of strings",
|
||||
details: nil
|
||||
))
|
||||
return
|
||||
}
|
||||
removeNotifications(Set(values), completion: result)
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeNotifications(
|
||||
_ notificationIds: Set<String>,
|
||||
completion: @escaping FlutterResult
|
||||
) {
|
||||
guard !notificationIds.isEmpty else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getDeliveredNotifications { delivered in
|
||||
let requestIds = delivered.compactMap { notification -> String? in
|
||||
let request = notification.request
|
||||
let notificationId = request.content.userInfo["notification_id"] as? String
|
||||
return notificationIds.contains(request.identifier)
|
||||
|| notificationId.map(notificationIds.contains) == true
|
||||
? request.identifier
|
||||
: nil
|
||||
}
|
||||
center.removeDeliveredNotifications(withIdentifiers: requestIds)
|
||||
center.getPendingNotificationRequests { pending in
|
||||
let pendingIds = pending.compactMap { request -> String? in
|
||||
let notificationId = request.content.userInfo["notification_id"] as? String
|
||||
return notificationIds.contains(request.identifier)
|
||||
|| notificationId.map(notificationIds.contains) == true
|
||||
? request.identifier
|
||||
: nil
|
||||
}
|
||||
center.removePendingNotificationRequests(withIdentifiers: pendingIds)
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hands Dart the App Group container shared between the app and its
|
||||
/// extensions. Must be registered on every Flutter engine that runs
|
||||
/// `AppEnvironment.init()`, not just the implicit one.
|
||||
class RuntimeStorageChannel {
|
||||
private static let appGroupIdentifier = "group.eu.twonly.runtime"
|
||||
|
||||
static func register(with registry: FlutterPluginRegistry) {
|
||||
guard let registrar = registry.registrar(forPlugin: "TwonlyRuntimeStorage") else {
|
||||
return
|
||||
}
|
||||
register(with: registrar.messenger())
|
||||
}
|
||||
|
||||
static func register(with messenger: FlutterBinaryMessenger) {
|
||||
let channel = FlutterMethodChannel(
|
||||
name: "eu.twonly/runtime_storage",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
channel.setMethodCallHandler { call, result in
|
||||
guard call.method == "runtimeSupportDirectory" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
guard
|
||||
let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: appGroupIdentifier
|
||||
)
|
||||
else {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "runtime_app_group_unavailable",
|
||||
message: "Could not open \(appGroupIdentifier)",
|
||||
details: nil
|
||||
))
|
||||
return
|
||||
}
|
||||
let runtimeDirectory = container.appendingPathComponent("runtime", isDirectory: true)
|
||||
do {
|
||||
try FileManager.default.createDirectory(
|
||||
at: runtimeDirectory,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
try (runtimeDirectory as NSURL).setResourceValue(
|
||||
URLFileProtection.completeUntilFirstUserAuthentication,
|
||||
forKey: .fileProtectionKey
|
||||
)
|
||||
result(runtimeDirectory.path)
|
||||
} catch {
|
||||
result(
|
||||
FlutterError(
|
||||
code: "runtime_directory_failed",
|
||||
message: error.localizedDescription,
|
||||
details: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@
|
|||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
<array>
|
||||
<string>applinks:me.twonly.eu</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.shareIntent</string>
|
||||
<string>group.eu.twonly.runtime</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
<array>
|
||||
<string>applinks:me.twonly.eu</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.shareIntent</string>
|
||||
<string>group.eu.twonly.runtime</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
<array>
|
||||
<string>applinks:me.twonly.eu</string>
|
||||
</array>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.eu.twonly.shareIntent</string>
|
||||
<string>group.eu.twonly.runtime</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|||
|
||||
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AnnouncedUser`, `OtherPromotion`
|
||||
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `initialize_twonly_notification`
|
||||
|
||||
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
||||
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,19 @@ class LegacyTableMigrationCount {
|
|||
class RustAppDatabase {
|
||||
const RustAppDatabase();
|
||||
|
||||
/// Streams the tables Rust has committed to.
|
||||
///
|
||||
/// Rust owns the connection, so writes it makes on its own never pass
|
||||
/// through the Drift compatibility executor and cannot invalidate Drift's
|
||||
/// query streams. Dart forwards each batch into `notifyUpdates` so
|
||||
/// `watch()` keeps reflecting Rust-side writes.
|
||||
///
|
||||
/// An empty list means "assume every table changed". It is sent right
|
||||
/// after (re)subscribing, and whenever the broadcast channel drops
|
||||
/// notifications, so Dart never silently keeps stale rows on screen.
|
||||
static Stream<List<String>> changes() => RustLib.instance.api
|
||||
.crateBridgeWrapperAppDatabaseRustAppDatabaseChanges();
|
||||
|
||||
static Future<SqlExecutionResult> execute({
|
||||
required String statement,
|
||||
required List<SqlValue> arguments,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -1286774374;
|
||||
int get rustContentHash => -1780439173;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
|
|
@ -409,6 +409,8 @@ abstract class RustLibApi extends BaseApi {
|
|||
required List<PqcPreKeyInput> prekeys,
|
||||
});
|
||||
|
||||
Stream<List<String>> crateBridgeWrapperAppDatabaseRustAppDatabaseChanges();
|
||||
|
||||
Future<SqlExecutionResult>
|
||||
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
||||
required String statement,
|
||||
|
|
@ -3283,6 +3285,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Stream<List<String>> crateBridgeWrapperAppDatabaseRustAppDatabaseChanges() {
|
||||
final sink = RustStreamSink<List<String>>();
|
||||
unawaited(
|
||||
handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_StreamSink_list_String_Sse(sink, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 77,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperAppDatabaseRustAppDatabaseChangesConstMeta,
|
||||
argValues: [sink],
|
||||
apiImpl: this,
|
||||
),
|
||||
),
|
||||
);
|
||||
return sink.stream;
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperAppDatabaseRustAppDatabaseChangesConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_app_database_changes",
|
||||
argNames: ["sink"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<SqlExecutionResult>
|
||||
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
||||
|
|
@ -3298,7 +3337,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 77,
|
||||
funcId: 78,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3331,7 +3370,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 78,
|
||||
funcId: 79,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3364,7 +3403,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 79,
|
||||
funcId: 80,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3401,7 +3440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 80,
|
||||
funcId: 81,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3433,7 +3472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 81,
|
||||
funcId: 82,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3466,7 +3505,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 82,
|
||||
funcId: 83,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3501,7 +3540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 83,
|
||||
funcId: 84,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3533,7 +3572,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 84,
|
||||
funcId: 85,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3571,7 +3610,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 85,
|
||||
funcId: 86,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3604,7 +3643,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 86,
|
||||
funcId: 87,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3642,7 +3681,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 87,
|
||||
funcId: 88,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3679,7 +3718,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 88,
|
||||
funcId: 89,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3716,7 +3755,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 89,
|
||||
funcId: 90,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3754,7 +3793,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 90,
|
||||
funcId: 91,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3792,7 +3831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 91,
|
||||
funcId: 92,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3824,7 +3863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 92,
|
||||
funcId: 93,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3857,7 +3896,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 93,
|
||||
funcId: 94,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3889,7 +3928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 94,
|
||||
funcId: 95,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3924,7 +3963,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 95,
|
||||
funcId: 96,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3961,7 +4000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 96,
|
||||
funcId: 97,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -3993,7 +4032,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 97,
|
||||
funcId: 98,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4025,7 +4064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 98,
|
||||
funcId: 99,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4060,7 +4099,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 99,
|
||||
funcId: 100,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4099,7 +4138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 100,
|
||||
funcId: 101,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4136,7 +4175,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 101,
|
||||
funcId: 102,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4166,7 +4205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 102,
|
||||
funcId: 103,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4198,7 +4237,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 103,
|
||||
funcId: 104,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4233,7 +4272,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 104,
|
||||
funcId: 105,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4265,7 +4304,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 105,
|
||||
funcId: 106,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4303,7 +4342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 106,
|
||||
funcId: 107,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4342,7 +4381,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 107,
|
||||
funcId: 108,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4377,7 +4416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 108,
|
||||
funcId: 109,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4412,7 +4451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 109,
|
||||
funcId: 110,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4447,7 +4486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 110,
|
||||
funcId: 111,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4480,7 +4519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 111,
|
||||
funcId: 112,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -4520,7 +4559,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 112,
|
||||
funcId: 113,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4565,7 +4604,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 113,
|
||||
funcId: 114,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4595,7 +4634,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 114,
|
||||
funcId: 115,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4628,7 +4667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 115,
|
||||
funcId: 116,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4663,7 +4702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 116,
|
||||
funcId: 117,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -4995,6 +5034,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> dco_decode_StreamSink_list_String_Sse(
|
||||
dynamic raw,
|
||||
) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
|
|
@ -5782,8 +5829,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
UserConfig dco_decode_user_config(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 58)
|
||||
throw Exception('unexpected arr length: expect 58 but see ${arr.length}');
|
||||
if (arr.length != 59)
|
||||
throw Exception('unexpected arr length: expect 59 but see ${arr.length}');
|
||||
return UserConfig(
|
||||
userId: dco_decode_i_64(arr[0]),
|
||||
username: dco_decode_String(arr[1]),
|
||||
|
|
@ -5845,9 +5892,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
passwordLessRecovery:
|
||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]),
|
||||
fcmToken: dco_decode_opt_String(arr[54]),
|
||||
currentSetupPage: dco_decode_opt_String(arr[55]),
|
||||
skipSetupPages: dco_decode_bool(arr[56]),
|
||||
hasZoomed: dco_decode_bool(arr[57]),
|
||||
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[55]),
|
||||
currentSetupPage: dco_decode_opt_String(arr[56]),
|
||||
skipSetupPages: dco_decode_bool(arr[57]),
|
||||
hasZoomed: dco_decode_bool(arr[58]),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -5912,6 +5960,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
throw UnimplementedError('Unreachable ()');
|
||||
}
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> sse_decode_StreamSink_list_String_Sse(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
throw UnimplementedError('Unreachable ()');
|
||||
}
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
|
@ -6938,6 +6994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
var var_passwordLessRecovery =
|
||||
sse_decode_opt_box_autoadd_passwordless_recovery_config(deserializer);
|
||||
var var_fcmToken = sse_decode_opt_String(deserializer);
|
||||
var var_lastFcmWakeupAt = sse_decode_opt_box_autoadd_i_64(deserializer);
|
||||
var var_currentSetupPage = sse_decode_opt_String(deserializer);
|
||||
var var_skipSetupPages = sse_decode_bool(deserializer);
|
||||
var var_hasZoomed = sse_decode_bool(deserializer);
|
||||
|
|
@ -7000,6 +7057,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
isBackupEnabled: var_isBackupEnabled,
|
||||
passwordLessRecovery: var_passwordLessRecovery,
|
||||
fcmToken: var_fcmToken,
|
||||
lastFcmWakeupAt: var_lastFcmWakeupAt,
|
||||
currentSetupPage: var_currentSetupPage,
|
||||
skipSetupPages: var_skipSetupPages,
|
||||
hasZoomed: var_hasZoomed,
|
||||
|
|
@ -7175,6 +7233,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_String_Sse(
|
||||
RustStreamSink<List<String>> self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_String(
|
||||
self.setupAndSerialize(
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_String,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
),
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
|
@ -8112,6 +8187,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
serializer,
|
||||
);
|
||||
sse_encode_opt_String(self.fcmToken, serializer);
|
||||
sse_encode_opt_box_autoadd_i_64(self.lastFcmWakeupAt, serializer);
|
||||
sse_encode_opt_String(self.currentSetupPage, serializer);
|
||||
sse_encode_bool(self.skipSetupPages, serializer);
|
||||
sse_encode_bool(self.hasZoomed, serializer);
|
||||
|
|
|
|||
|
|
@ -82,6 +82,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> dco_decode_StreamSink_list_String_Sse(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
|
|
@ -405,6 +410,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> sse_decode_StreamSink_list_String_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -828,6 +838,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_String_Sse(
|
||||
RustStreamSink<List<String>> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> dco_decode_StreamSink_list_String_Sse(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
String dco_decode_String(dynamic raw);
|
||||
|
||||
|
|
@ -407,6 +412,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
RustStreamSink<List<String>> sse_decode_StreamSink_list_String_Sse(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
String sse_decode_String(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -830,6 +840,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_StreamSink_list_String_Sse(
|
||||
RustStreamSink<List<String>> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_String(String self, SseSerializer serializer);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,11 @@ class UserConfig {
|
|||
bool isBackupEnabled;
|
||||
PasswordlessRecoveryConfig? passwordLessRecovery;
|
||||
String? fcmToken;
|
||||
|
||||
/// Unix seconds of the last opaque FCM/APNs wake-up that reached the native
|
||||
/// notification worker. Recorded in Rust because Flutter is no longer
|
||||
/// started for background delivery on either platform.
|
||||
PlatformInt64? lastFcmWakeupAt;
|
||||
String? currentSetupPage;
|
||||
bool skipSetupPages;
|
||||
bool hasZoomed;
|
||||
|
|
@ -218,6 +223,7 @@ class UserConfig {
|
|||
required this.isBackupEnabled,
|
||||
this.passwordLessRecovery,
|
||||
this.fcmToken,
|
||||
this.lastFcmWakeupAt,
|
||||
this.currentSetupPage,
|
||||
required this.skipSetupPages,
|
||||
required this.hasZoomed,
|
||||
|
|
@ -280,6 +286,7 @@ class UserConfig {
|
|||
isBackupEnabled.hashCode ^
|
||||
passwordLessRecovery.hashCode ^
|
||||
fcmToken.hashCode ^
|
||||
lastFcmWakeupAt.hashCode ^
|
||||
currentSetupPage.hashCode ^
|
||||
skipSetupPages.hashCode ^
|
||||
hasZoomed.hashCode;
|
||||
|
|
@ -351,6 +358,7 @@ class UserConfig {
|
|||
isBackupEnabled == other.isBackupEnabled &&
|
||||
passwordLessRecovery == other.passwordLessRecovery &&
|
||||
fcmToken == other.fcmToken &&
|
||||
lastFcmWakeupAt == other.lastFcmWakeupAt &&
|
||||
currentSetupPage == other.currentSetupPage &&
|
||||
skipSetupPages == other.skipSetupPages &&
|
||||
hasZoomed == other.hasZoomed;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
|
||||
|
||||
class AppEnvironment {
|
||||
static const _runtimeChannel = MethodChannel('eu.twonly/runtime_storage');
|
||||
static late String cacheDir;
|
||||
static late String supportDir;
|
||||
|
||||
|
|
@ -18,11 +21,66 @@ class AppEnvironment {
|
|||
static Future<void> init() async {
|
||||
if (_isInitialized) return;
|
||||
cacheDir = (await getApplicationCacheDirectory()).path;
|
||||
supportDir = (await getApplicationSupportDirectory()).path;
|
||||
final privateSupportDir = (await getApplicationSupportDirectory()).path;
|
||||
if (Platform.isIOS) {
|
||||
final sharedSupportDir = await _runtimeChannel.invokeMethod<String>(
|
||||
'runtimeSupportDirectory',
|
||||
);
|
||||
if (sharedSupportDir == null || sharedSupportDir.isEmpty) {
|
||||
throw StateError('The iOS runtime App Group is unavailable.');
|
||||
}
|
||||
await _migrateToSharedSupportDirectory(
|
||||
Directory(privateSupportDir),
|
||||
Directory(sharedSupportDir),
|
||||
);
|
||||
supportDir = sharedSupportDir;
|
||||
} else {
|
||||
supportDir = privateSupportDir;
|
||||
}
|
||||
Log.init();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
static Future<void> _migrateToSharedSupportDirectory(
|
||||
Directory source,
|
||||
Directory destination,
|
||||
) async {
|
||||
await destination.create(recursive: true);
|
||||
final marker = File('${destination.path}/.runtime_storage_migrated_v1');
|
||||
if (await marker.exists() || !await source.exists()) return;
|
||||
|
||||
await for (final entity in source.list(followLinks: false)) {
|
||||
await _copyEntity(entity, destination.path);
|
||||
}
|
||||
await marker.writeAsString(
|
||||
DateTime.now().toUtc().toIso8601String(),
|
||||
flush: true,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> _copyEntity(
|
||||
FileSystemEntity entity,
|
||||
String destinationDirectory,
|
||||
) async {
|
||||
final name = entity.uri.pathSegments.where((value) => value.isNotEmpty).last;
|
||||
final destinationPath = '$destinationDirectory/$name';
|
||||
if (entity is Directory) {
|
||||
final destination = Directory(destinationPath);
|
||||
await destination.create(recursive: true);
|
||||
await for (final child in entity.list(followLinks: false)) {
|
||||
await _copyEntity(child, destination.path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (entity is! File) return;
|
||||
|
||||
final destination = File(destinationPath);
|
||||
if (await destination.exists()) return;
|
||||
final temporary = File('$destinationPath.migrating');
|
||||
await entity.copy(temporary.path);
|
||||
await temporary.rename(destination.path);
|
||||
}
|
||||
|
||||
static void initTesting({String? customCacheDir, String? customSupportDir}) {
|
||||
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
|
||||
supportDir = customSupportDir ?? '/tmp/twonly_support';
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
|||
import 'package:twonly/src/services/memories/memories.service.dart';
|
||||
import 'package:twonly/src/services/migrations.service.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/native.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||
|
|
@ -102,6 +103,7 @@ void main() async {
|
|||
var storageError = await twonlyMinimumInitialization();
|
||||
await FcmNotificationService.initStartup();
|
||||
await setupPushNotification();
|
||||
NativeNotificationService.init();
|
||||
|
||||
var userExists = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,73 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
/// Matches ANSI escape sequences (CSI sequences, caret notation like ^[[3m or \^[[3m).
|
||||
final _ansiRegex = RegExp(r'(?:\x1B|\\?\^\[)\[[0-?]*[ -/]*[@-~]');
|
||||
|
||||
/// Matches the plain `ShortEventFormatter` output from `rust/src/log.rs`:
|
||||
/// `HH:MM:SS LEVEL dir/file.rs:12 <fields> <message>`.
|
||||
final _rustLogLine = RegExp(
|
||||
r'^\d{2}:\d{2}:\d{2} (TRACE|DEBUG|INFO|WARN|ERROR) +(\S+:\d+) ?(.*)$',
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
/// Rust levels mapped onto the `logging` levels the Dart side already prints.
|
||||
/// `SHOUT` is what [Log.error] uses, so a Rust `ERROR` reads the same as a Dart
|
||||
/// one.
|
||||
const Map<String, Level> _levels = {
|
||||
'TRACE': Level.FINEST,
|
||||
'DEBUG': Level.FINE,
|
||||
'INFO': Level.INFO,
|
||||
'WARN': Level.WARNING,
|
||||
'ERROR': Level.SHOUT,
|
||||
};
|
||||
|
||||
class LoggingCallbacks {
|
||||
static Future<RustStreamSink<String>> getStreamSink() async {
|
||||
final dartLogSink = RustStreamSink<String>();
|
||||
|
||||
// `stream` throws until flutter_rust_bridge has serialized the sink for
|
||||
// Rust, which only happens once this function has returned. Poll for it
|
||||
// instead of racing; buffered events are replayed on the first listen.
|
||||
var attempts = 0;
|
||||
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
attempts++;
|
||||
try {
|
||||
dartLogSink.stream.listen(
|
||||
(log) {
|
||||
if (log.contains('INFO ')) {
|
||||
Log.info(log.split('INFO ')[1]);
|
||||
} else if (log.contains('DEBUG ')) {
|
||||
Log.info(log.split('DEBUG ')[1]);
|
||||
} else if (kDebugMode &&
|
||||
!Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
// ignore: avoid_print
|
||||
print(log);
|
||||
}
|
||||
},
|
||||
);
|
||||
dartLogSink.stream.listen(_handleRustLog);
|
||||
timer.cancel();
|
||||
} catch (e) {
|
||||
// stream not yet initialized
|
||||
} catch (_) {
|
||||
// Stream not yet initialized.
|
||||
if (attempts >= 100) {
|
||||
timer.cancel();
|
||||
Log.warn('Rust log sink never became available.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return dartLogSink;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void handleRustLog(String log) => _handleRustLog(log);
|
||||
|
||||
static void _handleRustLog(String log) {
|
||||
final sanitizedLog = log.replaceAll(_ansiRegex, '');
|
||||
final match = _rustLogLine.firstMatch(sanitizedLog);
|
||||
if (match == null) {
|
||||
// Not a formatted event (panic output, a continuation line, ...).
|
||||
Log.warn(sanitizedLog);
|
||||
return;
|
||||
}
|
||||
|
||||
// The level and the `file.rs:line` origin belong in the record itself, not
|
||||
// repeated inside the message: the Dart call site here says nothing useful.
|
||||
Log.forward(
|
||||
level: _levels[match.group(1)] ?? Level.INFO,
|
||||
source: match.group(2)!,
|
||||
messageInput: match.group(3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ class SecureStorageKeys {
|
|||
static const String userData = 'userData';
|
||||
|
||||
// Not required for backup...
|
||||
static const String receivingPushKeys = 'push_keys_receiving';
|
||||
static const String sendingPushKeys = 'push_keys_sending';
|
||||
static const String lastFcmMessageTimestamp = 'last_fcm_message_timestamp';
|
||||
static const String lastServerMessageTimestamp =
|
||||
'last_server_message_timestamp';
|
||||
}
|
||||
|
|
|
|||
38
lib/src/database/rust_change_notifier.dart
Normal file
38
lib/src/database/rust_change_notifier.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/app_database.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
/// Bridges Rust-side commits into Drift's stream query invalidation.
|
||||
///
|
||||
/// Rust owns the SQLite connection. Statements Drift issues go through
|
||||
/// `openRustAppDatabase` and invalidate its query streams as usual, but every
|
||||
/// write Rust performs on its own bypasses that executor entirely — Drift never
|
||||
/// learns about it, so `watch()` keeps serving stale rows and the UI does not
|
||||
/// update until something else happens to touch the same table.
|
||||
///
|
||||
/// Rust already broadcasts the tables it commits to (`notify_committed`); this
|
||||
/// forwards those batches into [GeneratedDatabase.notifyUpdates].
|
||||
StreamSubscription<List<String>> listenToRustDatabaseChanges(
|
||||
GeneratedDatabase db,
|
||||
) {
|
||||
return RustAppDatabase.changes().listen(
|
||||
(tables) {
|
||||
// An empty batch means Rust could not tell us precisely what changed
|
||||
// (fresh subscription, or dropped notifications). Invalidate everything
|
||||
// rather than leaving the UI stale.
|
||||
final updates = tables.isEmpty
|
||||
? db.allTables.map(TableUpdate.onTable).toSet()
|
||||
: tables.map(TableUpdate.new).toSet();
|
||||
db.notifyUpdates(updates);
|
||||
},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
Log.error(
|
||||
'Rust database change stream failed',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
|
|
@ -9,6 +11,7 @@ import 'package:twonly/src/database/daos/reactions.dao.dart';
|
|||
import 'package:twonly/src/database/daos/receipts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||
import 'package:twonly/src/database/rust_change_notifier.dart';
|
||||
import 'package:twonly/src/database/rust_query_executor.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
|
|
@ -64,14 +67,27 @@ part 'twonly.db.g.dart';
|
|||
],
|
||||
)
|
||||
class TwonlyDB extends _$TwonlyDB {
|
||||
TwonlyDB([QueryExecutor? e])
|
||||
: super(
|
||||
e ?? openRustAppDatabase(),
|
||||
);
|
||||
TwonlyDB([QueryExecutor? e]) : super(e ?? openRustAppDatabase()) {
|
||||
// Only the Rust-backed connection needs external change notifications.
|
||||
// An explicit executor is a plain Drift-owned database (tests, and the
|
||||
// legacy import in main.dart), where Drift already sees every write.
|
||||
if (e == null) {
|
||||
_rustChanges = listenToRustDatabaseChanges(this);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: matching_super_parameters
|
||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
StreamSubscription<List<String>>? _rustChanges;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _rustChanges?.cancel();
|
||||
_rustChanges = null;
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 25;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ class TextMessage extends $pb.GeneratedMessage {
|
|||
factory TextMessage({
|
||||
$fixnum.Int64? userId,
|
||||
$core.List<$core.int>? body,
|
||||
$core.List<$core.int>? pushData,
|
||||
$core.bool? wakeReceiver,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
if (body != null) result.body = body;
|
||||
if (pushData != null) result.pushData = pushData;
|
||||
if (wakeReceiver != null) result.wakeReceiver = wakeReceiver;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -46,8 +46,7 @@ class TextMessage extends $pb.GeneratedMessage {
|
|||
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
||||
..a<$core.List<$core.int>>(
|
||||
2, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY)
|
||||
..aOB(4, _omitFieldNames ? '' : 'wakeReceiver')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -87,14 +86,14 @@ class TextMessage extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(2)
|
||||
void clearBody() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get pushData => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set pushData($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasPushData() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearPushData() => $_clearField(3);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool get wakeReceiver => $_getBF(2);
|
||||
@$pb.TagNumber(4)
|
||||
set wakeReceiver($core.bool value) => $_setBool(2, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasWakeReceiver() => $_has(2);
|
||||
@$pb.TagNumber(4)
|
||||
void clearWakeReceiver() => $_clearField(4);
|
||||
}
|
||||
|
||||
class UploadRequest extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -21,25 +21,17 @@ const TextMessage$json = {
|
|||
'2': [
|
||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||
{'1': 'body', '3': 2, '4': 1, '5': 12, '10': 'body'},
|
||||
{
|
||||
'1': 'push_data',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 12,
|
||||
'9': 0,
|
||||
'10': 'pushData',
|
||||
'17': true
|
||||
},
|
||||
{'1': 'wake_receiver', '3': 4, '4': 1, '5': 8, '10': 'wakeReceiver'},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_push_data'},
|
||||
'9': [
|
||||
{'1': 3, '2': 4},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `TextMessage`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List textMessageDescriptor = $convert.base64Decode(
|
||||
'CgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgCIAEoDFIEYm'
|
||||
'9keRIgCglwdXNoX2RhdGEYAyABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YQ==');
|
||||
'9keRIjCg13YWtlX3JlY2VpdmVyGAQgASgIUgx3YWtlUmVjZWl2ZXJKBAgDEAQ=');
|
||||
|
||||
@$core.Deprecated('Use uploadRequestDescriptor instead')
|
||||
const UploadRequest$json = {
|
||||
|
|
|
|||
|
|
@ -1140,98 +1140,6 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
|||
void clearDisplayName() => $_clearField(4);
|
||||
}
|
||||
|
||||
class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_PushKeys({
|
||||
EncryptedContent_PushKeys_Type? type,
|
||||
$fixnum.Int64? keyId,
|
||||
$core.List<$core.int>? key,
|
||||
$fixnum.Int64? createdAt,
|
||||
}) {
|
||||
final result = create();
|
||||
if (type != null) result.type = type;
|
||||
if (keyId != null) result.keyId = keyId;
|
||||
if (key != null) result.key = key;
|
||||
if (createdAt != null) result.createdAt = createdAt;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedContent_PushKeys._();
|
||||
|
||||
factory EncryptedContent_PushKeys.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedContent_PushKeys.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedContent.PushKeys',
|
||||
createEmptyInstance: create)
|
||||
..aE<EncryptedContent_PushKeys_Type>(1, _omitFieldNames ? '' : 'type',
|
||||
enumValues: EncryptedContent_PushKeys_Type.values)
|
||||
..aInt64(2, _omitFieldNames ? '' : 'keyId')
|
||||
..a<$core.List<$core.int>>(
|
||||
3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY)
|
||||
..aInt64(4, _omitFieldNames ? '' : 'createdAt')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PushKeys clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_PushKeys copyWith(
|
||||
void Function(EncryptedContent_PushKeys) updates) =>
|
||||
super.copyWith((message) => updates(message as EncryptedContent_PushKeys))
|
||||
as EncryptedContent_PushKeys;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PushKeys create() => EncryptedContent_PushKeys._();
|
||||
@$core.override
|
||||
EncryptedContent_PushKeys createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_PushKeys getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_PushKeys>(create);
|
||||
static EncryptedContent_PushKeys? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
EncryptedContent_PushKeys_Type get type => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set type(EncryptedContent_PushKeys_Type value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasType() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearType() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$fixnum.Int64 get keyId => $_getI64(1);
|
||||
@$pb.TagNumber(2)
|
||||
set keyId($fixnum.Int64 value) => $_setInt64(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasKeyId() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearKeyId() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get key => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set key($core.List<$core.int> value) => $_setBytes(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasKey() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearKey() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$fixnum.Int64 get createdAt => $_getI64(3);
|
||||
@$pb.TagNumber(4)
|
||||
set createdAt($fixnum.Int64 value) => $_setInt64(3, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasCreatedAt() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearCreatedAt() => $_clearField(4);
|
||||
}
|
||||
|
||||
class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_FlameSync({
|
||||
$fixnum.Int64? flameCounter,
|
||||
|
|
@ -1724,7 +1632,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
EncryptedContent_ContactUpdate? contactUpdate,
|
||||
EncryptedContent_ContactRequest? contactRequest,
|
||||
EncryptedContent_FlameSync? flameSync,
|
||||
EncryptedContent_PushKeys? pushKeys,
|
||||
EncryptedContent_Reaction? reaction,
|
||||
EncryptedContent_TextMessage? textMessage,
|
||||
EncryptedContent_GroupCreate? groupCreate,
|
||||
|
|
@ -1754,7 +1661,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
if (contactUpdate != null) result.contactUpdate = contactUpdate;
|
||||
if (contactRequest != null) result.contactRequest = contactRequest;
|
||||
if (flameSync != null) result.flameSync = flameSync;
|
||||
if (pushKeys != null) result.pushKeys = pushKeys;
|
||||
if (reaction != null) result.reaction = reaction;
|
||||
if (textMessage != null) result.textMessage = textMessage;
|
||||
if (groupCreate != null) result.groupCreate = groupCreate;
|
||||
|
|
@ -1813,8 +1719,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
subBuilder: EncryptedContent_ContactRequest.create)
|
||||
..aOM<EncryptedContent_FlameSync>(10, _omitFieldNames ? '' : 'flameSync',
|
||||
subBuilder: EncryptedContent_FlameSync.create)
|
||||
..aOM<EncryptedContent_PushKeys>(11, _omitFieldNames ? '' : 'pushKeys',
|
||||
subBuilder: EncryptedContent_PushKeys.create)
|
||||
..aOM<EncryptedContent_Reaction>(12, _omitFieldNames ? '' : 'reaction',
|
||||
subBuilder: EncryptedContent_Reaction.create)
|
||||
..aOM<EncryptedContent_TextMessage>(
|
||||
|
|
@ -1976,208 +1880,197 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(10)
|
||||
EncryptedContent_FlameSync ensureFlameSync() => $_ensure(8);
|
||||
|
||||
@$pb.TagNumber(11)
|
||||
EncryptedContent_PushKeys get pushKeys => $_getN(9);
|
||||
@$pb.TagNumber(11)
|
||||
set pushKeys(EncryptedContent_PushKeys value) => $_setField(11, value);
|
||||
@$pb.TagNumber(11)
|
||||
$core.bool hasPushKeys() => $_has(9);
|
||||
@$pb.TagNumber(11)
|
||||
void clearPushKeys() => $_clearField(11);
|
||||
@$pb.TagNumber(11)
|
||||
EncryptedContent_PushKeys ensurePushKeys() => $_ensure(9);
|
||||
|
||||
@$pb.TagNumber(12)
|
||||
EncryptedContent_Reaction get reaction => $_getN(10);
|
||||
EncryptedContent_Reaction get reaction => $_getN(9);
|
||||
@$pb.TagNumber(12)
|
||||
set reaction(EncryptedContent_Reaction value) => $_setField(12, value);
|
||||
@$pb.TagNumber(12)
|
||||
$core.bool hasReaction() => $_has(10);
|
||||
$core.bool hasReaction() => $_has(9);
|
||||
@$pb.TagNumber(12)
|
||||
void clearReaction() => $_clearField(12);
|
||||
@$pb.TagNumber(12)
|
||||
EncryptedContent_Reaction ensureReaction() => $_ensure(10);
|
||||
EncryptedContent_Reaction ensureReaction() => $_ensure(9);
|
||||
|
||||
@$pb.TagNumber(13)
|
||||
EncryptedContent_TextMessage get textMessage => $_getN(11);
|
||||
EncryptedContent_TextMessage get textMessage => $_getN(10);
|
||||
@$pb.TagNumber(13)
|
||||
set textMessage(EncryptedContent_TextMessage value) => $_setField(13, value);
|
||||
@$pb.TagNumber(13)
|
||||
$core.bool hasTextMessage() => $_has(11);
|
||||
$core.bool hasTextMessage() => $_has(10);
|
||||
@$pb.TagNumber(13)
|
||||
void clearTextMessage() => $_clearField(13);
|
||||
@$pb.TagNumber(13)
|
||||
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11);
|
||||
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(10);
|
||||
|
||||
@$pb.TagNumber(14)
|
||||
EncryptedContent_GroupCreate get groupCreate => $_getN(12);
|
||||
EncryptedContent_GroupCreate get groupCreate => $_getN(11);
|
||||
@$pb.TagNumber(14)
|
||||
set groupCreate(EncryptedContent_GroupCreate value) => $_setField(14, value);
|
||||
@$pb.TagNumber(14)
|
||||
$core.bool hasGroupCreate() => $_has(12);
|
||||
$core.bool hasGroupCreate() => $_has(11);
|
||||
@$pb.TagNumber(14)
|
||||
void clearGroupCreate() => $_clearField(14);
|
||||
@$pb.TagNumber(14)
|
||||
EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(12);
|
||||
EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(11);
|
||||
|
||||
@$pb.TagNumber(15)
|
||||
EncryptedContent_GroupJoin get groupJoin => $_getN(13);
|
||||
EncryptedContent_GroupJoin get groupJoin => $_getN(12);
|
||||
@$pb.TagNumber(15)
|
||||
set groupJoin(EncryptedContent_GroupJoin value) => $_setField(15, value);
|
||||
@$pb.TagNumber(15)
|
||||
$core.bool hasGroupJoin() => $_has(13);
|
||||
$core.bool hasGroupJoin() => $_has(12);
|
||||
@$pb.TagNumber(15)
|
||||
void clearGroupJoin() => $_clearField(15);
|
||||
@$pb.TagNumber(15)
|
||||
EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(13);
|
||||
EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(12);
|
||||
|
||||
@$pb.TagNumber(16)
|
||||
EncryptedContent_GroupUpdate get groupUpdate => $_getN(14);
|
||||
EncryptedContent_GroupUpdate get groupUpdate => $_getN(13);
|
||||
@$pb.TagNumber(16)
|
||||
set groupUpdate(EncryptedContent_GroupUpdate value) => $_setField(16, value);
|
||||
@$pb.TagNumber(16)
|
||||
$core.bool hasGroupUpdate() => $_has(14);
|
||||
$core.bool hasGroupUpdate() => $_has(13);
|
||||
@$pb.TagNumber(16)
|
||||
void clearGroupUpdate() => $_clearField(16);
|
||||
@$pb.TagNumber(16)
|
||||
EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14);
|
||||
EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(13);
|
||||
|
||||
@$pb.TagNumber(17)
|
||||
EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(15);
|
||||
EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(14);
|
||||
@$pb.TagNumber(17)
|
||||
set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey value) =>
|
||||
$_setField(17, value);
|
||||
@$pb.TagNumber(17)
|
||||
$core.bool hasResendGroupPublicKey() => $_has(15);
|
||||
$core.bool hasResendGroupPublicKey() => $_has(14);
|
||||
@$pb.TagNumber(17)
|
||||
void clearResendGroupPublicKey() => $_clearField(17);
|
||||
@$pb.TagNumber(17)
|
||||
EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() =>
|
||||
$_ensure(15);
|
||||
$_ensure(14);
|
||||
|
||||
@$pb.TagNumber(18)
|
||||
EncryptedContent_ErrorMessages get errorMessages => $_getN(16);
|
||||
EncryptedContent_ErrorMessages get errorMessages => $_getN(15);
|
||||
@$pb.TagNumber(18)
|
||||
set errorMessages(EncryptedContent_ErrorMessages value) =>
|
||||
$_setField(18, value);
|
||||
@$pb.TagNumber(18)
|
||||
$core.bool hasErrorMessages() => $_has(16);
|
||||
$core.bool hasErrorMessages() => $_has(15);
|
||||
@$pb.TagNumber(18)
|
||||
void clearErrorMessages() => $_clearField(18);
|
||||
@$pb.TagNumber(18)
|
||||
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16);
|
||||
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(15);
|
||||
|
||||
@$pb.TagNumber(19)
|
||||
EncryptedContent_AdditionalDataMessage get additionalDataMessage =>
|
||||
$_getN(17);
|
||||
$_getN(16);
|
||||
@$pb.TagNumber(19)
|
||||
set additionalDataMessage(EncryptedContent_AdditionalDataMessage value) =>
|
||||
$_setField(19, value);
|
||||
@$pb.TagNumber(19)
|
||||
$core.bool hasAdditionalDataMessage() => $_has(17);
|
||||
$core.bool hasAdditionalDataMessage() => $_has(16);
|
||||
@$pb.TagNumber(19)
|
||||
void clearAdditionalDataMessage() => $_clearField(19);
|
||||
@$pb.TagNumber(19)
|
||||
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
|
||||
$_ensure(17);
|
||||
$_ensure(16);
|
||||
|
||||
@$pb.TagNumber(20)
|
||||
EncryptedContent_TypingIndicator get typingIndicator => $_getN(18);
|
||||
EncryptedContent_TypingIndicator get typingIndicator => $_getN(17);
|
||||
@$pb.TagNumber(20)
|
||||
set typingIndicator(EncryptedContent_TypingIndicator value) =>
|
||||
$_setField(20, value);
|
||||
@$pb.TagNumber(20)
|
||||
$core.bool hasTypingIndicator() => $_has(18);
|
||||
$core.bool hasTypingIndicator() => $_has(17);
|
||||
@$pb.TagNumber(20)
|
||||
void clearTypingIndicator() => $_clearField(20);
|
||||
@$pb.TagNumber(20)
|
||||
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18);
|
||||
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(17);
|
||||
|
||||
@$pb.TagNumber(21)
|
||||
$core.List<$core.int> get senderUserDiscoveryVersion => $_getN(19);
|
||||
$core.List<$core.int> get senderUserDiscoveryVersion => $_getN(18);
|
||||
@$pb.TagNumber(21)
|
||||
set senderUserDiscoveryVersion($core.List<$core.int> value) =>
|
||||
$_setBytes(19, value);
|
||||
$_setBytes(18, value);
|
||||
@$pb.TagNumber(21)
|
||||
$core.bool hasSenderUserDiscoveryVersion() => $_has(19);
|
||||
$core.bool hasSenderUserDiscoveryVersion() => $_has(18);
|
||||
@$pb.TagNumber(21)
|
||||
void clearSenderUserDiscoveryVersion() => $_clearField(21);
|
||||
|
||||
@$pb.TagNumber(22)
|
||||
EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(20);
|
||||
EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(19);
|
||||
@$pb.TagNumber(22)
|
||||
set userDiscoveryRequest(EncryptedContent_UserDiscoveryRequest value) =>
|
||||
$_setField(22, value);
|
||||
@$pb.TagNumber(22)
|
||||
$core.bool hasUserDiscoveryRequest() => $_has(20);
|
||||
$core.bool hasUserDiscoveryRequest() => $_has(19);
|
||||
@$pb.TagNumber(22)
|
||||
void clearUserDiscoveryRequest() => $_clearField(22);
|
||||
@$pb.TagNumber(22)
|
||||
EncryptedContent_UserDiscoveryRequest ensureUserDiscoveryRequest() =>
|
||||
$_ensure(20);
|
||||
$_ensure(19);
|
||||
|
||||
@$pb.TagNumber(23)
|
||||
EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(21);
|
||||
EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(20);
|
||||
@$pb.TagNumber(23)
|
||||
set userDiscoveryUpdate(EncryptedContent_UserDiscoveryUpdate value) =>
|
||||
$_setField(23, value);
|
||||
@$pb.TagNumber(23)
|
||||
$core.bool hasUserDiscoveryUpdate() => $_has(21);
|
||||
$core.bool hasUserDiscoveryUpdate() => $_has(20);
|
||||
@$pb.TagNumber(23)
|
||||
void clearUserDiscoveryUpdate() => $_clearField(23);
|
||||
@$pb.TagNumber(23)
|
||||
EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() =>
|
||||
$_ensure(21);
|
||||
$_ensure(20);
|
||||
|
||||
@$pb.TagNumber(24)
|
||||
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(22);
|
||||
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(21);
|
||||
@$pb.TagNumber(24)
|
||||
set keyVerificationProof(EncryptedContent_KeyVerificationProof value) =>
|
||||
$_setField(24, value);
|
||||
@$pb.TagNumber(24)
|
||||
$core.bool hasKeyVerificationProof() => $_has(22);
|
||||
$core.bool hasKeyVerificationProof() => $_has(21);
|
||||
@$pb.TagNumber(24)
|
||||
void clearKeyVerificationProof() => $_clearField(24);
|
||||
@$pb.TagNumber(24)
|
||||
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
|
||||
$_ensure(22);
|
||||
$_ensure(21);
|
||||
|
||||
@$pb.TagNumber(25)
|
||||
$core.bool get askForFriendPromotions => $_getBF(23);
|
||||
$core.bool get askForFriendPromotions => $_getBF(22);
|
||||
@$pb.TagNumber(25)
|
||||
set askForFriendPromotions($core.bool value) => $_setBool(23, value);
|
||||
set askForFriendPromotions($core.bool value) => $_setBool(22, value);
|
||||
@$pb.TagNumber(25)
|
||||
$core.bool hasAskForFriendPromotions() => $_has(23);
|
||||
$core.bool hasAskForFriendPromotions() => $_has(22);
|
||||
@$pb.TagNumber(25)
|
||||
void clearAskForFriendPromotions() => $_clearField(25);
|
||||
|
||||
@$pb.TagNumber(26)
|
||||
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(24);
|
||||
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(23);
|
||||
@$pb.TagNumber(26)
|
||||
set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) =>
|
||||
$_setField(26, value);
|
||||
@$pb.TagNumber(26)
|
||||
$core.bool hasPasswordlessRecovery() => $_has(24);
|
||||
$core.bool hasPasswordlessRecovery() => $_has(23);
|
||||
@$pb.TagNumber(26)
|
||||
void clearPasswordlessRecovery() => $_clearField(26);
|
||||
@$pb.TagNumber(26)
|
||||
EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() =>
|
||||
$_ensure(24);
|
||||
$_ensure(23);
|
||||
|
||||
@$pb.TagNumber(27)
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||
get passwordlessRecoveryHeartbeat => $_getN(25);
|
||||
get passwordlessRecoveryHeartbeat => $_getN(24);
|
||||
@$pb.TagNumber(27)
|
||||
set passwordlessRecoveryHeartbeat(
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat value) =>
|
||||
$_setField(27, value);
|
||||
@$pb.TagNumber(27)
|
||||
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(25);
|
||||
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(24);
|
||||
@$pb.TagNumber(27)
|
||||
void clearPasswordlessRecoveryHeartbeat() => $_clearField(27);
|
||||
@$pb.TagNumber(27)
|
||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||
ensurePasswordlessRecoveryHeartbeat() => $_ensure(25);
|
||||
ensurePasswordlessRecoveryHeartbeat() => $_ensure(24);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -169,25 +169,5 @@ class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum {
|
|||
const EncryptedContent_ContactUpdate_Type._(super.value, super.name);
|
||||
}
|
||||
|
||||
class EncryptedContent_PushKeys_Type extends $pb.ProtobufEnum {
|
||||
static const EncryptedContent_PushKeys_Type REQUEST =
|
||||
EncryptedContent_PushKeys_Type._(0, _omitEnumNames ? '' : 'REQUEST');
|
||||
static const EncryptedContent_PushKeys_Type UPDATE =
|
||||
EncryptedContent_PushKeys_Type._(1, _omitEnumNames ? '' : 'UPDATE');
|
||||
|
||||
static const $core.List<EncryptedContent_PushKeys_Type> values =
|
||||
<EncryptedContent_PushKeys_Type>[
|
||||
REQUEST,
|
||||
UPDATE,
|
||||
];
|
||||
|
||||
static final $core.List<EncryptedContent_PushKeys_Type?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 1);
|
||||
static EncryptedContent_PushKeys_Type? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const EncryptedContent_PushKeys_Type._(super.value, super.name);
|
||||
}
|
||||
|
||||
const $core.bool _omitEnumNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
|
|
|
|||
|
|
@ -124,23 +124,13 @@ const EncryptedContent$json = {
|
|||
'10': 'flameSync',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'push_keys',
|
||||
'3': 11,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PushKeys',
|
||||
'9': 11,
|
||||
'10': 'pushKeys',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'reaction',
|
||||
'3': 12,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.Reaction',
|
||||
'9': 12,
|
||||
'9': 11,
|
||||
'10': 'reaction',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -150,7 +140,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.TextMessage',
|
||||
'9': 13,
|
||||
'9': 12,
|
||||
'10': 'textMessage',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -160,7 +150,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupCreate',
|
||||
'9': 14,
|
||||
'9': 13,
|
||||
'10': 'groupCreate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -170,7 +160,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupJoin',
|
||||
'9': 15,
|
||||
'9': 14,
|
||||
'10': 'groupJoin',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -180,7 +170,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupUpdate',
|
||||
'9': 16,
|
||||
'9': 15,
|
||||
'10': 'groupUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -190,7 +180,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ResendGroupPublicKey',
|
||||
'9': 17,
|
||||
'9': 16,
|
||||
'10': 'resendGroupPublicKey',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -200,7 +190,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ErrorMessages',
|
||||
'9': 18,
|
||||
'9': 17,
|
||||
'10': 'errorMessages',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -210,7 +200,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.AdditionalDataMessage',
|
||||
'9': 19,
|
||||
'9': 18,
|
||||
'10': 'additionalDataMessage',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -220,7 +210,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.TypingIndicator',
|
||||
'9': 20,
|
||||
'9': 19,
|
||||
'10': 'typingIndicator',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -230,7 +220,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.UserDiscoveryRequest',
|
||||
'9': 21,
|
||||
'9': 20,
|
||||
'10': 'userDiscoveryRequest',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -240,7 +230,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
||||
'9': 22,
|
||||
'9': 21,
|
||||
'10': 'userDiscoveryUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -250,7 +240,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.KeyVerificationProof',
|
||||
'9': 23,
|
||||
'9': 22,
|
||||
'10': 'keyVerificationProof',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -260,7 +250,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecovery',
|
||||
'9': 24,
|
||||
'9': 23,
|
||||
'10': 'passwordlessRecovery',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -270,7 +260,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||
'9': 25,
|
||||
'9': 24,
|
||||
'10': 'passwordlessRecoveryHeartbeat',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -289,7 +279,6 @@ const EncryptedContent$json = {
|
|||
EncryptedContent_MediaUpdate$json,
|
||||
EncryptedContent_ContactRequest$json,
|
||||
EncryptedContent_ContactUpdate$json,
|
||||
EncryptedContent_PushKeys$json,
|
||||
EncryptedContent_FlameSync$json,
|
||||
EncryptedContent_TypingIndicator$json,
|
||||
EncryptedContent_UserDiscoveryRequest$json,
|
||||
|
|
@ -310,7 +299,6 @@ const EncryptedContent$json = {
|
|||
{'1': '_contact_update'},
|
||||
{'1': '_contact_request'},
|
||||
{'1': '_flame_sync'},
|
||||
{'1': '_push_keys'},
|
||||
{'1': '_reaction'},
|
||||
{'1': '_text_message'},
|
||||
{'1': '_group_create'},
|
||||
|
|
@ -757,47 +745,6 @@ const EncryptedContent_ContactUpdate_Type$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_PushKeys$json = {
|
||||
'1': 'PushKeys',
|
||||
'2': [
|
||||
{
|
||||
'1': 'type',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.EncryptedContent.PushKeys.Type',
|
||||
'10': 'type'
|
||||
},
|
||||
{'1': 'key_id', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true},
|
||||
{'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true},
|
||||
{
|
||||
'1': 'created_at',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'9': 2,
|
||||
'10': 'createdAt',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'4': [EncryptedContent_PushKeys_Type$json],
|
||||
'8': [
|
||||
{'1': '_key_id'},
|
||||
{'1': '_key'},
|
||||
{'1': '_created_at'},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_PushKeys_Type$json = {
|
||||
'1': 'Type',
|
||||
'2': [
|
||||
{'1': 'REQUEST', '2': 0},
|
||||
{'1': 'UPDATE', '2': 1},
|
||||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_FlameSync$json = {
|
||||
'1': 'FlameSync',
|
||||
|
|
@ -891,107 +838,102 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'91cGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGVICFINY29udGFj'
|
||||
'dFVwZGF0ZYgBARJOCg9jb250YWN0X3JlcXVlc3QYCSABKAsyIC5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'NvbnRhY3RSZXF1ZXN0SAlSDmNvbnRhY3RSZXF1ZXN0iAEBEj8KCmZsYW1lX3N5bmMYCiABKAsy'
|
||||
'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESPAoJcHVzaF9rZX'
|
||||
'lzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0gLUghwdXNoS2V5c4gBARI7Cghy'
|
||||
'ZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVhY3Rpb25IDFIIcmVhY3Rpb26IAQ'
|
||||
'ESRQoMdGV4dF9tZXNzYWdlGA0gASgLMh0uRW5jcnlwdGVkQ29udGVudC5UZXh0TWVzc2FnZUgN'
|
||||
'Ugt0ZXh0TWVzc2FnZYgBARJFCgxncm91cF9jcmVhdGUYDiABKAsyHS5FbmNyeXB0ZWRDb250ZW'
|
||||
'50Lkdyb3VwQ3JlYXRlSA5SC2dyb3VwQ3JlYXRliAEBEj8KCmdyb3VwX2pvaW4YDyABKAsyGy5F'
|
||||
'bmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgPUglncm91cEpvaW6IAQESRQoMZ3JvdXBfdXBkYX'
|
||||
'RlGBAgASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cFVwZGF0ZUgQUgtncm91cFVwZGF0ZYgB'
|
||||
'ARJiChdyZXNlbmRfZ3JvdXBfcHVibGljX2tleRgRIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUm'
|
||||
'VzZW5kR3JvdXBQdWJsaWNLZXlIEVIUcmVzZW5kR3JvdXBQdWJsaWNLZXmIAQESSwoOZXJyb3Jf'
|
||||
'bWVzc2FnZXMYEiABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXNIElINZXJyb3'
|
||||
'JNZXNzYWdlc4gBARJkChdhZGRpdGlvbmFsX2RhdGFfbWVzc2FnZRgTIAEoCzInLkVuY3J5cHRl'
|
||||
'ZENvbnRlbnQuQWRkaXRpb25hbERhdGFNZXNzYWdlSBNSFWFkZGl0aW9uYWxEYXRhTWVzc2FnZY'
|
||||
'gBARJRChB0eXBpbmdfaW5kaWNhdG9yGBQgASgLMiEuRW5jcnlwdGVkQ29udGVudC5UeXBpbmdJ'
|
||||
'bmRpY2F0b3JIFFIPdHlwaW5nSW5kaWNhdG9yiAEBEmEKFnVzZXJfZGlzY292ZXJ5X3JlcXVlc3'
|
||||
'QYFiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlVzZXJEaXNjb3ZlcnlSZXF1ZXN0SBVSFHVzZXJE'
|
||||
'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3'
|
||||
'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl'
|
||||
'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3'
|
||||
'b3JkbGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUm'
|
||||
'Vjb3ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292'
|
||||
'ZXJ5X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3'
|
||||
'ZlcnlIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQEalgIKDUVy'
|
||||
'cm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYW'
|
||||
'dlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2Vp'
|
||||
'cHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU'
|
||||
'5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lP'
|
||||
'Tl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQ'
|
||||
'oLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJs'
|
||||
'aWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3'
|
||||
'VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tl'
|
||||
'eRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3'
|
||||
'VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoT'
|
||||
'YWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZX'
|
||||
'dfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNz'
|
||||
'YWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
|
||||
'1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25h'
|
||||
'bWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGq8BCgtUZXh0TW'
|
||||
'Vzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRl'
|
||||
'eHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbW'
|
||||
'Vzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdGVfbWVzc2FnZV9p'
|
||||
'ZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg'
|
||||
'9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgD'
|
||||
'IAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbm'
|
||||
'FsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9u'
|
||||
'EioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAi'
|
||||
'ABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgK'
|
||||
'BHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZR'
|
||||
'IvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVs'
|
||||
'dGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZH'
|
||||
'MSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1w'
|
||||
'Ii0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3Nlbm'
|
||||
'Rlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgB'
|
||||
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANI'
|
||||
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYX'
|
||||
'Rpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0'
|
||||
'aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBAR'
|
||||
'IqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRp'
|
||||
'b25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKA'
|
||||
'xIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5'
|
||||
'cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdG'
|
||||
'lvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoF'
|
||||
'VklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0X2luX21pbGxpc2'
|
||||
'Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW5CEQoPX2VuY3J5'
|
||||
'cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9ub25jZUIaChhfYW'
|
||||
'RkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5F'
|
||||
'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2'
|
||||
'VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RP'
|
||||
'UkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYAS'
|
||||
'ABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlw'
|
||||
'ZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNvbnRhY3RVcGRhdG'
|
||||
'USOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0'
|
||||
'eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcmVzc2'
|
||||
'VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3BsYXlfbmFtZRgE'
|
||||
'IAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAU'
|
||||
'IYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZGlzcGxheV9uYW1l'
|
||||
'GtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy'
|
||||
'5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVID'
|
||||
'a2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUk'
|
||||
'VRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3JlYXRlZF9hdBqv'
|
||||
'AQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYX'
|
||||
'N0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8K'
|
||||
'C2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm'
|
||||
'9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBp'
|
||||
'bmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZX'
|
||||
'N0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2Nv'
|
||||
'dmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW'
|
||||
'9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3'
|
||||
'b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3Zlcn'
|
||||
'lTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyAB'
|
||||
'KANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZW'
|
||||
'NvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNf'
|
||||
'ZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2'
|
||||
'Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNz'
|
||||
'YWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdG'
|
||||
'VCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9y'
|
||||
'ZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW'
|
||||
'5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9y'
|
||||
'X21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYX'
|
||||
'RvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRl'
|
||||
'QhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29mQhgKFl9wYXNzd29yZGxlc3NfcmVjb3ZlcnlCIg'
|
||||
'ogX3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZWFydGJlYXQ=');
|
||||
'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESOwoIcmVhY3Rpb2'
|
||||
'4YDCABKAsyGi5FbmNyeXB0ZWRDb250ZW50LlJlYWN0aW9uSAtSCHJlYWN0aW9uiAEBEkUKDHRl'
|
||||
'eHRfbWVzc2FnZRgNIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIDFILdGV4dE'
|
||||
'1lc3NhZ2WIAQESRQoMZ3JvdXBfY3JlYXRlGA4gASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91'
|
||||
'cENyZWF0ZUgNUgtncm91cENyZWF0ZYgBARI/Cgpncm91cF9qb2luGA8gASgLMhsuRW5jcnlwdG'
|
||||
'VkQ29udGVudC5Hcm91cEpvaW5IDlIJZ3JvdXBKb2luiAEBEkUKDGdyb3VwX3VwZGF0ZRgQIAEo'
|
||||
'CzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBVcGRhdGVID1ILZ3JvdXBVcGRhdGWIAQESYgoXcm'
|
||||
'VzZW5kX2dyb3VwX3B1YmxpY19rZXkYESABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlJlc2VuZEdy'
|
||||
'b3VwUHVibGljS2V5SBBSFHJlc2VuZEdyb3VwUHVibGljS2V5iAEBEksKDmVycm9yX21lc3NhZ2'
|
||||
'VzGBIgASgLMh8uRW5jcnlwdGVkQ29udGVudC5FcnJvck1lc3NhZ2VzSBFSDWVycm9yTWVzc2Fn'
|
||||
'ZXOIAQESZAoXYWRkaXRpb25hbF9kYXRhX21lc3NhZ2UYEyABKAsyJy5FbmNyeXB0ZWRDb250ZW'
|
||||
'50LkFkZGl0aW9uYWxEYXRhTWVzc2FnZUgSUhVhZGRpdGlvbmFsRGF0YU1lc3NhZ2WIAQESUQoQ'
|
||||
'dHlwaW5nX2luZGljYXRvchgUIAEoCzIhLkVuY3J5cHRlZENvbnRlbnQuVHlwaW5nSW5kaWNhdG'
|
||||
'9ySBNSD3R5cGluZ0luZGljYXRvcogBARJhChZ1c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0GBYgASgL'
|
||||
'MiYuRW5jcnlwdGVkQ29udGVudC5Vc2VyRGlzY292ZXJ5UmVxdWVzdEgUUhR1c2VyRGlzY292ZX'
|
||||
'J5UmVxdWVzdIgBARJeChV1c2VyX2Rpc2NvdmVyeV91cGRhdGUYFyABKAsyJS5FbmNyeXB0ZWRD'
|
||||
'b250ZW50LlVzZXJEaXNjb3ZlcnlVcGRhdGVIFVITdXNlckRpc2NvdmVyeVVwZGF0ZYgBARJhCh'
|
||||
'ZrZXlfdmVyaWZpY2F0aW9uX3Byb29mGBggASgLMiYuRW5jcnlwdGVkQ29udGVudC5LZXlWZXJp'
|
||||
'ZmljYXRpb25Qcm9vZkgWUhRrZXlWZXJpZmljYXRpb25Qcm9vZogBARJgChVwYXNzd29yZGxlc3'
|
||||
'NfcmVjb3ZlcnkYGiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5'
|
||||
'SBdSFHBhc3N3b3JkbGVzc1JlY292ZXJ5iAEBEnwKH3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZW'
|
||||
'FydGJlYXQYGyABKAsyLy5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5SGVh'
|
||||
'cnRiZWF0SBhSHXBhc3N3b3JkbGVzc1JlY292ZXJ5SGVhcnRiZWF0iAEBGpYCCg1FcnJvck1lc3'
|
||||
'NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlw'
|
||||
'ZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQinA'
|
||||
'EKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF9SRVFV'
|
||||
'RVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05fT1VUX0'
|
||||
'9GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0dyb3Vw'
|
||||
'Q3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGljX2tleR'
|
||||
'gEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE5hbWWI'
|
||||
'AQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkYASABKA'
|
||||
'xSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cFVwZGF0'
|
||||
'ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2FmZmVjdG'
|
||||
'VkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2dyb3Vw'
|
||||
'X25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2FnZXNfYW'
|
||||
'Z0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNl'
|
||||
'Y29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1lQikKJ1'
|
||||
'9uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqvAQoLVGV4dE1lc3NhZ2US'
|
||||
'KgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgAS'
|
||||
'gJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2Vf'
|
||||
'aWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWQazgEKFU'
|
||||
'FkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVy'
|
||||
'TWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBH'
|
||||
'R5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3Nh'
|
||||
'Z2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpkCghSZWFjdGlvbhIqChF0YX'
|
||||
'JnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVl'
|
||||
'bW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGA'
|
||||
'EgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLwoRc2Vu'
|
||||
'ZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEBEj0KG211bHRpcGxlX3'
|
||||
'RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRl'
|
||||
'eHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcCItCgRUeX'
|
||||
'BlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhQKEl9zZW5kZXJfbWVz'
|
||||
'c2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3'
|
||||
'NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5U'
|
||||
'eXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3'
|
||||
'BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3JlcXVpcmVzX2F1dGhlbnRpY2F0aW9uGAQg'
|
||||
'ASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW'
|
||||
'1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAVIOcXVvdGVNZXNzYWdlSWSIAQESKgoOZG93'
|
||||
'bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRUb2tlbogBARIqCg5lbmNyeXB0aW9uX2tleR'
|
||||
'gIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDmVuY3J5cHRpb25fbWFjGAkgASgMSARSDWVu'
|
||||
'Y3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9ub25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm'
|
||||
'9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1l'
|
||||
'c3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEA'
|
||||
'ISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNC'
|
||||
'EwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2'
|
||||
'tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9u'
|
||||
'YWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdG'
|
||||
'VkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIg'
|
||||
'ASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEh'
|
||||
'QKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUu'
|
||||
'RW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUk'
|
||||
'VRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGqQCCg1Db250YWN0VXBkYXRlEjgKBHR5'
|
||||
'cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3Ch'
|
||||
'VhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIf'
|
||||
'Cgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCABKAlIAl'
|
||||
'ILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2'
|
||||
'YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVCDwoNX2Rpc3BsYXlfbmFtZRqvAQoJRm'
|
||||
'xhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYXN0X2Zs'
|
||||
'YW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3'
|
||||
'RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm9yY2VV'
|
||||
'cGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBpbmcSHQ'
|
||||
'oKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZXN0EicK'
|
||||
'D2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2NvdmVyeV'
|
||||
'VwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW9uUHJv'
|
||||
'b2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3b3JkTG'
|
||||
'Vzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3ZlcnlTZWNy'
|
||||
'ZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyABKANSCX'
|
||||
'RocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZWNvdmVy'
|
||||
'eUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNfZGlyZW'
|
||||
'N0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2Rpc2Nv'
|
||||
'dmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNzYWdlX3'
|
||||
'VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdGVCEgoQ'
|
||||
'X2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcmVhY3Rpb25CDwoNX3RleHRfbW'
|
||||
'Vzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2luQg8KDV9ncm91cF91cGRhdGVC'
|
||||
'GgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl9tZXNzYWdlc0IaChhfYWRkaX'
|
||||
'Rpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0b3JCGQoXX3VzZXJfZGlzY292'
|
||||
'ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZUIZChdfa2V5X3ZlcmlmaWNhdG'
|
||||
'lvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIKIF9wYXNzd29yZGxlc3NfcmVj'
|
||||
'b3ZlcnlfaGVhcnRiZWF0');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ package http_requests;
|
|||
message TextMessage {
|
||||
int64 user_id = 1;
|
||||
bytes body = 2;
|
||||
optional bytes push_data = 3;
|
||||
reserved 3; // was: bytes push_data
|
||||
bool wake_receiver = 4;
|
||||
}
|
||||
|
||||
message UploadRequest {
|
||||
|
|
|
|||
|
|
@ -710,7 +710,10 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
|
||||
final messageOnSuccess = TextMessage()
|
||||
..body = cipherText
|
||||
..userId = Int64(groupMember.contactId);
|
||||
..userId = Int64(groupMember.contactId)
|
||||
// A media message is user visible, so the server may send the opaque
|
||||
// FCM wake-up once the upload completes.
|
||||
..wakeReceiver = true;
|
||||
|
||||
messagesOnSuccess.add(messageOnSuccess);
|
||||
downloadTokens.add(downloadToken);
|
||||
|
|
|
|||
|
|
@ -1,34 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> customLocalPushNotification(String title, String msg) async {
|
||||
final androidNotificationDetails = AndroidNotificationDetails(
|
||||
'1',
|
||||
'System',
|
||||
channelDescription: 'System messages.',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
styleInformation: BigTextStyleInformation(msg),
|
||||
icon: 'ic_launcher_foreground',
|
||||
);
|
||||
|
||||
const darwinNotificationDetails = DarwinNotificationDetails();
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
iOS: darwinNotificationDetails,
|
||||
);
|
||||
|
||||
final id = Random.secure().nextInt(9999);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
id,
|
||||
title,
|
||||
msg,
|
||||
notificationDetails,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
await AppEnvironment.init();
|
||||
final isInitialized = await initBackgroundExecution();
|
||||
await setupPushNotification();
|
||||
Log.info('Handling a background message: ${message.messageId}');
|
||||
await FcmNotificationService.handleRemoteMessage(message);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (isInitialized) {
|
||||
await backgroundFetch(lastExecutionInSecondsLimit: 3);
|
||||
}
|
||||
} else {
|
||||
// make sure every thing run...
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,8 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
|
|||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.background.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -19,14 +15,14 @@ import '../../../firebase_options.dart';
|
|||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||
|
||||
class FcmNotificationService {
|
||||
/// FCM is only an opaque wake-up transport now. Delivery is owned natively by
|
||||
/// the iOS Notification Service Extension and by
|
||||
/// `TwonlyFirebaseMessagingService` on Android, both of which call Rust
|
||||
/// directly, so no Dart isolate or message listener is registered here.
|
||||
static Future<void> initStartup() async {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||
|
||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
||||
}
|
||||
|
||||
static Future<void> initAfterUserLoaded() async {
|
||||
|
|
@ -143,67 +139,6 @@ class FcmNotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||
Log.info('handleRemoteMessage received message: ${message.messageId}');
|
||||
await _updateLastFcmMessageTimestamp();
|
||||
if (!Platform.isAndroid) {
|
||||
Log.error('Got message in Dart while on iOS');
|
||||
}
|
||||
if (message.notification != null && AppState.isAppInBackground) {
|
||||
Log.error(
|
||||
'Got notification but app is in background, so the SDK already have shown the message.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// In scenarios like Android Doze Mode or aggressive background restrictions, the OS may kill
|
||||
// or heavily restrict network access, preventing the WebSocket from connecting in time.
|
||||
// By parsing the FCM data payload offline, we can instantly display the notification, which also
|
||||
// prevents FCM from penalizing/downgrading the app's data message priority for failing to show a notification.
|
||||
// This is just a workarround until the new Rust decryption is enrolled fully.
|
||||
final pushDataString = message.data['push_data'] as String?;
|
||||
if (pushDataString != null) {
|
||||
final apiState = await RustApi.connectionState();
|
||||
if (apiState == ApiConnectionState.connected ||
|
||||
apiState == ApiConnectionState.authenticating ||
|
||||
apiState == ApiConnectionState.authenticated) {
|
||||
Log.info('Got FCM message, but API is connected...');
|
||||
} else {
|
||||
Log.info('Trying to connect to the API in the background.');
|
||||
|
||||
if (await backgroundFetch()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.notification != null || message.data['title'] != null) {
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] as String? ?? '';
|
||||
final body =
|
||||
message.notification?.body ?? message.data['body'] as String? ?? '';
|
||||
await customLocalPushNotification(title, body);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _updateLastFcmMessageTimestamp() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
try {
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||
value: nowMs,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
Log.info('Updated last FCM message timestamp to $nowMs');
|
||||
} catch (e) {
|
||||
Log.error('Could not write last FCM message timestamp: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> updateLastServerMessageTimestamp() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
|
@ -229,13 +164,6 @@ class FcmNotificationService {
|
|||
}
|
||||
const storage = FlutterSecureStorage();
|
||||
try {
|
||||
final lastFcmStr = await storage.read(
|
||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
final lastServerStr = await storage.read(
|
||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||
iOptions: const IOSOptions(
|
||||
|
|
@ -247,13 +175,12 @@ class FcmNotificationService {
|
|||
final now = DateTime.now();
|
||||
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
||||
|
||||
DateTime? lastFcmTime;
|
||||
if (lastFcmStr != null) {
|
||||
final ms = int.tryParse(lastFcmStr);
|
||||
if (ms != null) {
|
||||
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
||||
}
|
||||
}
|
||||
// Recorded by the Rust notification worker, because neither platform
|
||||
// starts Flutter for a background wake-up any more.
|
||||
final lastFcmWakeup = userService.currentUser.lastFcmWakeupAt;
|
||||
final lastFcmTime = lastFcmWakeup == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(lastFcmWakeup * 1000);
|
||||
|
||||
if (lastFcmTime != null) {
|
||||
Log.info(
|
||||
|
|
|
|||
77
lib/src/services/notifications/native.notifications.dart
Normal file
77
lib/src/services/notifications/native.notifications.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
/// Taps on notifications rendered natively (Android `MessagingStyle`) arrive
|
||||
/// through a platform channel instead of the Firebase Messaging plugin, which
|
||||
/// no longer owns background delivery.
|
||||
///
|
||||
/// Only the opaque conversation identifier crosses the channel; the Flutter
|
||||
/// route is built here so the native layer stays free of routing knowledge.
|
||||
class NativeNotificationService {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'eu.twonly/notificationTap',
|
||||
);
|
||||
|
||||
static const String _conversationIdKey = 'conversation_id';
|
||||
static const String _notificationIdsKey = 'notification_ids';
|
||||
|
||||
static final StreamController<String?> _taps =
|
||||
StreamController<String?>.broadcast();
|
||||
|
||||
/// Emits the conversation id of every notification tapped while the app is
|
||||
/// running. A `null` value means the notification had no specific
|
||||
/// conversation and should only open the chats tab.
|
||||
static Stream<String?> get taps => _taps.stream;
|
||||
|
||||
static void init() {
|
||||
if (!Platform.isAndroid) return;
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
if (call.method != 'onNotificationTapped') return;
|
||||
_taps.add(_conversationIdOf(call.arguments));
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the tap that launched the app, or `null` when the app was not
|
||||
/// started from a native notification. The result is consumed once.
|
||||
static Future<({String? conversationId})?> consumeInitialTap() async {
|
||||
if (!Platform.isAndroid) return null;
|
||||
try {
|
||||
final result = await _channel.invokeMapMethod<String, dynamic>(
|
||||
'consumeInitialNotification',
|
||||
);
|
||||
if (result == null) return null;
|
||||
return (conversationId: _conversationIdOf(result));
|
||||
} catch (e) {
|
||||
Log.error('Could not read the initial native notification: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Withdraws notifications for messages that have just been opened. Native
|
||||
/// notification identifiers are strings on iOS and stable hashes on Android,
|
||||
/// so the conversion stays in the platform implementation.
|
||||
static Future<void> cancelNotifications(
|
||||
Iterable<String> notificationIds,
|
||||
) async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return;
|
||||
final ids = notificationIds.where((id) => id.isNotEmpty).toSet().toList();
|
||||
if (ids.isEmpty) return;
|
||||
try {
|
||||
await _channel.invokeMethod<void>('cancelNotifications', {
|
||||
_notificationIdsKey: ids,
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error('Could not withdraw opened-message notifications: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static String? _conversationIdOf(Object? arguments) {
|
||||
if (arguments is! Map) return null;
|
||||
final conversationId = arguments[_conversationIdKey];
|
||||
if (conversationId is! String || conversationId.isEmpty) return null;
|
||||
return conversationId;
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,17 @@ class Log {
|
|||
final message = filterLogMessage('$messageInput');
|
||||
Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace);
|
||||
}
|
||||
|
||||
/// Re-emits a record that was produced outside Dart, keeping the origin's
|
||||
/// level and source location. Deriving either from the Dart call site would
|
||||
/// only ever point back at the forwarding code.
|
||||
static void forward({
|
||||
required Level level,
|
||||
required String source,
|
||||
required Object? messageInput,
|
||||
}) {
|
||||
Logger(source).log(level, filterLogMessage('$messageInput'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> loadLogFile() async {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/native.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||
|
|
@ -354,10 +354,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
List<GroupHistory> groupActions, {
|
||||
bool reportOpened = false,
|
||||
}) async {
|
||||
if (reportOpened && _isViewActive()) {
|
||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
||||
}
|
||||
|
||||
for (final msg in newMessages) {
|
||||
if (_animationState.hasReceivedFirstBatch &&
|
||||
!_animationState.knownMessageIds.contains(msg.messageId) &&
|
||||
|
|
@ -435,12 +431,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
_animationState.reportedOpenedMessageIds.addAll(
|
||||
openedMessages[contactId]!,
|
||||
);
|
||||
unawaited(
|
||||
RustApi.notifyMessagesOpened(
|
||||
contactId: contactId,
|
||||
messageIds: openedMessages[contactId]!,
|
||||
),
|
||||
);
|
||||
unawaited(_reportMessagesOpened(contactId, openedMessages[contactId]!));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -472,6 +463,23 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
|||
_updateGalleryItems(messages: storedMediaFiles);
|
||||
}
|
||||
|
||||
Future<void> _reportMessagesOpened(
|
||||
int contactId,
|
||||
List<String> messageIds,
|
||||
) async {
|
||||
// Cancel once immediately for an already visible alert, and once after the
|
||||
// durable outbox update to close the small race with a native push worker.
|
||||
await NativeNotificationService.cancelNotifications(messageIds);
|
||||
try {
|
||||
await RustApi.notifyMessagesOpened(
|
||||
contactId: contactId,
|
||||
messageIds: messageIds,
|
||||
);
|
||||
} finally {
|
||||
await NativeNotificationService.cancelNotifications(messageIds);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateGalleryItems({List<Message>? messages, bool force = false}) {
|
||||
final storedMediaMessages =
|
||||
messages ??
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
|
@ -5,6 +7,7 @@ import 'package:twonly/locator.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/services/notifications/native.notifications.dart';
|
||||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||
|
|
@ -217,10 +220,7 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
|||
_playerController.startPlayer();
|
||||
if (widget.message.senderId != null &&
|
||||
widget.message.openedAt == null) {
|
||||
RustApi.notifyMessagesOpened(
|
||||
contactId: widget.message.senderId!,
|
||||
messageIds: [widget.message.messageId],
|
||||
);
|
||||
unawaited(_notifyMessageOpened());
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
|
|
@ -259,6 +259,21 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _notifyMessageOpened() async {
|
||||
final senderId = widget.message.senderId;
|
||||
if (senderId == null) return;
|
||||
final messageIds = [widget.message.messageId];
|
||||
await NativeNotificationService.cancelNotifications(messageIds);
|
||||
try {
|
||||
await RustApi.notifyMessagesOpened(
|
||||
contactId: senderId,
|
||||
messageIds: messageIds,
|
||||
);
|
||||
} finally {
|
||||
await NativeNotificationService.cancelNotifications(messageIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String formatMsToMinSec(int milliseconds) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:screen_protector/screen_protector.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
|
|
@ -19,7 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
|||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/native.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
||||
|
|
@ -133,12 +132,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
|
||||
final Mutex _messageUpdateLock = Mutex();
|
||||
|
||||
bool _isViewActive() {
|
||||
if (!mounted) return false;
|
||||
return !AppState.isAppInBackground &&
|
||||
(ModalRoute.of(context)?.isCurrent ?? false);
|
||||
}
|
||||
|
||||
Future<void> listenForUnopenedMedia(bool firstRun) async {
|
||||
_subscription = twonlyDB.messagesDao
|
||||
.watchMediaNotOpened(widget.group.groupId)
|
||||
|
|
@ -256,10 +249,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
showSendTextMessageInput = false;
|
||||
});
|
||||
|
||||
if (_isViewActive()) {
|
||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
||||
}
|
||||
|
||||
final stream = twonlyDB.mediaFilesDao.watchMedia(
|
||||
allMediaFiles.first.mediaId!,
|
||||
);
|
||||
|
|
@ -416,10 +405,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
markAsOpenMessageIDs = messageIds;
|
||||
}
|
||||
|
||||
await RustApi.notifyMessagesOpened(
|
||||
contactId: currentMessage!.senderId!,
|
||||
messageIds: markAsOpenMessageIDs,
|
||||
);
|
||||
await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs);
|
||||
try {
|
||||
await RustApi.notifyMessagesOpened(
|
||||
contactId: currentMessage!.senderId!,
|
||||
messageIds: markAsOpenMessageIDs,
|
||||
);
|
||||
} finally {
|
||||
await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|||
import 'package:twonly/src/providers/routing.provider.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/native.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -50,6 +51,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||
StreamSubscription<int>? _homeViewPageIndexSub;
|
||||
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
||||
StreamSubscription<String?>? _nativeNotificationSub;
|
||||
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
||||
|
||||
static Uri? pendingSharedLink;
|
||||
|
|
@ -113,6 +115,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
streamHomeViewPageIndex.add(0);
|
||||
});
|
||||
|
||||
_nativeNotificationSub = NativeNotificationService.taps.listen(
|
||||
_openNativeNotification,
|
||||
);
|
||||
|
||||
_sharedLinkSub = streamSharedLink.stream.listen((uri) {
|
||||
HomeViewState.pendingSharedLink = null;
|
||||
_mainCameraController.setSharedLinkForPreview(uri);
|
||||
|
|
@ -192,7 +198,20 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
});
|
||||
}
|
||||
|
||||
void _openNativeNotification(String? conversationId) {
|
||||
Log.info('Opened app from a native push notification tap.');
|
||||
if (conversationId != null) {
|
||||
routerProvider.go(Routes.chatsMessages(conversationId));
|
||||
}
|
||||
streamHomeViewPageIndex.add(0);
|
||||
}
|
||||
|
||||
Future<void> _initAsync() async {
|
||||
final initialNativeTap = await NativeNotificationService.consumeInitialTap();
|
||||
if (initialNativeTap != null) {
|
||||
_openNativeNotification(initialNativeTap.conversationId);
|
||||
}
|
||||
|
||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
||||
.getNotificationAppLaunchDetails();
|
||||
|
||||
|
|
@ -245,6 +264,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
|||
_onMessageOpenedAppSub?.cancel();
|
||||
_homeViewPageIndexSub?.cancel();
|
||||
_selectNotificationSub?.cancel();
|
||||
_nativeNotificationSub?.cancel();
|
||||
_disableCameraTimer?.cancel();
|
||||
_mainCameraController.setState = null;
|
||||
_mainCameraController.closeCamera();
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class SetupView extends StatefulWidget {
|
|||
class _SetupViewState extends State<SetupView> {
|
||||
StreamSubscription<void>? _userUpdateStream;
|
||||
late UserDiscoverySetupState state;
|
||||
bool _setupDone = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -102,16 +103,27 @@ class _SetupViewState extends State<SetupView> {
|
|||
if (widget.onUpdate != null) {
|
||||
_userUpdateStream = userService.onUserUpdated.listen((u) {
|
||||
if (userService.currentUser.currentSetupPage == null) {
|
||||
widget.onUpdate?.call();
|
||||
_notifySetupDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies the parent exactly once. Further user updates must not trigger
|
||||
/// another callback, otherwise a parent popping this view would pop the
|
||||
/// route below it as well.
|
||||
void _notifySetupDone() {
|
||||
if (_setupDone) return;
|
||||
_setupDone = true;
|
||||
unawaited(_userUpdateStream?.cancel());
|
||||
_userUpdateStream = null;
|
||||
widget.onUpdate?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_userUpdateStream?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -184,7 +196,7 @@ class _SetupViewState extends State<SetupView> {
|
|||
await UserService.update(
|
||||
(u) => u.skipSetupPages = true,
|
||||
);
|
||||
widget.onUpdate?.call();
|
||||
_notifySetupDone();
|
||||
},
|
||||
variant: MyButtonVariant.text,
|
||||
child: Text(
|
||||
|
|
|
|||
|
|
@ -13,13 +13,18 @@ class FinishSetupComp extends StatefulWidget {
|
|||
|
||||
class _FinishSetupCompState extends State<FinishSetupComp> {
|
||||
Future<void> onTap() async {
|
||||
await context.navPush(
|
||||
SetupView(
|
||||
onUpdate: () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
// Captured before pushing so the callback never resolves the navigator
|
||||
// through this widget's context, which lives below the pushed route.
|
||||
final navigator = Navigator.of(context);
|
||||
await navigator.push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => SetupView(
|
||||
onUpdate: () {
|
||||
if (mounted && navigator.canPop()) {
|
||||
navigator.pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class DeveloperInformationsView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||
String? _lastFcmTimestamp;
|
||||
String? _lastServerTimestamp;
|
||||
|
||||
@override
|
||||
|
|
@ -26,13 +25,6 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
|||
Future<void> _loadInformations({bool showFeedback = false}) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
try {
|
||||
final lastFcm = await storage.read(
|
||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
final lastServer = await storage.read(
|
||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||
iOptions: const IOSOptions(
|
||||
|
|
@ -42,7 +34,6 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
|||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lastFcmTimestamp = lastFcm;
|
||||
_lastServerTimestamp = lastServer;
|
||||
});
|
||||
if (showFeedback) {
|
||||
|
|
@ -56,6 +47,14 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
|||
} catch (_) {}
|
||||
}
|
||||
|
||||
String _formatFcmWakeup() {
|
||||
final seconds = userService.currentUser.lastFcmWakeupAt;
|
||||
if (seconds == null) return 'Never';
|
||||
return DateTime.fromMillisecondsSinceEpoch(
|
||||
seconds * 1000,
|
||||
).toLocal().toString();
|
||||
}
|
||||
|
||||
String _formatTimestamp(String? timestampStr) {
|
||||
if (timestampStr == null) return 'Never';
|
||||
final ms = int.tryParse(timestampStr);
|
||||
|
|
@ -94,7 +93,7 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
|||
const Divider(),
|
||||
ListTile(
|
||||
title: const Text('Last FCM Message'),
|
||||
subtitle: Text(_formatTimestamp(_lastFcmTimestamp)),
|
||||
subtitle: Text(_formatFcmWakeup()),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Last Server Message'),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n promotion_id,\n public_id,\n from_contact_id,\n threshold,\n announcement_share,\n public_key_verified_timestamp\n FROM user_discovery_other_promotions\n WHERE public_id = ?\n ",
|
||||
"query": "\n SELECT promotion_id, public_id, from_contact_id, threshold,\n announcement_share, public_key_verified_timestamp\n FROM user_discovery_other_promotions\n WHERE public_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -82,5 +82,5 @@
|
|||
true
|
||||
]
|
||||
},
|
||||
"hash": "229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb"
|
||||
"hash": "0095795ef631e1c4a775c8984bbbe0ca0c546dcc3ef1a608527fa9c42484f763"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00e0c32ce73e5448c93de168ac43e7c46db5e3806197aeaf43850d7f8141f63d"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(\n SELECT 1 FROM group_members gm\n WHERE gm.group_id = messages.group_id AND NOT EXISTS(\n SELECT 1 FROM message_actions ma\n WHERE ma.message_id = messages.message_id\n AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'\n )\n ) THEN ? ELSE NULL END\n WHERE message_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "02f92beaef5af931c37b62b9ebfbdcf0490b17343ba0e11427f572cc9c41b8b4"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "03c020a855d64733408f6cf11297505ecaa67c943819bcb3a5ed2a824e668ca6"
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT media_id, type AS media_type, download_token,\n encryption_key, encryption_mac, encryption_nonce\n FROM media_files WHERE media_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "media_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "media_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "media_type",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "type"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "download_token",
|
||||
"ordinal": 2,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "download_token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "encryption_key",
|
||||
"ordinal": 3,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "encryption_key"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "encryption_mac",
|
||||
"ordinal": 4,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "encryption_mac"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "encryption_nonce",
|
||||
"ordinal": 5,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "encryption_nonce"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "051214ce0e6182d160aa7d3f039009d0fb62c946560da627d7de41e57a0d289d"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT promotion\n FROM user_discovery_own_promotions\n WHERE version_id > ?\n ",
|
||||
"query": "SELECT promotion FROM user_discovery_own_promotions\n WHERE contact_id = ? ORDER BY version_id DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -22,5 +22,5 @@
|
|||
false
|
||||
]
|
||||
},
|
||||
"hash": "1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3"
|
||||
"hash": "06bde3698b9256418111975b79dc581ea72a4f68f7dfd4ceb63bfac45aa031b3"
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT sender_id, content, is_deleted_from_sender FROM messages WHERE message_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "sender_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "messages",
|
||||
"name": "sender_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "messages",
|
||||
"name": "content"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_deleted_from_sender",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "messages",
|
||||
"name": "is_deleted_from_sender"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "09817a671f97e3aaeffcf37f8018c231462700155ea0b6b1914a8432cfeb6dcf"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO messages(\n group_id,\n message_id,\n sender_id,\n type,\n content,\n quotes_message_id,\n created_at,\n ack_by_server\n ) VALUES (?, ?, ?, 'text', ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))\n ON CONFLICT(message_id) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0a000bb74996bb70072c0b0dbb3bd54b690ec20c817a2cb1a29b0ac0fcc884ad"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO user_discovery_user_relations (\n announced_user_id, from_contact_id, public_key_verified_timestamp\n ) VALUES (?, ?, ?)\n ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0a969e410875ea46fde7c9e7fa773b73da80d2a71736532a4bea6dd2efb43cbb"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM reactions WHERE message_id = ? AND sender_id = ? AND emoji = ?)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "EXISTS(SELECT 1 FROM reactions WHERE message_id = ? AND sender_id = ? AND emoji = ?)",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0cf3d4227521bf00d715c1f66b595d9a4943283ab72635c63d184aab40dc19d5"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT display_name FROM contacts WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "display_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "display_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0dbbfbbf830b11f3e15c85e9e6004196d69f202929f836ac0c0500ea57804dfc"
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT last_flame_counter_change, flame_counter, max_flame_counter\n FROM groups\n WHERE group_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "last_flame_counter_change",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "last_flame_counter_change"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "flame_counter",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "flame_counter"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "max_flame_counter",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "max_flame_counter"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0dd1dbaa350aa766bada77410aef0da125561fd388fad0b4943f124bc2e73600"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE messages SET content = ?, modified_at = ?\n WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0e921f020426da5e0c76cd3c555f37d2abad38c2959ea971ae3085d51ad68ccb"
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT user_id, recovery_contacts_secret_share FROM contacts\n WHERE recovery_contacts_secret_share IS NOT NULL\n AND (recovery_contacts_last_heartbeat IS NULL\n OR recovery_contacts_last_heartbeat <= ?)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "user_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "user_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recovery_contacts_secret_share",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "recovery_contacts_secret_share"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0ee2e00e444da6707674388c8126e806a8ce02bd0008cc5774ca2d4e11aa5002"
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT group_id, total_media_counter, last_flame_counter_change,\n last_flame_sync, flame_counter\n FROM groups WHERE last_flame_counter_change IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "group_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "group_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "total_media_counter",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "total_media_counter"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_flame_counter_change",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "last_flame_counter_change"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_flame_sync",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "last_flame_sync"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "flame_counter",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "flame_counter"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "10facb63e1057c6acfe8b60cfe5f6deb3535eed60977d917f57cae97bb220e30"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE contacts\n SET username = COALESCE(?, username),\n display_name = ?,\n avatar_svg_compressed = ?,\n sender_profile_counter = COALESCE(?, sender_profile_counter)\n WHERE user_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "117582dbddd40553a8a622ed65b24284d8fb91c141ba989e2e3f4846c93d2a4c"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt)\n VALUES (?, ?, ?, 0)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "121378634faf12e63bdd7e116c8a0b24a9e20e39c41e11be8e98b32f7d0e8f61"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "media_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "messages",
|
||||
"name": "media_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "13e31c854a9086ef9fb5ae3a79ad5b915012fed2a79cf7275d74add38acff057"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt, wake_receiver)\n VALUES (?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "141c8905f5d1f47fdbd88a007115dda16edadf2e6f92e7b190e99925870bdc18"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS(\n SELECT 1 FROM group_members\n WHERE group_id = ? AND contact_id = ? AND member_state != 'leftGroup'\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "EXISTS(\n SELECT 1 FROM group_members\n WHERE group_id = ? AND contact_id = ? AND member_state != 'leftGroup'\n )",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1446c19ba57adcff2aeecf55e73196002e6e58ddd91f9fb7811fd43e67d956c8"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT media_id\n FROM messages\n WHERE message_id = ? AND sender_id = ?\n ",
|
||||
"query": "\n SELECT media_id\n FROM messages\n WHERE message_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -16,11 +16,11 @@
|
|||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8edba8bc58b60b678cb1730572119bd0f1ba1f98232e9d61eb0b7c920a5526f0"
|
||||
"hash": "18c5303b936b2615c4da0bd935b374fabd4973f6cbb5032d76a5d8824c5d2610"
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT accepted, requested FROM contacts WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "accepted",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "accepted"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "requested",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "requested"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "18c85d89372ea21ae6cc75750b1c2f8fac5d97ad75b1beb452176b36d3fbcaa7"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO media_files(media_id, type, download_state, upload_state) VALUES (?, 'image', 'ready', 'uploaded')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19645cdadf2646e2c3afb1f09dd509a62222baebc54743f3485368fdf27c167a"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO notification_outbox(\n event_id, notification_id, conversation_id, sender_id,\n message_id, kind, content, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "19af16bcf6c54900ed6032ce8ed7b5ef9d71456488805e343c2af10ad471b22a"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE receipts\n SET mark_for_retry = CAST(strftime('%s', 'now') AS INTEGER),\n retry_count = retry_count + 1,\n ack_by_server_at = NULL\n WHERE receipt_id = ? AND contact_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1acafcad398cb31491dcee91a005eddd33a03d40afa505b7e0b5f59e2f731a20"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO user_discovery_user_relations (\n announced_user_id,\n from_contact_id,\n public_key_verified_timestamp\n ) VALUES (?, ?, ?)\n ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT last_type_indicator FROM group_members WHERE group_id = ? AND contact_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "last_type_indicator",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "group_members",
|
||||
"name": "last_type_indicator"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1d8a2e974208c2ca67e4fe792c4aedcaf5bb86dc24f09ed71069d3868df833ee"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT upload_state FROM media_files WHERE media_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "upload_state",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "media_files",
|
||||
"name": "upload_state"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1dad19878770f615fde37f4fa8ce4cb8254a723c4d353a2ccc1d9d224bb68971"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT recovery_last_heartbeat FROM contacts WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recovery_last_heartbeat",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "recovery_last_heartbeat"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1e27aa2db6de33b41610d8fe9394b7d54224d8717dd687516da0ced5268e4899"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT EXISTS(\n SELECT 1 FROM user_discovery_own_promotions\n WHERE contact_id = ? AND length(promotion) > 0\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "EXISTS(\n SELECT 1 FROM user_discovery_own_promotions\n WHERE contact_id = ? AND length(promotion) > 0\n )",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1f22026fd1c3dc141303b1534b4d8fc497dc5fab1685aa8999f9ec9056b38df7"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE contacts SET recovery_secret_share = ?, recovery_is_trusted_friend = 1 WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f8ab52f86c62c865231b13253c61b6992a527e9fd1d89f074bed821fcb9095d"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO received_receipts(receipt_id)\n VALUES (?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2136bd64733c0268d4da52cf36db8df3533d34a45008bddf61e1090ae1ace885"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2138b7b197c5637580e0aa98d5e2d7e80b0cc1b660ab96ace53bb90ea5ff2968"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM api_outbox WHERE sequence_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "225cd55439cc89f0ee7bc53a0f19cf53a98ec2bdc32c82e43c6beef3bdc23d98"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE groups SET draft_message = 'draft text' WHERE group_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "23ce3aa70fcb31de3c6e6b1ba99750488d5d732b65ee326ca5b5a1626d43155c"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT COUNT(*) FROM receipts WHERE contact_id = 9",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "COUNT(*)",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "274f8f246a13f484fd1941bb0e570a06a7db075f26dfe0c943aefa0d66cff7aa"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO group_members(group_id, contact_id, member_state) VALUES (?, ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "28663e0e60055c3bef26992fecdaed69a4ae46aa385d8927a62504f838987dfc"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO group_histories(group_history_id, group_id, contact_id, type)\n VALUES (?, ?, ?, 'addMember')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "291bf3c6dfb80861e3a250243693beff0798bc51759ea525dae477bff2096585"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT contact_id FROM group_members WHERE group_id = ? AND group_public_key = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "contact_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "group_members",
|
||||
"name": "contact_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "29bd81e76831e95b2a26531cbcf59139c1954d23158600ace4b314fcd4626992"
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT r.contact_id, r.message, r.message_id, r.contact_will_sends_receipt,\n r.wake_receiver, c.account_deleted, c.signal_version\n FROM receipts r\n JOIN contacts c ON c.user_id = r.contact_id\n WHERE r.receipt_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "contact_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "receipts",
|
||||
"name": "contact_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "receipts",
|
||||
"name": "message"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "message_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "receipts",
|
||||
"name": "message_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "contact_will_sends_receipt",
|
||||
"ordinal": 3,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "receipts",
|
||||
"name": "contact_will_sends_receipt"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wake_receiver",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "receipts",
|
||||
"name": "wake_receiver"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "account_deleted",
|
||||
"ordinal": 5,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "account_deleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "signal_version",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "signal_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2a923146a880d7f8684495e93324a743ae58b3f32e01e5ad1fa5a304bb535ae5"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE media_files SET type = ?, download_state = 'pending',\n requires_authentication = ?, display_limit_in_milliseconds = ?,\n download_token = ?, encryption_key = ?, encryption_mac = ?,\n encryption_nonce = ?, created_at = ? WHERE media_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2b86ca80f38f47f115c8c8e6ce79343574e6ca435946c303dd8f35e68bfc57ef"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE contacts SET recovery_contacts_last_heartbeat = ? WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c4a9f54ba414124c446f52333a1c2dcf88c8ad4d4d96f746ce47532452ec19d"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE media_files SET download_state = 'reuploadRequested' WHERE media_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2cc4a73378cf37ff6e3dcbd761fec56686a8f9820a3bb2c489e94fb036de0786"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO group_members(group_id, contact_id, member_state)\n VALUES (?, ?, ?)\n ON CONFLICT(group_id, contact_id)\n DO UPDATE SET member_state = excluded.member_state\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2e24484b81be3b3a726c018b26311d543af677422a69318c0e187c3cf7379bf7"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT contact_id FROM group_members\n WHERE group_id = ? AND (member_state IS NULL OR member_state != 'leftGroup')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "contact_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "group_members",
|
||||
"name": "contact_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2e5c2ccc9be071bcebcb9c8a7b0564e6ef21a38106bb9bc90c840ac1903b75d5"
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT group_name FROM groups WHERE group_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "group_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "groups",
|
||||
"name": "group_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2ec060bf87e7c0a76c47be0010f9f9c37d20f2ea3ae1da27c757a5d32b60a1cc"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM key_verifications AS verification\n WHERE verification.contact_id = ?\n AND (\n verification.type != 'contactSharedByVerified'\n OR EXISTS(\n SELECT 1 FROM key_verifications AS verifier_verification\n WHERE verifier_verification.contact_id = verification.verified_by\n )\n )\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "EXISTS(\n SELECT 1\n FROM key_verifications AS verification\n WHERE verification.contact_id = ?\n AND (\n verification.type != 'contactSharedByVerified'\n OR EXISTS(\n SELECT 1 FROM key_verifications AS verifier_verification\n WHERE verifier_verification.contact_id = verification.verified_by\n )\n )\n )",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer",
|
||||
"origin": "Expression"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2f1854f10ac694a9dcabe133c32d986978bf168dccce04d461beeef220f8952c"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO message_histories(message_id, content, created_at)\n SELECT message_id, content, ? FROM messages\n WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2fec92a566a99026de111e1b9b175719cb54932ea03bd754747407d3291f764d"
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT sender_id\n FROM messages\n WHERE message_id = ?\n ",
|
||||
"query": "\n SELECT sender_id\n FROM messages\n WHERE message_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -22,5 +22,5 @@
|
|||
true
|
||||
]
|
||||
},
|
||||
"hash": "5988df8ec10b625bbd16ddcd2dccad54f2b209e74849509c1905e0f106756c7d"
|
||||
"hash": "345381ad177426da0800eaee0c04352e78377c106d7e156c07aef00858bc8bc8"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue