mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:54:08 +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 {
|
dependencies {
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
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 'com.otaliastudios:transcoder:0.11.0'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,25 @@
|
||||||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||||
tools:node="remove">
|
tools:node="remove">
|
||||||
</service>
|
</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
|
<meta-data
|
||||||
android:name="eu.twonly.service.TWONLY_LOGO"
|
android:name="eu.twonly.service.TWONLY_LOGO"
|
||||||
android:resource="@drawable/ic_launcher_foreground" />
|
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 android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import io.crates.keyring.Keyring
|
import io.crates.keyring.Keyring
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -16,6 +17,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import eu.twonly.notifications.NotificationTapChannel
|
||||||
|
|
||||||
class MainActivity : FlutterFragmentActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
private val CHANNEL = "eu.twonly/photo_picker"
|
private val CHANNEL = "eu.twonly/photo_picker"
|
||||||
|
|
@ -26,6 +28,10 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
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 ->
|
pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
|
||||||
if (uris.isNotEmpty()) {
|
if (uris.isNotEmpty()) {
|
||||||
val uriStrings = uris.map { it.toString() }
|
val uriStrings = uris.map { it.toString() }
|
||||||
|
|
@ -39,6 +45,12 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
NotificationTapChannel.handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
|
||||||
eventSink!!.success(true)
|
eventSink!!.success(true)
|
||||||
|
|
@ -58,6 +70,8 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
|
|
||||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||||
|
|
||||||
|
NotificationTapChannel.configure(flutterEngine, applicationContext)
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"pickImages" -> {
|
"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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>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>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
||||||
|
|
|
||||||
|
|
@ -1,326 +1,273 @@
|
||||||
import CryptoKit
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Intents
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
import rust_lib_twonly
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
private let runtimeAppGroup = "group.eu.twonly.runtime"
|
||||||
|
|
||||||
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 struct NativeNotificationResponse: Decodable {
|
||||||
|
let ok: Bool
|
||||||
|
let batch: NativeNotificationBatch?
|
||||||
|
let error: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPushNotificationData(pushData: String) -> (
|
private struct NativeNotificationBatch: Decodable {
|
||||||
title: String, body: String, notificationId: Int64
|
let additions: [NativeNotificationAddition]
|
||||||
)? {
|
let removals: [String]
|
||||||
|
let badgeCount: Int64
|
||||||
|
let completed: Bool
|
||||||
|
|
||||||
guard let data = Data(base64Encoded: pushData) else {
|
enum CodingKeys: String, CodingKey {
|
||||||
NSLog("Failed to decode base64 string")
|
case additions, removals, completed
|
||||||
return nil
|
case badgeCount = "badge_count"
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool {
|
private struct NativeNotificationAddition: Decodable {
|
||||||
guard uuid1.count >= 8, uuid2.count >= 8 else { return true }
|
let eventId: String
|
||||||
let hex1 = String(uuid1.prefix(8))
|
let notificationId: String
|
||||||
let hex2 = String(uuid2.prefix(8))
|
let conversationId: String?
|
||||||
guard let timestamp1 = UInt32(hex1, radix: 16),
|
let senderId: Int64
|
||||||
let timestamp2 = UInt32(hex2, radix: 16)
|
let senderName: String
|
||||||
else { return true }
|
let title: String
|
||||||
return timestamp1 > timestamp2
|
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 {
|
override func didReceive(
|
||||||
// Create a nonce for ChaChaPoly
|
_ request: UNNotificationRequest,
|
||||||
let nonce = try ChaChaPoly.Nonce(data: pushData.nonce)
|
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||||
|
) {
|
||||||
|
self.contentHandler = contentHandler
|
||||||
|
|
||||||
// Create a sealed box for ChaChaPoly
|
guard let runtimeDirectory = Self.runtimeDirectory() else {
|
||||||
let sealedBox = try ChaChaPoly.SealedBox(
|
suppress(reason: "shared runtime directory is unavailable")
|
||||||
nonce: nonce,
|
return
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
}
|
guard let response = Self.processWakeup(runtimeDirectory: runtimeDirectory) else {
|
||||||
|
self.suppress(reason: "notification worker returned no response")
|
||||||
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)")
|
|
||||||
return
|
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] = [
|
override func serviceExtensionTimeWillExpire() {
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
suppress(reason: "notification service extension timed out")
|
||||||
kSecAttrAccount as String: key,
|
}
|
||||||
kSecAttrService as String: "flutter_secure_storage_service",
|
|
||||||
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared"
|
|
||||||
]
|
|
||||||
|
|
||||||
// Delete existing item first to ensure a clean overwrite
|
private func render(batch: NativeNotificationBatch, original: UNNotificationContent) {
|
||||||
SecItemDelete(query as CFDictionary)
|
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
|
for (index, addition) in batch.additions.enumerated() {
|
||||||
var addQuery = query
|
group.enter()
|
||||||
addQuery[kSecValueData as String] = data
|
communicationContent(for: addition, badgeCount: batch.badgeCount, original: original) {
|
||||||
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
content in
|
||||||
|
let isFinal = index == batch.additions.count - 1
|
||||||
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
if isFinal {
|
||||||
if status != errSecSuccess {
|
stateLock.lock()
|
||||||
NSLog("Failed to write keychain item for key \(key): \(status)")
|
finalContent = content
|
||||||
} else {
|
deliveredEventIds.append(addition.eventId)
|
||||||
NSLog("Successfully wrote keychain item for key: \(key)")
|
stateLock.unlock()
|
||||||
}
|
group.leave()
|
||||||
}
|
return
|
||||||
|
|
||||||
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.",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (userKnown) {
|
let request = UNNotificationRequest(
|
||||||
pushNotificationText = [
|
identifier: addition.notificationId,
|
||||||
.text: "sent a message{inGroup}.",
|
content: content,
|
||||||
.twonly: "sent a twonly{inGroup}.",
|
trigger: nil
|
||||||
.video: "sent a video{inGroup}.",
|
)
|
||||||
.image: "sent an image{inGroup}.",
|
center.add(request) { error in
|
||||||
.audio: "sent a voice message{inGroup}.",
|
if let error {
|
||||||
.contactRequest: "wants to connect with you.",
|
NSLog("Could not schedule Twonly notification: \(error)")
|
||||||
.acceptRequest: "is now connected with you.",
|
} else {
|
||||||
.storedMediaFile: "has stored your image.",
|
stateLock.lock()
|
||||||
.reaction: "has reacted to your image.",
|
deliveredEventIds.append(addition.eventId)
|
||||||
.testNotification: "This is a test notification.",
|
stateLock.unlock()
|
||||||
.reopenedMedia: "has reopened your image.",
|
}
|
||||||
.reactionToVideo: "has reacted with {{content}} to your video.",
|
group.leave()
|
||||||
.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.",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = pushNotificationText[pushNotification.kind] ?? ""
|
group.notify(queue: .global(qos: .userInitiated)) { [weak self] in
|
||||||
if (content == "") {
|
stateLock.lock()
|
||||||
title = noTranslationFoundTitle
|
let eventIds = deliveredEventIds
|
||||||
content = noTranslationFoundBody
|
let content = finalContent
|
||||||
|
stateLock.unlock()
|
||||||
|
Self.acknowledge(eventIds: eventIds)
|
||||||
|
self?.finish(with: content)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if pushNotification.hasAdditionalContent {
|
private func communicationContent(
|
||||||
content.replace("{{content}}", with: pushNotification.additionalContent)
|
for addition: NativeNotificationAddition,
|
||||||
content.replace("{inGroup}", with: " in {inGroup}")
|
badgeCount: Int64,
|
||||||
content.replace("{inGroup}", with: pushNotification.additionalContent)
|
original: UNNotificationContent,
|
||||||
} else {
|
completion: @escaping (UNNotificationContent) -> Void
|
||||||
content.replace("{inGroup}", with: "")
|
) {
|
||||||
|
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
|
end
|
||||||
|
|
||||||
target 'NotificationService' do
|
target 'NotificationService' do
|
||||||
pod 'SwiftProtobuf'
|
pod 'rust_lib_twonly', :path => '.symlinks/plugins/rust_lib_twonly/ios'
|
||||||
# pod 'Firebase/Messaging'
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,7 @@ PODS:
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- rust_lib_twonly (0.0.1):
|
- rust_lib_twonly (0.0.1)
|
||||||
- Flutter
|
|
||||||
- screen_protector (1.5.1):
|
- screen_protector (1.5.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ScreenProtectorKit (= 1.5.1)
|
- ScreenProtectorKit (= 1.5.1)
|
||||||
|
|
@ -199,7 +198,7 @@ SPEC CHECKSUMS:
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
rust_lib_twonly: 6586fdf02e31cd8a3ad9f1ee84796da6bbf289bb
|
||||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||||
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
||||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||||
|
|
@ -207,6 +206,6 @@ SPEC CHECKSUMS:
|
||||||
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
|
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
|
||||||
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
|
||||||
|
|
||||||
PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf
|
PODFILE CHECKSUM: f83bbaaed0b8c29b006472e50864d40e54617e24
|
||||||
|
|
||||||
COCOAPODS: 1.17.0
|
COCOAPODS: 1.17.0
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
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 */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
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, ); }; };
|
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 */; };
|
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
|
||||||
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
@ -311,9 +311,6 @@
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
packageProductDependencies = (
|
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
);
|
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
|
@ -335,6 +332,9 @@
|
||||||
D25D4D792EFF41DB0029F805 /* PBXTargetDependency */,
|
D25D4D792EFF41DB0029F805 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
|
|
@ -385,9 +385,6 @@
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
packageReferences = (
|
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
|
||||||
);
|
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
|
@ -419,6 +416,9 @@
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|
@ -1319,12 +1319,14 @@
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
};
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/app-check.git",
|
"location" : "https://github.com/google/app-check.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
"revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
|
||||||
"version" : "11.2.0"
|
"version" : "11.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
"revision" : "346daa9f46316aa372b35b317e18224acc2e9063",
|
||||||
"version" : "12.14.0"
|
"version" : "12.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -41,8 +41,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
|
||||||
"version" : "3.6.0"
|
"version" : "3.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -50,8 +50,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
"revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5",
|
||||||
"version" : "12.14.0"
|
"version" : "12.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -59,8 +59,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
"revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974",
|
||||||
"version" : "10.1.0"
|
"version" : "10.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -68,8 +68,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
"revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3",
|
||||||
"version" : "8.1.0"
|
"version" : "8.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -86,8 +86,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
"revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc",
|
||||||
"version" : "5.3.0"
|
"version" : "5.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -113,8 +113,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/firebase/nanopb.git",
|
"location" : "https://github.com/firebase/nanopb.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||||
"version" : "2.30910.0"
|
"version" : "2.30910.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -122,8 +122,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/promises.git",
|
"location" : "https://github.com/google/promises.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||||
"version" : "2.4.0"
|
"version" : "2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import workmanager_apple
|
||||||
|
|
||||||
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
|
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
|
||||||
GeneratedPluginRegistrant.register(with: registry)
|
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(
|
WorkmanagerPlugin.registerPeriodicTask(
|
||||||
|
|
@ -51,6 +54,8 @@ import workmanager_apple
|
||||||
|
|
||||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
|
||||||
|
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func userNotificationCenter(
|
override func userNotificationCenter(
|
||||||
|
|
@ -70,23 +75,139 @@ import workmanager_apple
|
||||||
) {
|
) {
|
||||||
NSLog("userNotificationCenter:willPresent")
|
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])
|
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/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSUserActivityTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
<string>Use your camera to make photos or videos and share them encrypted with your friends.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:me.twonly.eu</string>
|
<string>applinks:me.twonly.eu</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.eu.twonly.shareIntent</string>
|
<string>group.eu.twonly.shareIntent</string>
|
||||||
|
<string>group.eu.twonly.runtime</string>
|
||||||
</array>
|
</array>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:me.twonly.eu</string>
|
<string>applinks:me.twonly.eu</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.eu.twonly.shareIntent</string>
|
<string>group.eu.twonly.shareIntent</string>
|
||||||
|
<string>group.eu.twonly.runtime</string>
|
||||||
</array>
|
</array>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:me.twonly.eu</string>
|
<string>applinks:me.twonly.eu</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.usernotifications.communication</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.eu.twonly.shareIntent</string>
|
<string>group.eu.twonly.shareIntent</string>
|
||||||
|
<string>group.eu.twonly.runtime</string>
|
||||||
</array>
|
</array>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<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 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 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}) =>
|
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
||||||
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,19 @@ class LegacyTableMigrationCount {
|
||||||
class RustAppDatabase {
|
class RustAppDatabase {
|
||||||
const 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({
|
static Future<SqlExecutionResult> execute({
|
||||||
required String statement,
|
required String statement,
|
||||||
required List<SqlValue> arguments,
|
required List<SqlValue> arguments,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => -1286774374;
|
int get rustContentHash => -1780439173;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -409,6 +409,8 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required List<PqcPreKeyInput> prekeys,
|
required List<PqcPreKeyInput> prekeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Stream<List<String>> crateBridgeWrapperAppDatabaseRustAppDatabaseChanges();
|
||||||
|
|
||||||
Future<SqlExecutionResult>
|
Future<SqlExecutionResult>
|
||||||
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
||||||
required String statement,
|
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
|
@override
|
||||||
Future<SqlExecutionResult>
|
Future<SqlExecutionResult>
|
||||||
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({
|
||||||
|
|
@ -3298,7 +3337,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 77,
|
funcId: 78,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3331,7 +3370,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 78,
|
funcId: 79,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3364,7 +3403,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 79,
|
funcId: 80,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3401,7 +3440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 80,
|
funcId: 81,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3433,7 +3472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 81,
|
funcId: 82,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3466,7 +3505,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 82,
|
funcId: 83,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3501,7 +3540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 83,
|
funcId: 84,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3533,7 +3572,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 84,
|
funcId: 85,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3571,7 +3610,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 85,
|
funcId: 86,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3604,7 +3643,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 86,
|
funcId: 87,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3642,7 +3681,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 87,
|
funcId: 88,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3679,7 +3718,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 88,
|
funcId: 89,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3716,7 +3755,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 89,
|
funcId: 90,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3754,7 +3793,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 90,
|
funcId: 91,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3792,7 +3831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 91,
|
funcId: 92,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3824,7 +3863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 92,
|
funcId: 93,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3857,7 +3896,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 93,
|
funcId: 94,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3889,7 +3928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 94,
|
funcId: 95,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3924,7 +3963,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 95,
|
funcId: 96,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3961,7 +4000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 96,
|
funcId: 97,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -3993,7 +4032,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 97,
|
funcId: 98,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4025,7 +4064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 98,
|
funcId: 99,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4060,7 +4099,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 99,
|
funcId: 100,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4099,7 +4138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 100,
|
funcId: 101,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4136,7 +4175,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 101,
|
funcId: 102,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4166,7 +4205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 102,
|
funcId: 103,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4198,7 +4237,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 103,
|
funcId: 104,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4233,7 +4272,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 104,
|
funcId: 105,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4265,7 +4304,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 105,
|
funcId: 106,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4303,7 +4342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 106,
|
funcId: 107,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4342,7 +4381,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 107,
|
funcId: 108,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4377,7 +4416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 108,
|
funcId: 109,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4412,7 +4451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 109,
|
funcId: 110,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4447,7 +4486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 110,
|
funcId: 111,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4480,7 +4519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return pdeCallFfi(
|
return pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 111,
|
funcId: 112,
|
||||||
)!;
|
)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
|
|
@ -4520,7 +4559,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 112,
|
funcId: 113,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4565,7 +4604,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 113,
|
funcId: 114,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4595,7 +4634,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 114,
|
funcId: 115,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4628,7 +4667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 115,
|
funcId: 116,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4663,7 +4702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 116,
|
funcId: 117,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -4995,6 +5034,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
throw UnimplementedError();
|
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
|
@protected
|
||||||
String dco_decode_String(dynamic raw) {
|
String dco_decode_String(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// 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) {
|
UserConfig dco_decode_user_config(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 58)
|
if (arr.length != 59)
|
||||||
throw Exception('unexpected arr length: expect 58 but see ${arr.length}');
|
throw Exception('unexpected arr length: expect 59 but see ${arr.length}');
|
||||||
return UserConfig(
|
return UserConfig(
|
||||||
userId: dco_decode_i_64(arr[0]),
|
userId: dco_decode_i_64(arr[0]),
|
||||||
username: dco_decode_String(arr[1]),
|
username: dco_decode_String(arr[1]),
|
||||||
|
|
@ -5845,9 +5892,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
passwordLessRecovery:
|
passwordLessRecovery:
|
||||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]),
|
dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]),
|
||||||
fcmToken: dco_decode_opt_String(arr[54]),
|
fcmToken: dco_decode_opt_String(arr[54]),
|
||||||
currentSetupPage: dco_decode_opt_String(arr[55]),
|
lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[55]),
|
||||||
skipSetupPages: dco_decode_bool(arr[56]),
|
currentSetupPage: dco_decode_opt_String(arr[56]),
|
||||||
hasZoomed: dco_decode_bool(arr[57]),
|
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 ()');
|
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
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer) {
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -6938,6 +6994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
var var_passwordLessRecovery =
|
var var_passwordLessRecovery =
|
||||||
sse_decode_opt_box_autoadd_passwordless_recovery_config(deserializer);
|
sse_decode_opt_box_autoadd_passwordless_recovery_config(deserializer);
|
||||||
var var_fcmToken = sse_decode_opt_String(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_currentSetupPage = sse_decode_opt_String(deserializer);
|
||||||
var var_skipSetupPages = sse_decode_bool(deserializer);
|
var var_skipSetupPages = sse_decode_bool(deserializer);
|
||||||
var var_hasZoomed = sse_decode_bool(deserializer);
|
var var_hasZoomed = sse_decode_bool(deserializer);
|
||||||
|
|
@ -7000,6 +7057,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
isBackupEnabled: var_isBackupEnabled,
|
isBackupEnabled: var_isBackupEnabled,
|
||||||
passwordLessRecovery: var_passwordLessRecovery,
|
passwordLessRecovery: var_passwordLessRecovery,
|
||||||
fcmToken: var_fcmToken,
|
fcmToken: var_fcmToken,
|
||||||
|
lastFcmWakeupAt: var_lastFcmWakeupAt,
|
||||||
currentSetupPage: var_currentSetupPage,
|
currentSetupPage: var_currentSetupPage,
|
||||||
skipSetupPages: var_skipSetupPages,
|
skipSetupPages: var_skipSetupPages,
|
||||||
hasZoomed: var_hasZoomed,
|
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
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer) {
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
@ -8112,6 +8187,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
serializer,
|
serializer,
|
||||||
);
|
);
|
||||||
sse_encode_opt_String(self.fcmToken, 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_opt_String(self.currentSetupPage, serializer);
|
||||||
sse_encode_bool(self.skipSetupPages, serializer);
|
sse_encode_bool(self.skipSetupPages, serializer);
|
||||||
sse_encode_bool(self.hasZoomed, serializer);
|
sse_encode_bool(self.hasZoomed, serializer);
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<List<String>> dco_decode_StreamSink_list_String_Sse(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -405,6 +410,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<List<String>> sse_decode_StreamSink_list_String_Sse(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -828,6 +838,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_StreamSink_list_String_Sse(
|
||||||
|
RustStreamSink<List<String>> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
RustStreamSink<ApiEvent> dco_decode_StreamSink_api_event_Sse(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<List<String>> dco_decode_StreamSink_list_String_Sse(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -407,6 +412,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustStreamSink<List<String>> sse_decode_StreamSink_list_String_Sse(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -830,6 +840,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_StreamSink_list_String_Sse(
|
||||||
|
RustStreamSink<List<String>> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,11 @@ class UserConfig {
|
||||||
bool isBackupEnabled;
|
bool isBackupEnabled;
|
||||||
PasswordlessRecoveryConfig? passwordLessRecovery;
|
PasswordlessRecoveryConfig? passwordLessRecovery;
|
||||||
String? fcmToken;
|
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;
|
String? currentSetupPage;
|
||||||
bool skipSetupPages;
|
bool skipSetupPages;
|
||||||
bool hasZoomed;
|
bool hasZoomed;
|
||||||
|
|
@ -218,6 +223,7 @@ class UserConfig {
|
||||||
required this.isBackupEnabled,
|
required this.isBackupEnabled,
|
||||||
this.passwordLessRecovery,
|
this.passwordLessRecovery,
|
||||||
this.fcmToken,
|
this.fcmToken,
|
||||||
|
this.lastFcmWakeupAt,
|
||||||
this.currentSetupPage,
|
this.currentSetupPage,
|
||||||
required this.skipSetupPages,
|
required this.skipSetupPages,
|
||||||
required this.hasZoomed,
|
required this.hasZoomed,
|
||||||
|
|
@ -280,6 +286,7 @@ class UserConfig {
|
||||||
isBackupEnabled.hashCode ^
|
isBackupEnabled.hashCode ^
|
||||||
passwordLessRecovery.hashCode ^
|
passwordLessRecovery.hashCode ^
|
||||||
fcmToken.hashCode ^
|
fcmToken.hashCode ^
|
||||||
|
lastFcmWakeupAt.hashCode ^
|
||||||
currentSetupPage.hashCode ^
|
currentSetupPage.hashCode ^
|
||||||
skipSetupPages.hashCode ^
|
skipSetupPages.hashCode ^
|
||||||
hasZoomed.hashCode;
|
hasZoomed.hashCode;
|
||||||
|
|
@ -351,6 +358,7 @@ class UserConfig {
|
||||||
isBackupEnabled == other.isBackupEnabled &&
|
isBackupEnabled == other.isBackupEnabled &&
|
||||||
passwordLessRecovery == other.passwordLessRecovery &&
|
passwordLessRecovery == other.passwordLessRecovery &&
|
||||||
fcmToken == other.fcmToken &&
|
fcmToken == other.fcmToken &&
|
||||||
|
lastFcmWakeupAt == other.lastFcmWakeupAt &&
|
||||||
currentSetupPage == other.currentSetupPage &&
|
currentSetupPage == other.currentSetupPage &&
|
||||||
skipSetupPages == other.skipSetupPages &&
|
skipSetupPages == other.skipSetupPages &&
|
||||||
hasZoomed == other.hasZoomed;
|
hasZoomed == other.hasZoomed;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
|
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
class AppEnvironment {
|
class AppEnvironment {
|
||||||
|
static const _runtimeChannel = MethodChannel('eu.twonly/runtime_storage');
|
||||||
static late String cacheDir;
|
static late String cacheDir;
|
||||||
static late String supportDir;
|
static late String supportDir;
|
||||||
|
|
||||||
|
|
@ -18,11 +21,66 @@ class AppEnvironment {
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
cacheDir = (await getApplicationCacheDirectory()).path;
|
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();
|
Log.init();
|
||||||
_isInitialized = true;
|
_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}) {
|
static void initTesting({String? customCacheDir, String? customSupportDir}) {
|
||||||
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
|
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
|
||||||
supportDir = customSupportDir ?? '/tmp/twonly_support';
|
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/memories/memories.service.dart';
|
||||||
import 'package:twonly/src/services/migrations.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/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/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
|
|
@ -102,6 +103,7 @@ void main() async {
|
||||||
var storageError = await twonlyMinimumInitialization();
|
var storageError = await twonlyMinimumInitialization();
|
||||||
await FcmNotificationService.initStartup();
|
await FcmNotificationService.initStartup();
|
||||||
await setupPushNotification();
|
await setupPushNotification();
|
||||||
|
NativeNotificationService.init();
|
||||||
|
|
||||||
var userExists = false;
|
var userExists = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,73 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/src/utils/log.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 {
|
class LoggingCallbacks {
|
||||||
static Future<RustStreamSink<String>> getStreamSink() async {
|
static Future<RustStreamSink<String>> getStreamSink() async {
|
||||||
final dartLogSink = RustStreamSink<String>();
|
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) {
|
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
|
attempts++;
|
||||||
try {
|
try {
|
||||||
dartLogSink.stream.listen(
|
dartLogSink.stream.listen(_handleRustLog);
|
||||||
(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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
// stream not yet initialized
|
// Stream not yet initialized.
|
||||||
|
if (attempts >= 100) {
|
||||||
|
timer.cancel();
|
||||||
|
Log.warn('Rust log sink never became available.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return dartLogSink;
|
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';
|
static const String userData = 'userData';
|
||||||
|
|
||||||
// Not required for backup...
|
// 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 =
|
static const String lastServerMessageTimestamp =
|
||||||
'last_server_message_timestamp';
|
'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:drift/drift.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
import 'package:twonly/src/database/daos/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/receipts.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/shortcuts.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/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/rust_query_executor.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
|
|
@ -64,14 +67,27 @@ part 'twonly.db.g.dart';
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class TwonlyDB extends _$TwonlyDB {
|
class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB([QueryExecutor? e])
|
TwonlyDB([QueryExecutor? e]) : super(e ?? openRustAppDatabase()) {
|
||||||
: super(
|
// Only the Rust-backed connection needs external change notifications.
|
||||||
e ?? openRustAppDatabase(),
|
// 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
|
// ignore: matching_super_parameters
|
||||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
|
StreamSubscription<List<String>>? _rustChanges;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _rustChanges?.cancel();
|
||||||
|
_rustChanges = null;
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 25;
|
int get schemaVersion => 25;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,12 @@ class TextMessage extends $pb.GeneratedMessage {
|
||||||
factory TextMessage({
|
factory TextMessage({
|
||||||
$fixnum.Int64? userId,
|
$fixnum.Int64? userId,
|
||||||
$core.List<$core.int>? body,
|
$core.List<$core.int>? body,
|
||||||
$core.List<$core.int>? pushData,
|
$core.bool? wakeReceiver,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (userId != null) result.userId = userId;
|
if (userId != null) result.userId = userId;
|
||||||
if (body != null) result.body = body;
|
if (body != null) result.body = body;
|
||||||
if (pushData != null) result.pushData = pushData;
|
if (wakeReceiver != null) result.wakeReceiver = wakeReceiver;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +46,7 @@ class TextMessage extends $pb.GeneratedMessage {
|
||||||
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
..aInt64(1, _omitFieldNames ? '' : 'userId')
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
2, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY)
|
2, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY)
|
||||||
..a<$core.List<$core.int>>(
|
..aOB(4, _omitFieldNames ? '' : 'wakeReceiver')
|
||||||
3, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY)
|
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
|
@ -87,14 +86,14 @@ class TextMessage extends $pb.GeneratedMessage {
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearBody() => $_clearField(2);
|
void clearBody() => $_clearField(2);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(4)
|
||||||
$core.List<$core.int> get pushData => $_getN(2);
|
$core.bool get wakeReceiver => $_getBF(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(4)
|
||||||
set pushData($core.List<$core.int> value) => $_setBytes(2, value);
|
set wakeReceiver($core.bool value) => $_setBool(2, value);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(4)
|
||||||
$core.bool hasPushData() => $_has(2);
|
$core.bool hasWakeReceiver() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(4)
|
||||||
void clearPushData() => $_clearField(3);
|
void clearWakeReceiver() => $_clearField(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadRequest extends $pb.GeneratedMessage {
|
class UploadRequest extends $pb.GeneratedMessage {
|
||||||
|
|
|
||||||
|
|
@ -21,25 +21,17 @@ const TextMessage$json = {
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||||
{'1': 'body', '3': 2, '4': 1, '5': 12, '10': 'body'},
|
{'1': 'body', '3': 2, '4': 1, '5': 12, '10': 'body'},
|
||||||
{
|
{'1': 'wake_receiver', '3': 4, '4': 1, '5': 8, '10': 'wakeReceiver'},
|
||||||
'1': 'push_data',
|
|
||||||
'3': 3,
|
|
||||||
'4': 1,
|
|
||||||
'5': 12,
|
|
||||||
'9': 0,
|
|
||||||
'10': 'pushData',
|
|
||||||
'17': true
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
'8': [
|
'9': [
|
||||||
{'1': '_push_data'},
|
{'1': 3, '2': 4},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `TextMessage`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `TextMessage`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List textMessageDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List textMessageDescriptor = $convert.base64Decode(
|
||||||
'CgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgCIAEoDFIEYm'
|
'CgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgCIAEoDFIEYm'
|
||||||
'9keRIgCglwdXNoX2RhdGEYAyABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YQ==');
|
'9keRIjCg13YWtlX3JlY2VpdmVyGAQgASgIUgx3YWtlUmVjZWl2ZXJKBAgDEAQ=');
|
||||||
|
|
||||||
@$core.Deprecated('Use uploadRequestDescriptor instead')
|
@$core.Deprecated('Use uploadRequestDescriptor instead')
|
||||||
const UploadRequest$json = {
|
const UploadRequest$json = {
|
||||||
|
|
|
||||||
|
|
@ -1140,98 +1140,6 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
||||||
void clearDisplayName() => $_clearField(4);
|
void clearDisplayName() => $_clearField(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
|
|
||||||
factory EncryptedContent_PushKeys({
|
|
||||||
EncryptedContent_PushKeys_Type? type,
|
|
||||||
$fixnum.Int64? keyId,
|
|
||||||
$core.List<$core.int>? key,
|
|
||||||
$fixnum.Int64? createdAt,
|
|
||||||
}) {
|
|
||||||
final result = create();
|
|
||||||
if (type != null) result.type = type;
|
|
||||||
if (keyId != null) result.keyId = keyId;
|
|
||||||
if (key != null) result.key = key;
|
|
||||||
if (createdAt != null) result.createdAt = createdAt;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
EncryptedContent_PushKeys._();
|
|
||||||
|
|
||||||
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 {
|
class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
||||||
factory EncryptedContent_FlameSync({
|
factory EncryptedContent_FlameSync({
|
||||||
$fixnum.Int64? flameCounter,
|
$fixnum.Int64? flameCounter,
|
||||||
|
|
@ -1724,7 +1632,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
EncryptedContent_ContactUpdate? contactUpdate,
|
EncryptedContent_ContactUpdate? contactUpdate,
|
||||||
EncryptedContent_ContactRequest? contactRequest,
|
EncryptedContent_ContactRequest? contactRequest,
|
||||||
EncryptedContent_FlameSync? flameSync,
|
EncryptedContent_FlameSync? flameSync,
|
||||||
EncryptedContent_PushKeys? pushKeys,
|
|
||||||
EncryptedContent_Reaction? reaction,
|
EncryptedContent_Reaction? reaction,
|
||||||
EncryptedContent_TextMessage? textMessage,
|
EncryptedContent_TextMessage? textMessage,
|
||||||
EncryptedContent_GroupCreate? groupCreate,
|
EncryptedContent_GroupCreate? groupCreate,
|
||||||
|
|
@ -1754,7 +1661,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
if (contactUpdate != null) result.contactUpdate = contactUpdate;
|
if (contactUpdate != null) result.contactUpdate = contactUpdate;
|
||||||
if (contactRequest != null) result.contactRequest = contactRequest;
|
if (contactRequest != null) result.contactRequest = contactRequest;
|
||||||
if (flameSync != null) result.flameSync = flameSync;
|
if (flameSync != null) result.flameSync = flameSync;
|
||||||
if (pushKeys != null) result.pushKeys = pushKeys;
|
|
||||||
if (reaction != null) result.reaction = reaction;
|
if (reaction != null) result.reaction = reaction;
|
||||||
if (textMessage != null) result.textMessage = textMessage;
|
if (textMessage != null) result.textMessage = textMessage;
|
||||||
if (groupCreate != null) result.groupCreate = groupCreate;
|
if (groupCreate != null) result.groupCreate = groupCreate;
|
||||||
|
|
@ -1813,8 +1719,6 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
subBuilder: EncryptedContent_ContactRequest.create)
|
subBuilder: EncryptedContent_ContactRequest.create)
|
||||||
..aOM<EncryptedContent_FlameSync>(10, _omitFieldNames ? '' : 'flameSync',
|
..aOM<EncryptedContent_FlameSync>(10, _omitFieldNames ? '' : 'flameSync',
|
||||||
subBuilder: EncryptedContent_FlameSync.create)
|
subBuilder: EncryptedContent_FlameSync.create)
|
||||||
..aOM<EncryptedContent_PushKeys>(11, _omitFieldNames ? '' : 'pushKeys',
|
|
||||||
subBuilder: EncryptedContent_PushKeys.create)
|
|
||||||
..aOM<EncryptedContent_Reaction>(12, _omitFieldNames ? '' : 'reaction',
|
..aOM<EncryptedContent_Reaction>(12, _omitFieldNames ? '' : 'reaction',
|
||||||
subBuilder: EncryptedContent_Reaction.create)
|
subBuilder: EncryptedContent_Reaction.create)
|
||||||
..aOM<EncryptedContent_TextMessage>(
|
..aOM<EncryptedContent_TextMessage>(
|
||||||
|
|
@ -1976,208 +1880,197 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
@$pb.TagNumber(10)
|
@$pb.TagNumber(10)
|
||||||
EncryptedContent_FlameSync ensureFlameSync() => $_ensure(8);
|
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)
|
@$pb.TagNumber(12)
|
||||||
EncryptedContent_Reaction get reaction => $_getN(10);
|
EncryptedContent_Reaction get reaction => $_getN(9);
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
set reaction(EncryptedContent_Reaction value) => $_setField(12, value);
|
set reaction(EncryptedContent_Reaction value) => $_setField(12, value);
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
$core.bool hasReaction() => $_has(10);
|
$core.bool hasReaction() => $_has(9);
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
void clearReaction() => $_clearField(12);
|
void clearReaction() => $_clearField(12);
|
||||||
@$pb.TagNumber(12)
|
@$pb.TagNumber(12)
|
||||||
EncryptedContent_Reaction ensureReaction() => $_ensure(10);
|
EncryptedContent_Reaction ensureReaction() => $_ensure(9);
|
||||||
|
|
||||||
@$pb.TagNumber(13)
|
@$pb.TagNumber(13)
|
||||||
EncryptedContent_TextMessage get textMessage => $_getN(11);
|
EncryptedContent_TextMessage get textMessage => $_getN(10);
|
||||||
@$pb.TagNumber(13)
|
@$pb.TagNumber(13)
|
||||||
set textMessage(EncryptedContent_TextMessage value) => $_setField(13, value);
|
set textMessage(EncryptedContent_TextMessage value) => $_setField(13, value);
|
||||||
@$pb.TagNumber(13)
|
@$pb.TagNumber(13)
|
||||||
$core.bool hasTextMessage() => $_has(11);
|
$core.bool hasTextMessage() => $_has(10);
|
||||||
@$pb.TagNumber(13)
|
@$pb.TagNumber(13)
|
||||||
void clearTextMessage() => $_clearField(13);
|
void clearTextMessage() => $_clearField(13);
|
||||||
@$pb.TagNumber(13)
|
@$pb.TagNumber(13)
|
||||||
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11);
|
EncryptedContent_TextMessage ensureTextMessage() => $_ensure(10);
|
||||||
|
|
||||||
@$pb.TagNumber(14)
|
@$pb.TagNumber(14)
|
||||||
EncryptedContent_GroupCreate get groupCreate => $_getN(12);
|
EncryptedContent_GroupCreate get groupCreate => $_getN(11);
|
||||||
@$pb.TagNumber(14)
|
@$pb.TagNumber(14)
|
||||||
set groupCreate(EncryptedContent_GroupCreate value) => $_setField(14, value);
|
set groupCreate(EncryptedContent_GroupCreate value) => $_setField(14, value);
|
||||||
@$pb.TagNumber(14)
|
@$pb.TagNumber(14)
|
||||||
$core.bool hasGroupCreate() => $_has(12);
|
$core.bool hasGroupCreate() => $_has(11);
|
||||||
@$pb.TagNumber(14)
|
@$pb.TagNumber(14)
|
||||||
void clearGroupCreate() => $_clearField(14);
|
void clearGroupCreate() => $_clearField(14);
|
||||||
@$pb.TagNumber(14)
|
@$pb.TagNumber(14)
|
||||||
EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(12);
|
EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(11);
|
||||||
|
|
||||||
@$pb.TagNumber(15)
|
@$pb.TagNumber(15)
|
||||||
EncryptedContent_GroupJoin get groupJoin => $_getN(13);
|
EncryptedContent_GroupJoin get groupJoin => $_getN(12);
|
||||||
@$pb.TagNumber(15)
|
@$pb.TagNumber(15)
|
||||||
set groupJoin(EncryptedContent_GroupJoin value) => $_setField(15, value);
|
set groupJoin(EncryptedContent_GroupJoin value) => $_setField(15, value);
|
||||||
@$pb.TagNumber(15)
|
@$pb.TagNumber(15)
|
||||||
$core.bool hasGroupJoin() => $_has(13);
|
$core.bool hasGroupJoin() => $_has(12);
|
||||||
@$pb.TagNumber(15)
|
@$pb.TagNumber(15)
|
||||||
void clearGroupJoin() => $_clearField(15);
|
void clearGroupJoin() => $_clearField(15);
|
||||||
@$pb.TagNumber(15)
|
@$pb.TagNumber(15)
|
||||||
EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(13);
|
EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(12);
|
||||||
|
|
||||||
@$pb.TagNumber(16)
|
@$pb.TagNumber(16)
|
||||||
EncryptedContent_GroupUpdate get groupUpdate => $_getN(14);
|
EncryptedContent_GroupUpdate get groupUpdate => $_getN(13);
|
||||||
@$pb.TagNumber(16)
|
@$pb.TagNumber(16)
|
||||||
set groupUpdate(EncryptedContent_GroupUpdate value) => $_setField(16, value);
|
set groupUpdate(EncryptedContent_GroupUpdate value) => $_setField(16, value);
|
||||||
@$pb.TagNumber(16)
|
@$pb.TagNumber(16)
|
||||||
$core.bool hasGroupUpdate() => $_has(14);
|
$core.bool hasGroupUpdate() => $_has(13);
|
||||||
@$pb.TagNumber(16)
|
@$pb.TagNumber(16)
|
||||||
void clearGroupUpdate() => $_clearField(16);
|
void clearGroupUpdate() => $_clearField(16);
|
||||||
@$pb.TagNumber(16)
|
@$pb.TagNumber(16)
|
||||||
EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14);
|
EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(13);
|
||||||
|
|
||||||
@$pb.TagNumber(17)
|
@$pb.TagNumber(17)
|
||||||
EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(15);
|
EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(14);
|
||||||
@$pb.TagNumber(17)
|
@$pb.TagNumber(17)
|
||||||
set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey value) =>
|
set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey value) =>
|
||||||
$_setField(17, value);
|
$_setField(17, value);
|
||||||
@$pb.TagNumber(17)
|
@$pb.TagNumber(17)
|
||||||
$core.bool hasResendGroupPublicKey() => $_has(15);
|
$core.bool hasResendGroupPublicKey() => $_has(14);
|
||||||
@$pb.TagNumber(17)
|
@$pb.TagNumber(17)
|
||||||
void clearResendGroupPublicKey() => $_clearField(17);
|
void clearResendGroupPublicKey() => $_clearField(17);
|
||||||
@$pb.TagNumber(17)
|
@$pb.TagNumber(17)
|
||||||
EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() =>
|
EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() =>
|
||||||
$_ensure(15);
|
$_ensure(14);
|
||||||
|
|
||||||
@$pb.TagNumber(18)
|
@$pb.TagNumber(18)
|
||||||
EncryptedContent_ErrorMessages get errorMessages => $_getN(16);
|
EncryptedContent_ErrorMessages get errorMessages => $_getN(15);
|
||||||
@$pb.TagNumber(18)
|
@$pb.TagNumber(18)
|
||||||
set errorMessages(EncryptedContent_ErrorMessages value) =>
|
set errorMessages(EncryptedContent_ErrorMessages value) =>
|
||||||
$_setField(18, value);
|
$_setField(18, value);
|
||||||
@$pb.TagNumber(18)
|
@$pb.TagNumber(18)
|
||||||
$core.bool hasErrorMessages() => $_has(16);
|
$core.bool hasErrorMessages() => $_has(15);
|
||||||
@$pb.TagNumber(18)
|
@$pb.TagNumber(18)
|
||||||
void clearErrorMessages() => $_clearField(18);
|
void clearErrorMessages() => $_clearField(18);
|
||||||
@$pb.TagNumber(18)
|
@$pb.TagNumber(18)
|
||||||
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16);
|
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(15);
|
||||||
|
|
||||||
@$pb.TagNumber(19)
|
@$pb.TagNumber(19)
|
||||||
EncryptedContent_AdditionalDataMessage get additionalDataMessage =>
|
EncryptedContent_AdditionalDataMessage get additionalDataMessage =>
|
||||||
$_getN(17);
|
$_getN(16);
|
||||||
@$pb.TagNumber(19)
|
@$pb.TagNumber(19)
|
||||||
set additionalDataMessage(EncryptedContent_AdditionalDataMessage value) =>
|
set additionalDataMessage(EncryptedContent_AdditionalDataMessage value) =>
|
||||||
$_setField(19, value);
|
$_setField(19, value);
|
||||||
@$pb.TagNumber(19)
|
@$pb.TagNumber(19)
|
||||||
$core.bool hasAdditionalDataMessage() => $_has(17);
|
$core.bool hasAdditionalDataMessage() => $_has(16);
|
||||||
@$pb.TagNumber(19)
|
@$pb.TagNumber(19)
|
||||||
void clearAdditionalDataMessage() => $_clearField(19);
|
void clearAdditionalDataMessage() => $_clearField(19);
|
||||||
@$pb.TagNumber(19)
|
@$pb.TagNumber(19)
|
||||||
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
|
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
|
||||||
$_ensure(17);
|
$_ensure(16);
|
||||||
|
|
||||||
@$pb.TagNumber(20)
|
@$pb.TagNumber(20)
|
||||||
EncryptedContent_TypingIndicator get typingIndicator => $_getN(18);
|
EncryptedContent_TypingIndicator get typingIndicator => $_getN(17);
|
||||||
@$pb.TagNumber(20)
|
@$pb.TagNumber(20)
|
||||||
set typingIndicator(EncryptedContent_TypingIndicator value) =>
|
set typingIndicator(EncryptedContent_TypingIndicator value) =>
|
||||||
$_setField(20, value);
|
$_setField(20, value);
|
||||||
@$pb.TagNumber(20)
|
@$pb.TagNumber(20)
|
||||||
$core.bool hasTypingIndicator() => $_has(18);
|
$core.bool hasTypingIndicator() => $_has(17);
|
||||||
@$pb.TagNumber(20)
|
@$pb.TagNumber(20)
|
||||||
void clearTypingIndicator() => $_clearField(20);
|
void clearTypingIndicator() => $_clearField(20);
|
||||||
@$pb.TagNumber(20)
|
@$pb.TagNumber(20)
|
||||||
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18);
|
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(17);
|
||||||
|
|
||||||
@$pb.TagNumber(21)
|
@$pb.TagNumber(21)
|
||||||
$core.List<$core.int> get senderUserDiscoveryVersion => $_getN(19);
|
$core.List<$core.int> get senderUserDiscoveryVersion => $_getN(18);
|
||||||
@$pb.TagNumber(21)
|
@$pb.TagNumber(21)
|
||||||
set senderUserDiscoveryVersion($core.List<$core.int> value) =>
|
set senderUserDiscoveryVersion($core.List<$core.int> value) =>
|
||||||
$_setBytes(19, value);
|
$_setBytes(18, value);
|
||||||
@$pb.TagNumber(21)
|
@$pb.TagNumber(21)
|
||||||
$core.bool hasSenderUserDiscoveryVersion() => $_has(19);
|
$core.bool hasSenderUserDiscoveryVersion() => $_has(18);
|
||||||
@$pb.TagNumber(21)
|
@$pb.TagNumber(21)
|
||||||
void clearSenderUserDiscoveryVersion() => $_clearField(21);
|
void clearSenderUserDiscoveryVersion() => $_clearField(21);
|
||||||
|
|
||||||
@$pb.TagNumber(22)
|
@$pb.TagNumber(22)
|
||||||
EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(20);
|
EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(19);
|
||||||
@$pb.TagNumber(22)
|
@$pb.TagNumber(22)
|
||||||
set userDiscoveryRequest(EncryptedContent_UserDiscoveryRequest value) =>
|
set userDiscoveryRequest(EncryptedContent_UserDiscoveryRequest value) =>
|
||||||
$_setField(22, value);
|
$_setField(22, value);
|
||||||
@$pb.TagNumber(22)
|
@$pb.TagNumber(22)
|
||||||
$core.bool hasUserDiscoveryRequest() => $_has(20);
|
$core.bool hasUserDiscoveryRequest() => $_has(19);
|
||||||
@$pb.TagNumber(22)
|
@$pb.TagNumber(22)
|
||||||
void clearUserDiscoveryRequest() => $_clearField(22);
|
void clearUserDiscoveryRequest() => $_clearField(22);
|
||||||
@$pb.TagNumber(22)
|
@$pb.TagNumber(22)
|
||||||
EncryptedContent_UserDiscoveryRequest ensureUserDiscoveryRequest() =>
|
EncryptedContent_UserDiscoveryRequest ensureUserDiscoveryRequest() =>
|
||||||
$_ensure(20);
|
$_ensure(19);
|
||||||
|
|
||||||
@$pb.TagNumber(23)
|
@$pb.TagNumber(23)
|
||||||
EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(21);
|
EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(20);
|
||||||
@$pb.TagNumber(23)
|
@$pb.TagNumber(23)
|
||||||
set userDiscoveryUpdate(EncryptedContent_UserDiscoveryUpdate value) =>
|
set userDiscoveryUpdate(EncryptedContent_UserDiscoveryUpdate value) =>
|
||||||
$_setField(23, value);
|
$_setField(23, value);
|
||||||
@$pb.TagNumber(23)
|
@$pb.TagNumber(23)
|
||||||
$core.bool hasUserDiscoveryUpdate() => $_has(21);
|
$core.bool hasUserDiscoveryUpdate() => $_has(20);
|
||||||
@$pb.TagNumber(23)
|
@$pb.TagNumber(23)
|
||||||
void clearUserDiscoveryUpdate() => $_clearField(23);
|
void clearUserDiscoveryUpdate() => $_clearField(23);
|
||||||
@$pb.TagNumber(23)
|
@$pb.TagNumber(23)
|
||||||
EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() =>
|
EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() =>
|
||||||
$_ensure(21);
|
$_ensure(20);
|
||||||
|
|
||||||
@$pb.TagNumber(24)
|
@$pb.TagNumber(24)
|
||||||
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(22);
|
EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(21);
|
||||||
@$pb.TagNumber(24)
|
@$pb.TagNumber(24)
|
||||||
set keyVerificationProof(EncryptedContent_KeyVerificationProof value) =>
|
set keyVerificationProof(EncryptedContent_KeyVerificationProof value) =>
|
||||||
$_setField(24, value);
|
$_setField(24, value);
|
||||||
@$pb.TagNumber(24)
|
@$pb.TagNumber(24)
|
||||||
$core.bool hasKeyVerificationProof() => $_has(22);
|
$core.bool hasKeyVerificationProof() => $_has(21);
|
||||||
@$pb.TagNumber(24)
|
@$pb.TagNumber(24)
|
||||||
void clearKeyVerificationProof() => $_clearField(24);
|
void clearKeyVerificationProof() => $_clearField(24);
|
||||||
@$pb.TagNumber(24)
|
@$pb.TagNumber(24)
|
||||||
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
|
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
|
||||||
$_ensure(22);
|
$_ensure(21);
|
||||||
|
|
||||||
@$pb.TagNumber(25)
|
@$pb.TagNumber(25)
|
||||||
$core.bool get askForFriendPromotions => $_getBF(23);
|
$core.bool get askForFriendPromotions => $_getBF(22);
|
||||||
@$pb.TagNumber(25)
|
@$pb.TagNumber(25)
|
||||||
set askForFriendPromotions($core.bool value) => $_setBool(23, value);
|
set askForFriendPromotions($core.bool value) => $_setBool(22, value);
|
||||||
@$pb.TagNumber(25)
|
@$pb.TagNumber(25)
|
||||||
$core.bool hasAskForFriendPromotions() => $_has(23);
|
$core.bool hasAskForFriendPromotions() => $_has(22);
|
||||||
@$pb.TagNumber(25)
|
@$pb.TagNumber(25)
|
||||||
void clearAskForFriendPromotions() => $_clearField(25);
|
void clearAskForFriendPromotions() => $_clearField(25);
|
||||||
|
|
||||||
@$pb.TagNumber(26)
|
@$pb.TagNumber(26)
|
||||||
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(24);
|
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(23);
|
||||||
@$pb.TagNumber(26)
|
@$pb.TagNumber(26)
|
||||||
set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) =>
|
set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) =>
|
||||||
$_setField(26, value);
|
$_setField(26, value);
|
||||||
@$pb.TagNumber(26)
|
@$pb.TagNumber(26)
|
||||||
$core.bool hasPasswordlessRecovery() => $_has(24);
|
$core.bool hasPasswordlessRecovery() => $_has(23);
|
||||||
@$pb.TagNumber(26)
|
@$pb.TagNumber(26)
|
||||||
void clearPasswordlessRecovery() => $_clearField(26);
|
void clearPasswordlessRecovery() => $_clearField(26);
|
||||||
@$pb.TagNumber(26)
|
@$pb.TagNumber(26)
|
||||||
EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() =>
|
EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() =>
|
||||||
$_ensure(24);
|
$_ensure(23);
|
||||||
|
|
||||||
@$pb.TagNumber(27)
|
@$pb.TagNumber(27)
|
||||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||||
get passwordlessRecoveryHeartbeat => $_getN(25);
|
get passwordlessRecoveryHeartbeat => $_getN(24);
|
||||||
@$pb.TagNumber(27)
|
@$pb.TagNumber(27)
|
||||||
set passwordlessRecoveryHeartbeat(
|
set passwordlessRecoveryHeartbeat(
|
||||||
EncryptedContent_PasswordLessRecoveryHeartbeat value) =>
|
EncryptedContent_PasswordLessRecoveryHeartbeat value) =>
|
||||||
$_setField(27, value);
|
$_setField(27, value);
|
||||||
@$pb.TagNumber(27)
|
@$pb.TagNumber(27)
|
||||||
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(25);
|
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(24);
|
||||||
@$pb.TagNumber(27)
|
@$pb.TagNumber(27)
|
||||||
void clearPasswordlessRecoveryHeartbeat() => $_clearField(27);
|
void clearPasswordlessRecoveryHeartbeat() => $_clearField(27);
|
||||||
@$pb.TagNumber(27)
|
@$pb.TagNumber(27)
|
||||||
EncryptedContent_PasswordLessRecoveryHeartbeat
|
EncryptedContent_PasswordLessRecoveryHeartbeat
|
||||||
ensurePasswordlessRecoveryHeartbeat() => $_ensure(25);
|
ensurePasswordlessRecoveryHeartbeat() => $_ensure(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -169,25 +169,5 @@ class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum {
|
||||||
const EncryptedContent_ContactUpdate_Type._(super.value, super.name);
|
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 =
|
const $core.bool _omitEnumNames =
|
||||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||||
|
|
|
||||||
|
|
@ -124,23 +124,13 @@ const EncryptedContent$json = {
|
||||||
'10': 'flameSync',
|
'10': 'flameSync',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'1': 'push_keys',
|
|
||||||
'3': 11,
|
|
||||||
'4': 1,
|
|
||||||
'5': 11,
|
|
||||||
'6': '.EncryptedContent.PushKeys',
|
|
||||||
'9': 11,
|
|
||||||
'10': 'pushKeys',
|
|
||||||
'17': true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'1': 'reaction',
|
'1': 'reaction',
|
||||||
'3': 12,
|
'3': 12,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.Reaction',
|
'6': '.EncryptedContent.Reaction',
|
||||||
'9': 12,
|
'9': 11,
|
||||||
'10': 'reaction',
|
'10': 'reaction',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +140,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.TextMessage',
|
'6': '.EncryptedContent.TextMessage',
|
||||||
'9': 13,
|
'9': 12,
|
||||||
'10': 'textMessage',
|
'10': 'textMessage',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -160,7 +150,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupCreate',
|
'6': '.EncryptedContent.GroupCreate',
|
||||||
'9': 14,
|
'9': 13,
|
||||||
'10': 'groupCreate',
|
'10': 'groupCreate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -170,7 +160,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupJoin',
|
'6': '.EncryptedContent.GroupJoin',
|
||||||
'9': 15,
|
'9': 14,
|
||||||
'10': 'groupJoin',
|
'10': 'groupJoin',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -180,7 +170,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupUpdate',
|
'6': '.EncryptedContent.GroupUpdate',
|
||||||
'9': 16,
|
'9': 15,
|
||||||
'10': 'groupUpdate',
|
'10': 'groupUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -190,7 +180,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ResendGroupPublicKey',
|
'6': '.EncryptedContent.ResendGroupPublicKey',
|
||||||
'9': 17,
|
'9': 16,
|
||||||
'10': 'resendGroupPublicKey',
|
'10': 'resendGroupPublicKey',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -200,7 +190,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ErrorMessages',
|
'6': '.EncryptedContent.ErrorMessages',
|
||||||
'9': 18,
|
'9': 17,
|
||||||
'10': 'errorMessages',
|
'10': 'errorMessages',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -210,7 +200,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.AdditionalDataMessage',
|
'6': '.EncryptedContent.AdditionalDataMessage',
|
||||||
'9': 19,
|
'9': 18,
|
||||||
'10': 'additionalDataMessage',
|
'10': 'additionalDataMessage',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -220,7 +210,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.TypingIndicator',
|
'6': '.EncryptedContent.TypingIndicator',
|
||||||
'9': 20,
|
'9': 19,
|
||||||
'10': 'typingIndicator',
|
'10': 'typingIndicator',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -230,7 +220,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.UserDiscoveryRequest',
|
'6': '.EncryptedContent.UserDiscoveryRequest',
|
||||||
'9': 21,
|
'9': 20,
|
||||||
'10': 'userDiscoveryRequest',
|
'10': 'userDiscoveryRequest',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -240,7 +230,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
||||||
'9': 22,
|
'9': 21,
|
||||||
'10': 'userDiscoveryUpdate',
|
'10': 'userDiscoveryUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -250,7 +240,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.KeyVerificationProof',
|
'6': '.EncryptedContent.KeyVerificationProof',
|
||||||
'9': 23,
|
'9': 22,
|
||||||
'10': 'keyVerificationProof',
|
'10': 'keyVerificationProof',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -260,7 +250,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.PasswordLessRecovery',
|
'6': '.EncryptedContent.PasswordLessRecovery',
|
||||||
'9': 24,
|
'9': 23,
|
||||||
'10': 'passwordlessRecovery',
|
'10': 'passwordlessRecovery',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -270,7 +260,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||||
'9': 25,
|
'9': 24,
|
||||||
'10': 'passwordlessRecoveryHeartbeat',
|
'10': 'passwordlessRecoveryHeartbeat',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -289,7 +279,6 @@ const EncryptedContent$json = {
|
||||||
EncryptedContent_MediaUpdate$json,
|
EncryptedContent_MediaUpdate$json,
|
||||||
EncryptedContent_ContactRequest$json,
|
EncryptedContent_ContactRequest$json,
|
||||||
EncryptedContent_ContactUpdate$json,
|
EncryptedContent_ContactUpdate$json,
|
||||||
EncryptedContent_PushKeys$json,
|
|
||||||
EncryptedContent_FlameSync$json,
|
EncryptedContent_FlameSync$json,
|
||||||
EncryptedContent_TypingIndicator$json,
|
EncryptedContent_TypingIndicator$json,
|
||||||
EncryptedContent_UserDiscoveryRequest$json,
|
EncryptedContent_UserDiscoveryRequest$json,
|
||||||
|
|
@ -310,7 +299,6 @@ const EncryptedContent$json = {
|
||||||
{'1': '_contact_update'},
|
{'1': '_contact_update'},
|
||||||
{'1': '_contact_request'},
|
{'1': '_contact_request'},
|
||||||
{'1': '_flame_sync'},
|
{'1': '_flame_sync'},
|
||||||
{'1': '_push_keys'},
|
|
||||||
{'1': '_reaction'},
|
{'1': '_reaction'},
|
||||||
{'1': '_text_message'},
|
{'1': '_text_message'},
|
||||||
{'1': '_group_create'},
|
{'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')
|
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||||
const EncryptedContent_FlameSync$json = {
|
const EncryptedContent_FlameSync$json = {
|
||||||
'1': 'FlameSync',
|
'1': 'FlameSync',
|
||||||
|
|
@ -891,107 +838,102 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||||
'91cGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGVICFINY29udGFj'
|
'91cGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGVICFINY29udGFj'
|
||||||
'dFVwZGF0ZYgBARJOCg9jb250YWN0X3JlcXVlc3QYCSABKAsyIC5FbmNyeXB0ZWRDb250ZW50Lk'
|
'dFVwZGF0ZYgBARJOCg9jb250YWN0X3JlcXVlc3QYCSABKAsyIC5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||||
'NvbnRhY3RSZXF1ZXN0SAlSDmNvbnRhY3RSZXF1ZXN0iAEBEj8KCmZsYW1lX3N5bmMYCiABKAsy'
|
'NvbnRhY3RSZXF1ZXN0SAlSDmNvbnRhY3RSZXF1ZXN0iAEBEj8KCmZsYW1lX3N5bmMYCiABKAsy'
|
||||||
'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESPAoJcHVzaF9rZX'
|
'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESOwoIcmVhY3Rpb2'
|
||||||
'lzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0gLUghwdXNoS2V5c4gBARI7Cghy'
|
'4YDCABKAsyGi5FbmNyeXB0ZWRDb250ZW50LlJlYWN0aW9uSAtSCHJlYWN0aW9uiAEBEkUKDHRl'
|
||||||
'ZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVhY3Rpb25IDFIIcmVhY3Rpb26IAQ'
|
'eHRfbWVzc2FnZRgNIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIDFILdGV4dE'
|
||||||
'ESRQoMdGV4dF9tZXNzYWdlGA0gASgLMh0uRW5jcnlwdGVkQ29udGVudC5UZXh0TWVzc2FnZUgN'
|
'1lc3NhZ2WIAQESRQoMZ3JvdXBfY3JlYXRlGA4gASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91'
|
||||||
'Ugt0ZXh0TWVzc2FnZYgBARJFCgxncm91cF9jcmVhdGUYDiABKAsyHS5FbmNyeXB0ZWRDb250ZW'
|
'cENyZWF0ZUgNUgtncm91cENyZWF0ZYgBARI/Cgpncm91cF9qb2luGA8gASgLMhsuRW5jcnlwdG'
|
||||||
'50Lkdyb3VwQ3JlYXRlSA5SC2dyb3VwQ3JlYXRliAEBEj8KCmdyb3VwX2pvaW4YDyABKAsyGy5F'
|
'VkQ29udGVudC5Hcm91cEpvaW5IDlIJZ3JvdXBKb2luiAEBEkUKDGdyb3VwX3VwZGF0ZRgQIAEo'
|
||||||
'bmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgPUglncm91cEpvaW6IAQESRQoMZ3JvdXBfdXBkYX'
|
'CzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBVcGRhdGVID1ILZ3JvdXBVcGRhdGWIAQESYgoXcm'
|
||||||
'RlGBAgASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cFVwZGF0ZUgQUgtncm91cFVwZGF0ZYgB'
|
'VzZW5kX2dyb3VwX3B1YmxpY19rZXkYESABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlJlc2VuZEdy'
|
||||||
'ARJiChdyZXNlbmRfZ3JvdXBfcHVibGljX2tleRgRIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUm'
|
'b3VwUHVibGljS2V5SBBSFHJlc2VuZEdyb3VwUHVibGljS2V5iAEBEksKDmVycm9yX21lc3NhZ2'
|
||||||
'VzZW5kR3JvdXBQdWJsaWNLZXlIEVIUcmVzZW5kR3JvdXBQdWJsaWNLZXmIAQESSwoOZXJyb3Jf'
|
'VzGBIgASgLMh8uRW5jcnlwdGVkQ29udGVudC5FcnJvck1lc3NhZ2VzSBFSDWVycm9yTWVzc2Fn'
|
||||||
'bWVzc2FnZXMYEiABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXNIElINZXJyb3'
|
'ZXOIAQESZAoXYWRkaXRpb25hbF9kYXRhX21lc3NhZ2UYEyABKAsyJy5FbmNyeXB0ZWRDb250ZW'
|
||||||
'JNZXNzYWdlc4gBARJkChdhZGRpdGlvbmFsX2RhdGFfbWVzc2FnZRgTIAEoCzInLkVuY3J5cHRl'
|
'50LkFkZGl0aW9uYWxEYXRhTWVzc2FnZUgSUhVhZGRpdGlvbmFsRGF0YU1lc3NhZ2WIAQESUQoQ'
|
||||||
'ZENvbnRlbnQuQWRkaXRpb25hbERhdGFNZXNzYWdlSBNSFWFkZGl0aW9uYWxEYXRhTWVzc2FnZY'
|
'dHlwaW5nX2luZGljYXRvchgUIAEoCzIhLkVuY3J5cHRlZENvbnRlbnQuVHlwaW5nSW5kaWNhdG'
|
||||||
'gBARJRChB0eXBpbmdfaW5kaWNhdG9yGBQgASgLMiEuRW5jcnlwdGVkQ29udGVudC5UeXBpbmdJ'
|
'9ySBNSD3R5cGluZ0luZGljYXRvcogBARJhChZ1c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0GBYgASgL'
|
||||||
'bmRpY2F0b3JIFFIPdHlwaW5nSW5kaWNhdG9yiAEBEmEKFnVzZXJfZGlzY292ZXJ5X3JlcXVlc3'
|
'MiYuRW5jcnlwdGVkQ29udGVudC5Vc2VyRGlzY292ZXJ5UmVxdWVzdEgUUhR1c2VyRGlzY292ZX'
|
||||||
'QYFiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlVzZXJEaXNjb3ZlcnlSZXF1ZXN0SBVSFHVzZXJE'
|
'J5UmVxdWVzdIgBARJeChV1c2VyX2Rpc2NvdmVyeV91cGRhdGUYFyABKAsyJS5FbmNyeXB0ZWRD'
|
||||||
'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3'
|
'b250ZW50LlVzZXJEaXNjb3ZlcnlVcGRhdGVIFVITdXNlckRpc2NvdmVyeVVwZGF0ZYgBARJhCh'
|
||||||
'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl'
|
'ZrZXlfdmVyaWZpY2F0aW9uX3Byb29mGBggASgLMiYuRW5jcnlwdGVkQ29udGVudC5LZXlWZXJp'
|
||||||
'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk'
|
'ZmljYXRpb25Qcm9vZkgWUhRrZXlWZXJpZmljYXRpb25Qcm9vZogBARJgChVwYXNzd29yZGxlc3'
|
||||||
'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3'
|
'NfcmVjb3ZlcnkYGiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5'
|
||||||
'b3JkbGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUm'
|
'SBdSFHBhc3N3b3JkbGVzc1JlY292ZXJ5iAEBEnwKH3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZW'
|
||||||
'Vjb3ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292'
|
'FydGJlYXQYGyABKAsyLy5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5SGVh'
|
||||||
'ZXJ5X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3'
|
'cnRiZWF0SBhSHXBhc3N3b3JkbGVzc1JlY292ZXJ5SGVhcnRiZWF0iAEBGpYCCg1FcnJvck1lc3'
|
||||||
'ZlcnlIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQEalgIKDUVy'
|
'NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlw'
|
||||||
'cm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYW'
|
'ZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQinA'
|
||||||
'dlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2Vp'
|
'EKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF9SRVFV'
|
||||||
'cHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU'
|
'RVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05fT1VUX0'
|
||||||
'5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lP'
|
'9GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0dyb3Vw'
|
||||||
'Tl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQ'
|
'Q3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGljX2tleR'
|
||||||
'oLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJs'
|
'gEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE5hbWWI'
|
||||||
'aWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3'
|
'AQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkYASABKA'
|
||||||
'VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tl'
|
'xSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cFVwZGF0'
|
||||||
'eRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3'
|
'ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2FmZmVjdG'
|
||||||
'VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoT'
|
'VkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2dyb3Vw'
|
||||||
'YWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZX'
|
'X25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2FnZXNfYW'
|
||||||
'dfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNz'
|
'Z0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNl'
|
||||||
'YWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
|
'Y29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1lQikKJ1'
|
||||||
'1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25h'
|
'9uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqvAQoLVGV4dE1lc3NhZ2US'
|
||||||
'bWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGq8BCgtUZXh0TW'
|
'KgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgAS'
|
||||||
'Vzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRl'
|
'gJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2Vf'
|
||||||
'eHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbW'
|
'aWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWQazgEKFU'
|
||||||
'Vzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdGVfbWVzc2FnZV9p'
|
'FkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVy'
|
||||||
'ZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg'
|
'TWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBH'
|
||||||
'9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgD'
|
'R5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3Nh'
|
||||||
'IAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbm'
|
'Z2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpkCghSZWFjdGlvbhIqChF0YX'
|
||||||
'FsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9u'
|
'JnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVl'
|
||||||
'EioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAi'
|
'bW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGA'
|
||||||
'ABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgK'
|
'EgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLwoRc2Vu'
|
||||||
'BHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZR'
|
'ZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEBEj0KG211bHRpcGxlX3'
|
||||||
'IvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVs'
|
'RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRl'
|
||||||
'dGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZH'
|
'eHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcCItCgRUeX'
|
||||||
'MSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1w'
|
'BlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhQKEl9zZW5kZXJfbWVz'
|
||||||
'Ii0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3Nlbm'
|
'c2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3'
|
||||||
'Rlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgB'
|
'NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5U'
|
||||||
'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
|
'eXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3'
|
||||||
'1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANI'
|
'BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3JlcXVpcmVzX2F1dGhlbnRpY2F0aW9uGAQg'
|
||||||
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYX'
|
'ASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW'
|
||||||
'Rpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0'
|
'1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAVIOcXVvdGVNZXNzYWdlSWSIAQESKgoOZG93'
|
||||||
'aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBAR'
|
'bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRUb2tlbogBARIqCg5lbmNyeXB0aW9uX2tleR'
|
||||||
'IqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRp'
|
'gIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDmVuY3J5cHRpb25fbWFjGAkgASgMSARSDWVu'
|
||||||
'b25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKA'
|
'Y3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9ub25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm'
|
||||||
'xIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5'
|
'9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1l'
|
||||||
'cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdG'
|
'c3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEA'
|
||||||
'lvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoF'
|
'ISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNC'
|
||||||
'VklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0X2luX21pbGxpc2'
|
'EwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2'
|
||||||
'Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW5CEQoPX2VuY3J5'
|
'tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9u'
|
||||||
'cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9ub25jZUIaChhfYW'
|
'YWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdG'
|
||||||
'RkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5F'
|
'VkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIg'
|
||||||
'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2'
|
'ASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEh'
|
||||||
'VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RP'
|
'QKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUu'
|
||||||
'UkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYAS'
|
'RW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUk'
|
||||||
'ABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlw'
|
'VRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGqQCCg1Db250YWN0VXBkYXRlEjgKBHR5'
|
||||||
'ZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNvbnRhY3RVcGRhdG'
|
'cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3Ch'
|
||||||
'USOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0'
|
'VhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIf'
|
||||||
'eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcmVzc2'
|
'Cgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCABKAlIAl'
|
||||||
'VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3BsYXlfbmFtZRgE'
|
'ILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2'
|
||||||
'IAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAU'
|
'YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVCDwoNX2Rpc3BsYXlfbmFtZRqvAQoJRm'
|
||||||
'IYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZGlzcGxheV9uYW1l'
|
'xhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYXN0X2Zs'
|
||||||
'GtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy'
|
'YW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3'
|
||||||
'5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVID'
|
'RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm9yY2VV'
|
||||||
'a2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUk'
|
'cGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBpbmcSHQ'
|
||||||
'VRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3JlYXRlZF9hdBqv'
|
'oKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZXN0EicK'
|
||||||
'AQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYX'
|
'D2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2NvdmVyeV'
|
||||||
'N0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8K'
|
'VwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW9uUHJv'
|
||||||
'C2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm'
|
'b2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3b3JkTG'
|
||||||
'9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBp'
|
'Vzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3ZlcnlTZWNy'
|
||||||
'bmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZX'
|
'ZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyABKANSCX'
|
||||||
'N0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2Nv'
|
'RocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZWNvdmVy'
|
||||||
'dmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW'
|
'eUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNfZGlyZW'
|
||||||
'9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3'
|
'N0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2Rpc2Nv'
|
||||||
'b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3Zlcn'
|
'dmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNzYWdlX3'
|
||||||
'lTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyAB'
|
'VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdGVCEgoQ'
|
||||||
'KANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZW'
|
'X2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcmVhY3Rpb25CDwoNX3RleHRfbW'
|
||||||
'NvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNf'
|
'Vzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2luQg8KDV9ncm91cF91cGRhdGVC'
|
||||||
'ZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2'
|
'GgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl9tZXNzYWdlc0IaChhfYWRkaX'
|
||||||
'Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNz'
|
'Rpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0b3JCGQoXX3VzZXJfZGlzY292'
|
||||||
'YWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdG'
|
'ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZUIZChdfa2V5X3ZlcmlmaWNhdG'
|
||||||
'VCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9y'
|
'lvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIKIF9wYXNzd29yZGxlc3NfcmVj'
|
||||||
'ZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW'
|
'b3ZlcnlfaGVhcnRiZWF0');
|
||||||
'5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9y'
|
|
||||||
'X21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYX'
|
|
||||||
'RvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRl'
|
|
||||||
'QhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29mQhgKFl9wYXNzd29yZGxlc3NfcmVjb3ZlcnlCIg'
|
|
||||||
'ogX3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZWFydGJlYXQ=');
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ package http_requests;
|
||||||
message TextMessage {
|
message TextMessage {
|
||||||
int64 user_id = 1;
|
int64 user_id = 1;
|
||||||
bytes body = 2;
|
bytes body = 2;
|
||||||
optional bytes push_data = 3;
|
reserved 3; // was: bytes push_data
|
||||||
|
bool wake_receiver = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UploadRequest {
|
message UploadRequest {
|
||||||
|
|
|
||||||
|
|
@ -710,7 +710,10 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
||||||
|
|
||||||
final messageOnSuccess = TextMessage()
|
final messageOnSuccess = TextMessage()
|
||||||
..body = cipherText
|
..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);
|
messagesOnSuccess.add(messageOnSuccess);
|
||||||
downloadTokens.add(downloadToken);
|
downloadTokens.add(downloadToken);
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,4 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin 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_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:twonly/globals.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/fcm.background.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.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
|
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||||
|
|
||||||
class FcmNotificationService {
|
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 {
|
static Future<void> initStartup() async {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
);
|
);
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initAfterUserLoaded() async {
|
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 {
|
static Future<void> updateLastServerMessageTimestamp() async {
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
|
@ -229,13 +164,6 @@ class FcmNotificationService {
|
||||||
}
|
}
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
try {
|
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(
|
final lastServerStr = await storage.read(
|
||||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
iOptions: const IOSOptions(
|
iOptions: const IOSOptions(
|
||||||
|
|
@ -247,13 +175,12 @@ class FcmNotificationService {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
final threeDaysAgo = now.subtract(const Duration(days: 3));
|
||||||
|
|
||||||
DateTime? lastFcmTime;
|
// Recorded by the Rust notification worker, because neither platform
|
||||||
if (lastFcmStr != null) {
|
// starts Flutter for a background wake-up any more.
|
||||||
final ms = int.tryParse(lastFcmStr);
|
final lastFcmWakeup = userService.currentUser.lastFcmWakeupAt;
|
||||||
if (ms != null) {
|
final lastFcmTime = lastFcmWakeup == null
|
||||||
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
|
? null
|
||||||
}
|
: DateTime.fromMillisecondsSinceEpoch(lastFcmWakeup * 1000);
|
||||||
}
|
|
||||||
|
|
||||||
if (lastFcmTime != null) {
|
if (lastFcmTime != null) {
|
||||||
Log.info(
|
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');
|
final message = filterLogMessage('$messageInput');
|
||||||
Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace);
|
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 {
|
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/memory_item.model.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.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/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/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
import 'package:twonly/src/visual/components/contact_labels.comp.dart';
|
||||||
|
|
@ -354,10 +354,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
List<GroupHistory> groupActions, {
|
List<GroupHistory> groupActions, {
|
||||||
bool reportOpened = false,
|
bool reportOpened = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (reportOpened && _isViewActive()) {
|
|
||||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final msg in newMessages) {
|
for (final msg in newMessages) {
|
||||||
if (_animationState.hasReceivedFirstBatch &&
|
if (_animationState.hasReceivedFirstBatch &&
|
||||||
!_animationState.knownMessageIds.contains(msg.messageId) &&
|
!_animationState.knownMessageIds.contains(msg.messageId) &&
|
||||||
|
|
@ -435,12 +431,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
_animationState.reportedOpenedMessageIds.addAll(
|
_animationState.reportedOpenedMessageIds.addAll(
|
||||||
openedMessages[contactId]!,
|
openedMessages[contactId]!,
|
||||||
);
|
);
|
||||||
unawaited(
|
unawaited(_reportMessagesOpened(contactId, openedMessages[contactId]!));
|
||||||
RustApi.notifyMessagesOpened(
|
|
||||||
contactId: contactId,
|
|
||||||
messageIds: openedMessages[contactId]!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,6 +463,23 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
_updateGalleryItems(messages: storedMediaFiles);
|
_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}) {
|
void _updateGalleryItems({List<Message>? messages, bool force = false}) {
|
||||||
final storedMediaMessages =
|
final storedMediaMessages =
|
||||||
messages ??
|
messages ??
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/native.notifications.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_text.element.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/common.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.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();
|
_playerController.startPlayer();
|
||||||
if (widget.message.senderId != null &&
|
if (widget.message.senderId != null &&
|
||||||
widget.message.openedAt == null) {
|
widget.message.openedAt == null) {
|
||||||
RustApi.notifyMessagesOpened(
|
unawaited(_notifyMessageOpened());
|
||||||
contactId: widget.message.senderId!,
|
|
||||||
messageIds: [widget.message.messageId],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
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) {
|
String formatMsToMinSec(int milliseconds) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:screen_protector/screen_protector.dart';
|
import 'package:screen_protector/screen_protector.dart';
|
||||||
import 'package:twonly/globals.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.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/mediafiles/download.api.dart';
|
||||||
import 'package:twonly/src/services/api/utils.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/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/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.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();
|
final Mutex _messageUpdateLock = Mutex();
|
||||||
|
|
||||||
bool _isViewActive() {
|
|
||||||
if (!mounted) return false;
|
|
||||||
return !AppState.isAppInBackground &&
|
|
||||||
(ModalRoute.of(context)?.isCurrent ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> listenForUnopenedMedia(bool firstRun) async {
|
Future<void> listenForUnopenedMedia(bool firstRun) async {
|
||||||
_subscription = twonlyDB.messagesDao
|
_subscription = twonlyDB.messagesDao
|
||||||
.watchMediaNotOpened(widget.group.groupId)
|
.watchMediaNotOpened(widget.group.groupId)
|
||||||
|
|
@ -256,10 +249,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
showSendTextMessageInput = false;
|
showSendTextMessageInput = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_isViewActive()) {
|
|
||||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
final stream = twonlyDB.mediaFilesDao.watchMedia(
|
final stream = twonlyDB.mediaFilesDao.watchMedia(
|
||||||
allMediaFiles.first.mediaId!,
|
allMediaFiles.first.mediaId!,
|
||||||
);
|
);
|
||||||
|
|
@ -416,10 +405,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
markAsOpenMessageIDs = messageIds;
|
markAsOpenMessageIDs = messageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
await RustApi.notifyMessagesOpened(
|
await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs);
|
||||||
contactId: currentMessage!.senderId!,
|
try {
|
||||||
messageIds: markAsOpenMessageIDs,
|
await RustApi.notifyMessagesOpened(
|
||||||
);
|
contactId: currentMessage!.senderId!,
|
||||||
|
messageIds: markAsOpenMessageIDs,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
|
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/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.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/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/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
@ -50,6 +51,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||||
StreamSubscription<int>? _homeViewPageIndexSub;
|
StreamSubscription<int>? _homeViewPageIndexSub;
|
||||||
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
StreamSubscription<NotificationResponse>? _selectNotificationSub;
|
||||||
|
StreamSubscription<String?>? _nativeNotificationSub;
|
||||||
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
|
||||||
|
|
||||||
static Uri? pendingSharedLink;
|
static Uri? pendingSharedLink;
|
||||||
|
|
@ -113,6 +115,10 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_nativeNotificationSub = NativeNotificationService.taps.listen(
|
||||||
|
_openNativeNotification,
|
||||||
|
);
|
||||||
|
|
||||||
_sharedLinkSub = streamSharedLink.stream.listen((uri) {
|
_sharedLinkSub = streamSharedLink.stream.listen((uri) {
|
||||||
HomeViewState.pendingSharedLink = null;
|
HomeViewState.pendingSharedLink = null;
|
||||||
_mainCameraController.setSharedLinkForPreview(uri);
|
_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 {
|
Future<void> _initAsync() async {
|
||||||
|
final initialNativeTap = await NativeNotificationService.consumeInitialTap();
|
||||||
|
if (initialNativeTap != null) {
|
||||||
|
_openNativeNotification(initialNativeTap.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
||||||
.getNotificationAppLaunchDetails();
|
.getNotificationAppLaunchDetails();
|
||||||
|
|
||||||
|
|
@ -245,6 +264,7 @@ class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
_onMessageOpenedAppSub?.cancel();
|
_onMessageOpenedAppSub?.cancel();
|
||||||
_homeViewPageIndexSub?.cancel();
|
_homeViewPageIndexSub?.cancel();
|
||||||
_selectNotificationSub?.cancel();
|
_selectNotificationSub?.cancel();
|
||||||
|
_nativeNotificationSub?.cancel();
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
_mainCameraController.setState = null;
|
_mainCameraController.setState = null;
|
||||||
_mainCameraController.closeCamera();
|
_mainCameraController.closeCamera();
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ class SetupView extends StatefulWidget {
|
||||||
class _SetupViewState extends State<SetupView> {
|
class _SetupViewState extends State<SetupView> {
|
||||||
StreamSubscription<void>? _userUpdateStream;
|
StreamSubscription<void>? _userUpdateStream;
|
||||||
late UserDiscoverySetupState state;
|
late UserDiscoverySetupState state;
|
||||||
|
bool _setupDone = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -102,16 +103,27 @@ class _SetupViewState extends State<SetupView> {
|
||||||
if (widget.onUpdate != null) {
|
if (widget.onUpdate != null) {
|
||||||
_userUpdateStream = userService.onUserUpdated.listen((u) {
|
_userUpdateStream = userService.onUserUpdated.listen((u) {
|
||||||
if (userService.currentUser.currentSetupPage == null) {
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
_userUpdateStream?.cancel();
|
_userUpdateStream?.cancel();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -184,7 +196,7 @@ class _SetupViewState extends State<SetupView> {
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u.skipSetupPages = true,
|
(u) => u.skipSetupPages = true,
|
||||||
);
|
);
|
||||||
widget.onUpdate?.call();
|
_notifySetupDone();
|
||||||
},
|
},
|
||||||
variant: MyButtonVariant.text,
|
variant: MyButtonVariant.text,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,18 @@ class FinishSetupComp extends StatefulWidget {
|
||||||
|
|
||||||
class _FinishSetupCompState extends State<FinishSetupComp> {
|
class _FinishSetupCompState extends State<FinishSetupComp> {
|
||||||
Future<void> onTap() async {
|
Future<void> onTap() async {
|
||||||
await context.navPush(
|
// Captured before pushing so the callback never resolves the navigator
|
||||||
SetupView(
|
// through this widget's context, which lives below the pushed route.
|
||||||
onUpdate: () {
|
final navigator = Navigator.of(context);
|
||||||
if (mounted) {
|
await navigator.push<void>(
|
||||||
Navigator.pop(context);
|
MaterialPageRoute<void>(
|
||||||
}
|
builder: (_) => SetupView(
|
||||||
},
|
onUpdate: () {
|
||||||
|
if (mounted && navigator.canPop()) {
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ class DeveloperInformationsView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
String? _lastFcmTimestamp;
|
|
||||||
String? _lastServerTimestamp;
|
String? _lastServerTimestamp;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -26,13 +25,6 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
Future<void> _loadInformations({bool showFeedback = false}) async {
|
Future<void> _loadInformations({bool showFeedback = false}) async {
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
try {
|
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(
|
final lastServer = await storage.read(
|
||||||
key: SecureStorageKeys.lastServerMessageTimestamp,
|
key: SecureStorageKeys.lastServerMessageTimestamp,
|
||||||
iOptions: const IOSOptions(
|
iOptions: const IOSOptions(
|
||||||
|
|
@ -42,7 +34,6 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
);
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastFcmTimestamp = lastFcm;
|
|
||||||
_lastServerTimestamp = lastServer;
|
_lastServerTimestamp = lastServer;
|
||||||
});
|
});
|
||||||
if (showFeedback) {
|
if (showFeedback) {
|
||||||
|
|
@ -56,6 +47,14 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatFcmWakeup() {
|
||||||
|
final seconds = userService.currentUser.lastFcmWakeupAt;
|
||||||
|
if (seconds == null) return 'Never';
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
seconds * 1000,
|
||||||
|
).toLocal().toString();
|
||||||
|
}
|
||||||
|
|
||||||
String _formatTimestamp(String? timestampStr) {
|
String _formatTimestamp(String? timestampStr) {
|
||||||
if (timestampStr == null) return 'Never';
|
if (timestampStr == null) return 'Never';
|
||||||
final ms = int.tryParse(timestampStr);
|
final ms = int.tryParse(timestampStr);
|
||||||
|
|
@ -94,7 +93,7 @@ class _DeveloperInformationsViewState extends State<DeveloperInformationsView> {
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Last FCM Message'),
|
title: const Text('Last FCM Message'),
|
||||||
subtitle: Text(_formatTimestamp(_lastFcmTimestamp)),
|
subtitle: Text(_formatFcmWakeup()),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Last Server Message'),
|
title: const Text('Last Server Message'),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -82,5 +82,5 @@
|
||||||
true
|
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",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -22,5 +22,5 @@
|
||||||
false
|
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",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 2
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
true
|
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",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
|
@ -22,5 +22,5 @@
|
||||||
true
|
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