diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 3e129247..3953f290 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -59,6 +59,13 @@
+
+
+
when (call.method) {
@@ -107,6 +117,7 @@ class MainActivity : FlutterFragmentActivity() {
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
NotificationTapChannel.detach()
WidgetRuntimeChannel.detach()
+ WebxdcChannel.detach()
super.cleanUpFlutterEngine(flutterEngine)
}
}
diff --git a/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcChannel.kt b/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcChannel.kt
new file mode 100644
index 00000000..bf0887ed
--- /dev/null
+++ b/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcChannel.kt
@@ -0,0 +1,165 @@
+package eu.twonly.webxdc
+
+import android.app.Activity
+import android.os.Handler
+import android.os.Looper
+import android.webkit.WebStorage
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+
+/**
+ * The Dart side of the webxdc runtime.
+ *
+ * The webview is a platform view embedded in an ordinary Flutter route, so the
+ * app runs inside twonly rather than on a screen of its own. Everything the
+ * page asks for is answered by Dart, which asks Rust: nothing here reads a
+ * bundle, decides a limit, or trusts a value the page produced.
+ *
+ * Shares its channel name with the iOS implementation, so Dart talks to one
+ * channel on both platforms.
+ */
+object WebxdcChannel {
+ private const val CHANNEL = "eu.twonly/webxdc"
+
+ private var channel: MethodChannel? = null
+ private val main = Handler(Looper.getMainLooper())
+
+ fun configure(flutterEngine: FlutterEngine, activity: Activity) {
+ val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
+ channel.setMethodCallHandler { call, result -> handle(call, result) }
+ this.channel = channel
+
+ flutterEngine.platformViewsController.registry.registerViewFactory(
+ WebxdcViewFactory.VIEW_TYPE,
+ WebxdcViewFactory(activity),
+ )
+ }
+
+ fun detach() {
+ channel?.setMethodCallHandler(null)
+ channel = null
+ }
+
+ private fun handle(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "deliver" -> {
+ val instanceId = call.argument("instanceId")
+ val message = call.argument("message")
+ if (instanceId != null && message != null) {
+ WebxdcView.deliver(instanceId, message)
+ }
+ result.success(null)
+ }
+
+ "setPaused" -> {
+ // The screen showing the app is animating in or out, and a page
+ // that keeps drawing competes with the animation for the very
+ // frames it needs.
+ val instanceId = call.argument("instanceId")
+ val paused = call.argument("paused")
+ if (instanceId != null && paused != null) {
+ WebxdcView.setPaused(instanceId, paused)
+ }
+ result.success(null)
+ }
+
+ "clearOrigin" -> {
+ // The update log is only half an app's state; the rest is
+ // whatever it put in localStorage or IndexedDB, which only the
+ // WebView can delete.
+ val origin = call.argument("origin")
+ if (origin != null) {
+ WebStorage.getInstance().deleteOrigin(WebxdcView.originUrl(origin))
+ }
+ result.success(null)
+ }
+
+ else -> result.notImplemented()
+ }
+ }
+
+ /**
+ * Asks Dart for one file out of the bundle.
+ *
+ * Called on the WebView's background thread, which needs an answer before
+ * it returns, so the caller blocks while the channel call runs on the main
+ * thread. Different threads, so the wait cannot deadlock; the timeout
+ * covers an engine that has gone away.
+ */
+ fun serve(instanceId: String, path: String): ServedResponse? {
+ var response: ServedResponse? = null
+ awaitReply("serve", mapOf("instanceId" to instanceId, "path" to path), 10) { value ->
+ @Suppress("UNCHECKED_CAST")
+ val map = value as? Map ?: return@awaitReply
+ response = ServedResponse(
+ status = (map["status"] as? Number)?.toInt() ?: 500,
+ mime = map["mime"] as? String ?: "application/octet-stream",
+ headerNames = (map["headerNames"] as? List<*>)?.map { it.toString() } ?: emptyList(),
+ headerValues = (map["headerValues"] as? List<*>)?.map { it.toString() } ?: emptyList(),
+ body = map["body"] as? ByteArray ?: ByteArray(0),
+ )
+ }
+ return response
+ }
+
+ /** Forwards one `webxdc.js` call and returns the JSON reply for the page. */
+ fun bridge(instanceId: String, message: String): String {
+ var reply = "{\"error\":\"unavailable\"}"
+ awaitReply(
+ "bridge",
+ mapOf("instanceId" to instanceId, "message" to message),
+ 30,
+ ) { value ->
+ reply = value as? String ?: reply
+ }
+ return reply
+ }
+
+ /**
+ * Hands a link the user tapped to Dart, which shows the whole URL and asks
+ * before anything opens. Fire and forget: the page is not told.
+ */
+ fun openLink(url: String) {
+ main.post { channel?.invokeMethod("openLink", mapOf("url" to url)) }
+ }
+
+ private fun awaitReply(
+ method: String,
+ arguments: Map,
+ timeoutSeconds: Long,
+ onSuccess: (Any?) -> Unit,
+ ) {
+ val channel = this.channel ?: return
+ val latch = java.util.concurrent.CountDownLatch(1)
+ main.post {
+ channel.invokeMethod(
+ method,
+ arguments,
+ object : MethodChannel.Result {
+ override fun success(value: Any?) {
+ onSuccess(value)
+ latch.countDown()
+ }
+
+ override fun error(code: String, message: String?, details: Any?) {
+ latch.countDown()
+ }
+
+ override fun notImplemented() {
+ latch.countDown()
+ }
+ },
+ )
+ }
+ latch.await(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
+ }
+
+ data class ServedResponse(
+ val status: Int,
+ val mime: String,
+ val headerNames: List,
+ val headerValues: List,
+ val body: ByteArray,
+ )
+}
diff --git a/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcView.kt b/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcView.kt
new file mode 100644
index 00000000..b1a4c551
--- /dev/null
+++ b/android/app/src/main/kotlin/eu/twonly/webxdc/WebxdcView.kt
@@ -0,0 +1,425 @@
+package eu.twonly.webxdc
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.net.Uri
+import android.view.View
+import android.webkit.JavascriptInterface
+import android.webkit.PermissionRequest
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import io.flutter.plugin.common.StandardMessageCodec
+import io.flutter.plugin.platform.PlatformView
+import io.flutter.plugin.platform.PlatformViewFactory
+import java.io.ByteArrayInputStream
+import org.json.JSONArray
+import org.json.JSONObject
+
+class WebxdcViewFactory(private val activity: Activity) :
+ PlatformViewFactory(StandardMessageCodec.INSTANCE) {
+
+ companion object {
+ const val VIEW_TYPE = "eu.twonly/webxdc_webview"
+ }
+
+ override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
+ @Suppress("UNCHECKED_CAST")
+ val params = args as? Map ?: emptyMap()
+ return WebxdcView(
+ activity,
+ instanceId = params["instanceId"] as? String ?: "",
+ origin = params["origin"] as? String ?: "",
+ )
+ }
+}
+
+/**
+ * The one WebView twonly ever creates, embedded in a Flutter route.
+ *
+ * A webxdc app is third-party code, so this view is built around denying it
+ * things:
+ *
+ * - every request is answered from the bundle or refused; nothing reaches the
+ * network, because [WebViewClient.shouldInterceptRequest] never returns null
+ * for a request it did not serve itself,
+ * - the origin is unique per instance, so the browser's own origin model keeps
+ * one app's storage out of reach of every other app,
+ * - every permission the page can ask for is denied without a prompt,
+ * - navigation away from the app's own origin is cancelled, and a link the
+ * user taps goes to Dart for confirmation before it reaches a browser.
+ *
+ * The bundle is read in Rust and arrives here as bytes with the headers already
+ * attached; nothing here parses a zip or decides a policy.
+ */
+class WebxdcView(
+ private val activity: Activity,
+ private val instanceId: String,
+ private val origin: String,
+) : PlatformView {
+
+ companion object {
+ private const val IMPORT_REQUEST = 4711
+
+ /** Refuses to read more than this in one import, however many files. */
+ private const val MAX_IMPORT_BYTES = 32 * 1024 * 1024
+
+ private var current: WebxdcView? = null
+
+ /**
+ * `https` rather than a custom scheme, so the page is a secure context
+ * and `crypto.subtle`, IndexedDB and workers behave as apps expect.
+ * `.localhost` can never resolve, and nothing is ever fetched anyway.
+ */
+ fun originUrl(origin: String): String = "https://$origin.webxdc.localhost"
+
+ fun deliver(instanceId: String, message: String) {
+ val view = current ?: return
+ if (view.instanceId != instanceId) return
+ view.webView.post { view.deliverToPage(message) }
+ }
+
+ /** Stops or restarts the app while its screen is animating. */
+ fun setPaused(instanceId: String, paused: Boolean) {
+ val view = current ?: return
+ if (view.instanceId != instanceId) return
+ view.webView.post { view.setPaused(paused) }
+ }
+
+ /**
+ * Routes a picker result back to the view that opened it. Called by the
+ * activity, which is the only thing that receives one.
+ */
+ fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
+ if (requestCode != IMPORT_REQUEST) return false
+ current?.finishImport(resultCode, data)
+ return true
+ }
+ }
+
+ private val webView: WebView
+ /** The `importFiles` call waiting on the picker, if one is open. */
+ private var importCallId: Int? = null
+ private var importExtensions: List = emptyList()
+
+ init {
+ current = this
+ webView = WebView(activity)
+
+ @SuppressLint("SetJavaScriptEnabled")
+ webView.settings.apply {
+ javaScriptEnabled = true
+ // Apps keep their state here, and the unique origin is what keeps
+ // it to themselves.
+ domStorageEnabled = true
+
+ // No path a page could use to read the device's files.
+ allowFileAccess = false
+ allowContentAccess = false
+ @Suppress("DEPRECATION")
+ allowFileAccessFromFileURLs = false
+ @Suppress("DEPRECATION")
+ allowUniversalAccessFromFileURLs = false
+
+ setGeolocationEnabled(false)
+ javaScriptCanOpenWindowsAutomatically = false
+ setSupportMultipleWindows(false)
+ mediaPlaybackRequiresUserGesture = true
+ cacheMode = WebSettings.LOAD_NO_CACHE
+ mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
+ builtInZoomControls = false
+ displayZoomControls = false
+ }
+
+ // A remote debugger attached to a page running somebody else's code is
+ // not something a release build should offer.
+ WebView.setWebContentsDebuggingEnabled(
+ (activity.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0,
+ )
+
+ webView.addJavascriptInterface(Bridge(), "__twonlyWebxdcBridge")
+ webView.webViewClient = Client()
+ webView.webChromeClient = ChromeClient()
+
+ // Nothing is injected into the document. `webxdc.js` is served with its
+ // values already in it, and `__twonlyWebxdcBridge` exists before any
+ // page script runs, so there is no window in which app code could
+ // observe or race the setup.
+ webView.loadUrl("${originUrl(origin)}/index.html")
+ }
+
+ override fun getView(): View = webView
+
+ override fun dispose() {
+ if (current === this) {
+ current = null
+ }
+ // Stopped now, destroyed on the next turn of the main loop. A
+ // synchronous destroy spends its milliseconds on the frame that removes
+ // the view, which is the last frame of the closing animation and the
+ // one place the cost is visible.
+ webView.onPause()
+ webView.post {
+ // Timers are a process-wide setting, so they are handed back before
+ // this view goes: the next app opened must not start frozen.
+ webView.resumeTimers()
+ webView.destroy()
+ }
+ }
+
+ /**
+ * Rendering and timers, stopped while the route is animating away.
+ *
+ * A game redraws every frame it is given, and those are exactly the frames
+ * the transition needs. Nothing is lost by this: the page keeps its state,
+ * it is the webview's own pause rather than a reload.
+ */
+ private fun setPaused(paused: Boolean) {
+ if (paused) {
+ webView.onPause()
+ webView.pauseTimers()
+ } else {
+ webView.onResume()
+ webView.resumeTimers()
+ }
+ }
+
+ private fun quote(value: String): String = JSONObject.quote(value)
+
+ private fun deliverToPage(message: String) {
+ webView.evaluateJavascript(
+ "window.__twonlyWebxdcDeliver(JSON.parse(${quote(message)}))",
+ null,
+ )
+ }
+
+ /** The single entry point from the page into twonly. */
+ private inner class Bridge {
+ @JavascriptInterface
+ fun call(message: String): String {
+ // `importFiles` is answered here rather than in Dart: the picker
+ // is an activity result, and the bytes the user chose have no
+ // reason to travel any further than the page that asked for them.
+ val parsed = try {
+ JSONObject(message)
+ } catch (error: Exception) {
+ null
+ }
+ if (parsed != null && parsed.optString("method") == "importFiles") {
+ webView.post { startImport(parsed) }
+ // Empty means "answered later"; the page keeps waiting for a
+ // delivery rather than treating this as the reply.
+ return ""
+ }
+
+ // The instance is the one this view was created for. A page cannot
+ // name a different one, whatever it puts in the message.
+ return WebxdcChannel.bridge(instanceId, message)
+ }
+ }
+
+ private fun startImport(call: JSONObject) {
+ if (importCallId != null) {
+ // One picker at a time; a second request while one is open is
+ // answered empty rather than queued.
+ deliverImportResult(call.optInt("id"), JSONArray())
+ return
+ }
+ importCallId = call.optInt("id")
+
+ val params = call.optJSONObject("params") ?: JSONObject()
+ importExtensions = params.optJSONArray("extensions").toStringList()
+ .map { it.lowercase().removePrefix(".") }
+ val mimeTypes = params.optJSONArray("mimeTypes").toStringList()
+
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "*/*"
+ if (mimeTypes.isNotEmpty()) {
+ putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+ }
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, params.optBoolean("multiple", false))
+ }
+ try {
+ activity.startActivityForResult(intent, IMPORT_REQUEST)
+ } catch (error: Exception) {
+ deliverImportResult(importCallId ?: 0, JSONArray())
+ importCallId = null
+ }
+ }
+
+ private fun finishImport(resultCode: Int, data: Intent?) {
+ val callId = importCallId ?: return
+ importCallId = null
+
+ val files = JSONArray()
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ var budget = MAX_IMPORT_BYTES
+ for (uri in data.selectedUris()) {
+ val entry = readImportedFile(uri, budget) ?: continue
+ budget -= entry.second
+ files.put(entry.first)
+ if (budget <= 0) break
+ }
+ }
+ // A cancelled picker resolves with nothing rather than rejecting: the
+ // app asked to import, the user declined, and that is not an error.
+ deliverImportResult(callId, files)
+ }
+
+ private fun Intent.selectedUris(): List {
+ val clip = clipData
+ if (clip != null) {
+ return (0 until clip.itemCount).mapNotNull { clip.getItemAt(it).uri }
+ }
+ return listOfNotNull(data)
+ }
+
+ /** Returns the JSON for one file and how many bytes it cost. */
+ private fun readImportedFile(uri: Uri, budget: Int): Pair? {
+ val resolver = activity.contentResolver
+ val name = displayName(uri) ?: return null
+ if (importExtensions.isNotEmpty() &&
+ !importExtensions.contains(name.substringAfterLast('.', "").lowercase())
+ ) {
+ return null
+ }
+ // Grown as the file is read rather than allocated at the ceiling: the
+ // budget is what a file may not exceed, not what every file costs.
+ val bytes = try {
+ resolver.openInputStream(uri)?.use { stream ->
+ val collected = java.io.ByteArrayOutputStream()
+ val chunk = ByteArray(64 * 1024)
+ while (true) {
+ val read = stream.read(chunk)
+ if (read <= 0) break
+ if (collected.size() + read > budget) return null
+ collected.write(chunk, 0, read)
+ }
+ collected.toByteArray()
+ }
+ } catch (error: Exception) {
+ null
+ } ?: return null
+
+ val entry = JSONObject()
+ .put("name", name)
+ .put("type", resolver.getType(uri) ?: "")
+ .put("base64", android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP))
+ return entry to bytes.size
+ }
+
+ private fun displayName(uri: Uri): String? {
+ activity.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val index = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
+ if (index >= 0 && cursor.moveToFirst()) {
+ // Only the file's own name; a document provider is free to put
+ // anything in here, and it must not read as a path.
+ return cursor.getString(index)?.substringAfterLast('/')
+ }
+ }
+ return uri.lastPathSegment?.substringAfterLast('/')
+ }
+
+ private fun deliverImportResult(callId: Int, files: JSONArray) {
+ deliverToPage(JSONObject().put("id", callId).put("result", files).toString())
+ }
+
+ private fun JSONArray?.toStringList(): List {
+ if (this == null) return emptyList()
+ return (0 until length()).mapNotNull { optString(it).takeIf { value -> value.isNotEmpty() } }
+ }
+
+ private inner class Client : WebViewClient() {
+ override fun shouldInterceptRequest(
+ view: WebView,
+ request: WebResourceRequest,
+ ): WebResourceResponse? {
+ val url = request.url
+ if (!isOwnOrigin(url)) {
+ // Every other host, including the ones a CSS `url()` or an
+ // injected script might reach for. Refused here rather than by
+ // CSP, which has too much history of gaps to be the only
+ // control.
+ return refused()
+ }
+
+ val served = WebxdcChannel.serve(instanceId, url.encodedPath ?: "/")
+ ?: return refused()
+
+ val headers = LinkedHashMap()
+ served.headerNames.forEachIndexed { index, name ->
+ served.headerValues.getOrNull(index)?.let { headers[name] = it }
+ }
+
+ return WebResourceResponse(
+ served.mime.substringBefore(';'),
+ "utf-8",
+ served.status,
+ if (served.status == 200) "OK" else "Error",
+ headers,
+ ByteArrayInputStream(served.body),
+ )
+ }
+
+ override fun shouldOverrideUrlLoading(
+ view: WebView,
+ request: WebResourceRequest,
+ ): Boolean {
+ if (isOwnOrigin(request.url)) {
+ return false
+ }
+ // A link out of the app. Never followed here: Dart shows the whole
+ // URL and says it leaves twonly before anything opens.
+ if (request.hasGesture()) {
+ WebxdcChannel.openLink(request.url.toString())
+ }
+ return true
+ }
+
+ private fun refused(): WebResourceResponse = WebResourceResponse(
+ "text/plain",
+ "utf-8",
+ 403,
+ "Forbidden",
+ emptyMap(),
+ ByteArrayInputStream(ByteArray(0)),
+ )
+
+ private fun isOwnOrigin(url: Uri): Boolean =
+ url.scheme == "https" && url.host == "$origin.webxdc.localhost"
+ }
+
+ private inner class ChromeClient : WebChromeClient() {
+ /** Camera, microphone, midi, protected media: all of it, denied. */
+ override fun onPermissionRequest(request: PermissionRequest) = request.deny()
+
+ override fun onGeolocationPermissionsShowPrompt(
+ origin: String,
+ callback: android.webkit.GeolocationPermissions.Callback,
+ ) = callback.invoke(origin, false, false)
+
+ override fun onCreateWindow(
+ view: WebView,
+ isDialog: Boolean,
+ isUserGesture: Boolean,
+ resultMsg: android.os.Message,
+ ): Boolean = false
+
+ /** No file chooser: files cross the boundary only through importFiles. */
+ override fun onShowFileChooser(
+ webView: WebView,
+ filePathCallback: android.webkit.ValueCallback>,
+ fileChooserParams: FileChooserParams,
+ ): Boolean {
+ filePathCallback.onReceiveValue(null)
+ return true
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetConfigureActivity.kt b/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetConfigureActivity.kt
index 441d9d0d..7b907da2 100644
--- a/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetConfigureActivity.kt
+++ b/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetConfigureActivity.kt
@@ -85,12 +85,7 @@ class TwonlyWidgetConfigureActivity : AppCompatActivity() {
TwonlyWidgetProvider.preferences(this)
.edit().putStringSet(TwonlyWidgetProvider.groupsKey(widgetId), selected).apply()
persistNativeConfiguration(this)
- TwonlyWidgetProvider.update(
- this,
- AppWidgetManager.getInstance(this),
- widgetId,
- advance = false,
- )
+ TwonlyWidgetProvider.update(this, AppWidgetManager.getInstance(this), widgetId)
setResult(RESULT_OK, Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId))
finish()
}
diff --git a/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetProvider.kt b/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetProvider.kt
index 8d618335..988a8d46 100644
--- a/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetProvider.kt
+++ b/android/app/src/main/kotlin/eu/twonly/widget/TwonlyWidgetProvider.kt
@@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
-import android.content.Intent
import android.graphics.BitmapFactory
import android.view.View
import android.widget.RemoteViews
@@ -15,108 +14,72 @@ import org.json.JSONObject
class TwonlyWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, manager: AppWidgetManager, ids: IntArray) {
- ids.forEach { update(context, manager, it, advance = true) }
- }
-
- override fun onReceive(context: Context, intent: Intent) {
- super.onReceive(context, intent)
- if (intent.action != ACTION_ADVANCE) return
- val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
- if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
- update(context, AppWidgetManager.getInstance(context), widgetId, advance = true)
- }
+ ids.forEach { update(context, manager, it) }
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
val preferences = preferences(context)
- appWidgetIds.forEach { id ->
- preferences.edit().remove(groupsKey(id)).remove(indexKey(id)).remove(newestKey(id)).apply()
- }
+ appWidgetIds.forEach { id -> preferences.edit().remove(groupsKey(id)).apply() }
TwonlyWidgetConfigureActivity.persistNativeConfiguration(context)
}
companion object {
- const val ACTION_ADVANCE = "eu.twonly.widget.ADVANCE"
private const val PREFERENCES = "twonly_home_widgets"
fun preferences(context: Context) = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
fun groupsKey(widgetId: Int) = "groups_$widgetId"
- private fun indexKey(widgetId: Int) = "index_$widgetId"
-
- /** The newest image this widget has already drawn. */
- private fun newestKey(widgetId: Int) = "newest_$widgetId"
fun refreshAll(context: Context) {
val manager = AppWidgetManager.getInstance(context)
val ids = manager.getAppWidgetIds(ComponentName(context, TwonlyWidgetProvider::class.java))
- ids.forEach { update(context, manager, it, advance = false) }
+ ids.forEach { update(context, manager, it) }
}
- fun update(context: Context, manager: AppWidgetManager, widgetId: Int, advance: Boolean) {
+ /**
+ * Draws the one image this widget has: the most recent one the app
+ * published for any of its contact groups. The app keeps a single image
+ * per group and deletes the one it replaces, so the manifest β written
+ * newest first β offers nothing else to fall back on.
+ */
+ fun update(context: Context, manager: AppWidgetManager, widgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.twonly_widget)
val selected = preferences(context).getStringSet(groupsKey(widgetId), emptySet()).orEmpty()
val manifestFile = File(context.filesDir, "widget/manifest.json")
- val matching = runCatching {
+ val image = runCatching {
val images = JSONObject(manifestFile.readText()).getJSONArray("images")
- buildList {
- for (index in 0 until images.length()) {
- val image = images.getJSONObject(index)
- val groups = image.getJSONArray("group_ids")
- val visible = (0 until groups.length()).any { selected.contains(groups.getLong(it).toString()) }
- if (visible && image.optLong("expires_at") > System.currentTimeMillis() / 1000) add(image)
+ (0 until images.length())
+ .map { images.getJSONObject(it) }
+ .firstOrNull { candidate ->
+ val groups = candidate.getJSONArray("group_ids")
+ (0 until groups.length()).any { selected.contains(groups.getLong(it).toString()) }
}
- }
- }.getOrDefault(emptyList())
+ }.getOrNull()
- if (matching.isEmpty()) {
+ val bitmap = image?.let { BitmapFactory.decodeFile(it.getString("path")) }
+ if (bitmap == null) {
views.setImageViewResource(R.id.twonly_widget_image, R.drawable.logo)
views.setInt(R.id.twonly_widget_image, "setImageAlpha", 110)
views.setViewVisibility(R.id.twonly_widget_sender, View.GONE)
} else {
- // The manifest is newest first, so an arriving image is prepended
- // and the stored index keeps pointing at an older one: a redraw
- // alone would never show what just came in. Remembering which
- // image was newest last time is what separates an arrival from
- // every other reason this widget is asked to redraw, and an
- // arrival outranks the rotation β including a tap that has not
- // seen the new image yet.
- val preferences = preferences(context)
- val newest = matching[0].optString("media_id")
- val hasArrived = preferences.getString(newestKey(widgetId), null) != newest
- val oldIndex = preferences.getInt(indexKey(widgetId), -1)
- val index = when {
- hasArrived -> 0
- advance -> (oldIndex + 1).mod(matching.size)
- else -> oldIndex.coerceAtLeast(0).mod(matching.size)
- }
- preferences.edit().putInt(indexKey(widgetId), index).putString(newestKey(widgetId), newest).apply()
- val image = matching[index]
- val bitmap = BitmapFactory.decodeFile(image.getString("path"))
- if (bitmap == null) {
- views.setImageViewResource(R.id.twonly_widget_image, R.drawable.logo)
- views.setInt(R.id.twonly_widget_image, "setImageAlpha", 110)
- views.setViewVisibility(R.id.twonly_widget_sender, View.GONE)
- } else {
- views.setImageViewBitmap(R.id.twonly_widget_image, bitmap)
- views.setInt(R.id.twonly_widget_image, "setImageAlpha", 255)
- views.setTextViewText(R.id.twonly_widget_sender, image.optString("sender"))
- views.setViewVisibility(R.id.twonly_widget_sender, View.VISIBLE)
- }
+ views.setImageViewBitmap(R.id.twonly_widget_image, bitmap)
+ views.setInt(R.id.twonly_widget_image, "setImageAlpha", 255)
+ views.setTextViewText(R.id.twonly_widget_sender, image.optString("sender"))
+ views.setViewVisibility(R.id.twonly_widget_sender, View.VISIBLE)
}
- val intent = Intent(context, TwonlyWidgetProvider::class.java).apply {
- action = ACTION_ADVANCE
- putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+ // Nothing to rotate through any more, so a tap opens twonly.
+ val launch = context.packageManager.getLaunchIntentForPackage(context.packageName)
+ if (launch != null) {
+ views.setOnClickPendingIntent(
+ R.id.twonly_widget_root,
+ PendingIntent.getActivity(
+ context,
+ widgetId,
+ launch,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ ),
+ )
}
- views.setOnClickPendingIntent(
- R.id.twonly_widget_root,
- PendingIntent.getBroadcast(
- context,
- widgetId,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
- ),
- )
manager.updateAppWidget(widgetId, views)
}
}
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 5c6133a6..c704c950 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -31,6 +31,7 @@
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100042F70000100D1A002 /* NativeImageCodec.swift */; };
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100062F70000100D1A003 /* NativeVideoCodec.swift */; };
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100082F70000100D1A004 /* NativeGallery.swift */; };
+ D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
@@ -134,6 +135,7 @@
D3A100042F70000100D1A002 /* NativeImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeImageCodec.swift; sourceTree = ""; };
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoCodec.swift; sourceTree = ""; };
D3A100082F70000100D1A004 /* NativeGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeGallery.swift; sourceTree = ""; };
+ D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcHost.swift; sourceTree = ""; };
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
@@ -303,6 +305,7 @@
D3A100042F70000100D1A002 /* NativeImageCodec.swift */,
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */,
D3A100082F70000100D1A004 /* NativeGallery.swift */,
+ D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */,
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
D2265DD42D920142000D99BB /* Runner.entitlements */,
@@ -736,6 +739,7 @@
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */,
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */,
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */,
+ D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */,
D4A100022F81000100A10002 /* TwonlyWidgetShared.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 6a8a4eb1..c6097586 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -63,6 +63,7 @@ import flutter_sharing_intent
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
+ WebxdcHostChannel.register(with: engineBridge.pluginRegistry)
}
override func userNotificationCenter(
diff --git a/ios/Runner/WebxdcHost.swift b/ios/Runner/WebxdcHost.swift
new file mode 100644
index 00000000..93205cb5
--- /dev/null
+++ b/ios/Runner/WebxdcHost.swift
@@ -0,0 +1,470 @@
+import Flutter
+import UIKit
+import UniformTypeIdentifiers
+import WebKit
+
+/// The Dart side of the webxdc runtime.
+///
+/// The webview is a platform view embedded in an ordinary Flutter route, so the
+/// app runs inside twonly rather than on a screen of its own. Everything the
+/// page asks for is answered by Dart, which asks Rust: nothing here reads a
+/// bundle, decides a limit, or trusts a value the page produced.
+///
+/// Shares its channel name with the Android implementation, so Dart talks to
+/// one channel on both platforms.
+class WebxdcHostChannel: NSObject {
+ private static let channelName = "eu.twonly/webxdc"
+ static private(set) var shared: WebxdcHostChannel?
+
+ private var channel: FlutterMethodChannel?
+
+ static func register(with registry: FlutterPluginRegistry) {
+ guard let registrar = registry.registrar(forPlugin: "TwonlyWebxdc") else {
+ return
+ }
+ let host = WebxdcHostChannel()
+ let channel = FlutterMethodChannel(
+ name: channelName,
+ binaryMessenger: registrar.messenger()
+ )
+ host.channel = channel
+ channel.setMethodCallHandler { call, result in
+ host.handle(call, result: result)
+ }
+ shared = host
+
+ registrar.register(
+ WebxdcViewFactory(),
+ withId: WebxdcViewFactory.viewType
+ )
+ }
+
+ private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ let arguments = call.arguments as? [String: Any] ?? [:]
+
+ switch call.method {
+ case "deliver":
+ if let instanceId = arguments["instanceId"] as? String,
+ let message = arguments["message"] as? String
+ {
+ WebxdcPlatformView.deliver(instanceId: instanceId, message: message)
+ }
+ result(nil)
+
+ case "clearOrigin":
+ // The update log is only half an app's state; the rest is whatever it put
+ // in localStorage or IndexedDB, which only WebKit can delete.
+ guard let origin = arguments["origin"] as? String else {
+ result(nil)
+ return
+ }
+ let store = WKWebsiteDataStore.default()
+ store.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
+ let matching = records.filter { $0.displayName.contains(origin) }
+ store.removeData(
+ ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: matching
+ ) {
+ result(nil)
+ }
+ }
+
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
+ /// Asks Dart for one file out of the bundle.
+ func serve(
+ instanceId: String, path: String, completion: @escaping ([String: Any]?) -> Void
+ ) {
+ guard let channel = channel else {
+ completion(nil)
+ return
+ }
+ channel.invokeMethod("serve", arguments: ["instanceId": instanceId, "path": path]) { reply in
+ completion(reply as? [String: Any])
+ }
+ }
+
+ /// Forwards one `webxdc.js` call; the JSON reply goes back to the page.
+ func bridge(instanceId: String, message: String, completion: @escaping (String) -> Void) {
+ guard let channel = channel else {
+ completion("{\"error\":\"unavailable\"}")
+ return
+ }
+ channel.invokeMethod("bridge", arguments: ["instanceId": instanceId, "message": message]) {
+ reply in
+ completion(reply as? String ?? "{\"error\":\"unavailable\"}")
+ }
+ }
+
+ /// Hands a link the user tapped to Dart, which shows the whole URL and asks
+ /// before anything opens. Fire and forget: the page is not told.
+ func openLink(_ url: String) {
+ channel?.invokeMethod("openLink", arguments: ["url": url])
+ }
+}
+
+class WebxdcViewFactory: NSObject, FlutterPlatformViewFactory {
+ static let viewType = "eu.twonly/webxdc_webview"
+
+ func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
+ FlutterStandardMessageCodec.sharedInstance()
+ }
+
+ func create(
+ withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?
+ ) -> FlutterPlatformView {
+ let params = args as? [String: Any] ?? [:]
+ return WebxdcPlatformView(
+ frame: frame,
+ instanceId: params["instanceId"] as? String ?? "",
+ origin: params["origin"] as? String ?? ""
+ )
+ }
+}
+
+/// The one WKWebView twonly ever creates, embedded in a Flutter route.
+///
+/// A webxdc app is third-party code, so this view is built around denying it
+/// things:
+///
+/// - it is served from a custom scheme handled in-process, so every request it
+/// makes is answered from the bundle or refused, and nothing reaches the
+/// network,
+/// - its host is unique per instance, so WebKit's own origin model keeps one
+/// app's storage out of reach of every other app,
+/// - navigation away from that origin is cancelled, and a link the user taps
+/// goes to Dart for confirmation before it reaches Safari,
+/// - `webxdc.js` is served, not injected, so it carries the same CSP as the
+/// rest of the app and cannot race the page's own scripts.
+///
+/// Nothing here parses a bundle or decides a limit: the bytes and the headers
+/// come from Rust, by way of Dart.
+class WebxdcPlatformView: NSObject, FlutterPlatformView {
+ /// Handled in-process, so a request can never leave the device.
+ ///
+ /// It is not `https`, which WebKit will not let an app handle, so pages here
+ /// are not a secure context. That is an accepted limitation: webxdc apps do
+ /// not require one.
+ static let scheme = "twonly-webxdc"
+
+ /// Refuses to read more than this in one import, however many files.
+ private static let maxImportBytes = 32 * 1024 * 1024
+
+ private static weak var current: WebxdcPlatformView?
+
+ let instanceId: String
+ private let origin: String
+ private var webView: WKWebView!
+
+ /// The `importFiles` call waiting on the picker, if one is open.
+ private var importCallId: Int?
+ private var importExtensions: [String] = []
+
+ init(frame: CGRect, instanceId: String, origin: String) {
+ self.instanceId = instanceId
+ self.origin = origin
+ super.init()
+
+ let configuration = WKWebViewConfiguration()
+ configuration.setURLSchemeHandler(
+ SchemeHandler(view: self), forURLScheme: WebxdcPlatformView.scheme)
+ configuration.userContentController.add(BridgeHandler(view: self), name: "twonlyWebxdc")
+ // Nothing plays without the user asking for it, and no page may take over
+ // the screen on its own.
+ configuration.mediaTypesRequiringUserActionForPlayback = .all
+ configuration.allowsInlineMediaPlayback = true
+ configuration.allowsPictureInPictureMediaPlayback = false
+
+ let webView = WKWebView(frame: frame, configuration: configuration)
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ webView.navigationDelegate = self
+ webView.uiDelegate = self
+ webView.allowsBackForwardNavigationGestures = false
+ webView.allowsLinkPreview = false
+ self.webView = webView
+
+ WebxdcPlatformView.current = self
+ webView.load(URLRequest(url: URL(string: "\(originUrl())/index.html")!))
+ }
+
+ func view() -> UIView {
+ webView
+ }
+
+ private func originUrl() -> String {
+ "\(WebxdcPlatformView.scheme)://\(origin)"
+ }
+
+ static func deliver(instanceId: String, message: String) {
+ guard let view = current, view.instanceId == instanceId else { return }
+ view.deliverToPage(message)
+ }
+
+ func deliverToPage(_ message: String) {
+ guard let data = try? JSONSerialization.data(withJSONObject: [message]),
+ let quoted = String(data: data, encoding: .utf8)
+ else { return }
+ // `quoted` is a JSON array of one string; taking element zero hands the page
+ // a correctly escaped literal without building one by hand.
+ webView.evaluateJavaScript(
+ "window.__twonlyWebxdcDeliver(JSON.parse((\(quoted))[0]))", completionHandler: nil)
+ }
+
+ fileprivate func isOwnOrigin(_ url: URL?) -> Bool {
+ url?.scheme == WebxdcPlatformView.scheme && url?.host == origin
+ }
+
+ fileprivate func askHostToServe(
+ path: String, completion: @escaping ([String: Any]?) -> Void
+ ) {
+ guard let host = WebxdcHostChannel.shared else {
+ completion(nil)
+ return
+ }
+ host.serve(instanceId: instanceId, path: path, completion: completion)
+ }
+
+ fileprivate func askHostToBridge(_ message: String, completion: @escaping (String) -> Void) {
+ guard let host = WebxdcHostChannel.shared else {
+ completion("{\"error\":\"unavailable\"}")
+ return
+ }
+ host.bridge(instanceId: instanceId, message: message, completion: completion)
+ }
+
+ /// The controller a picker can be presented from. A platform view has no
+ /// controller of its own, so it borrows the one showing the Flutter route it
+ /// is embedded in.
+ private func presenter() -> UIViewController? {
+ var controller = UIApplication.shared.delegate?.window??.rootViewController
+ while let presented = controller?.presentedViewController {
+ controller = presented
+ }
+ return controller
+ }
+
+ /// Answers `importFiles` here rather than in Dart: the picker belongs to this
+ /// screen, and the bytes the user chose have no reason to travel any further
+ /// than the page that asked for them.
+ fileprivate func startImport(_ call: [String: Any]) {
+ let callId = call["id"] as? Int ?? 0
+ guard importCallId == nil else {
+ // One picker at a time; a second request while one is open is answered
+ // empty rather than queued.
+ deliverImportResult(callId: callId, files: [])
+ return
+ }
+ importCallId = callId
+
+ let params = call["params"] as? [String: Any] ?? [:]
+ importExtensions = (params["extensions"] as? [String] ?? []).map {
+ let lowered = $0.lowercased()
+ return lowered.hasPrefix(".") ? String(lowered.dropFirst()) : lowered
+ }
+ let mimeTypes = params["mimeTypes"] as? [String] ?? []
+
+ var types: [UTType] = mimeTypes.compactMap { UTType(mimeType: $0) }
+ types += importExtensions.compactMap { UTType(filenameExtension: $0) }
+ if types.isEmpty {
+ types = [.item]
+ }
+
+ let picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
+ picker.allowsMultipleSelection = params["multiple"] as? Bool ?? false
+ picker.delegate = self
+ guard let presenter = presenter() else {
+ finishImport(urls: [])
+ return
+ }
+ presenter.present(picker, animated: true)
+ }
+
+ fileprivate func finishImport(urls: [URL]) {
+ guard let callId = importCallId else { return }
+ importCallId = nil
+
+ var files: [[String: Any]] = []
+ var budget = WebxdcPlatformView.maxImportBytes
+
+ for url in urls {
+ let needsScope = url.startAccessingSecurityScopedResource()
+ defer { if needsScope { url.stopAccessingSecurityScopedResource() } }
+
+ // Only the file's own name; it must never read as a path.
+ let name = url.lastPathComponent
+ if !importExtensions.isEmpty
+ && !importExtensions.contains(url.pathExtension.lowercased())
+ {
+ continue
+ }
+ guard let data = try? Data(contentsOf: url), data.count <= budget else { continue }
+ budget -= data.count
+
+ files.append([
+ "name": name,
+ "type": UTType(filenameExtension: url.pathExtension)?.preferredMIMEType ?? "",
+ "base64": data.base64EncodedString(),
+ ])
+ if budget <= 0 { break }
+ }
+
+ deliverImportResult(callId: callId, files: files)
+ }
+
+ private func deliverImportResult(callId: Int, files: [[String: Any]]) {
+ let reply: [String: Any] = ["id": callId, "result": files]
+ guard let data = try? JSONSerialization.data(withJSONObject: reply),
+ let json = String(data: data, encoding: .utf8)
+ else { return }
+ deliverToPage(json)
+ }
+}
+
+extension WebxdcPlatformView: UIDocumentPickerDelegate {
+ func documentPicker(
+ _ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]
+ ) {
+ finishImport(urls: urls)
+ }
+
+ /// A cancelled picker resolves with nothing rather than rejecting: the app
+ /// asked to import, the user declined, and that is not an error.
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
+ finishImport(urls: [])
+ }
+}
+
+extension WebxdcPlatformView: WKNavigationDelegate {
+ func webView(
+ _ webView: WKWebView,
+ decidePolicyFor navigationAction: WKNavigationAction,
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
+ ) {
+ let url = navigationAction.request.url
+ if isOwnOrigin(url) {
+ decisionHandler(.allow)
+ return
+ }
+ // A link out of the app. Never followed here: Dart shows the whole URL and
+ // says it leaves twonly before anything opens.
+ decisionHandler(.cancel)
+ if navigationAction.navigationType == .linkActivated, let url = url {
+ WebxdcHostChannel.shared?.openLink(url.absoluteString)
+ }
+ }
+}
+
+extension WebxdcPlatformView: WKUIDelegate {
+ /// No second window, ever: `target="_blank"` and `window.open` both end here.
+ func webView(
+ _ webView: WKWebView,
+ createWebViewWith configuration: WKWebViewConfiguration,
+ for navigationAction: WKNavigationAction,
+ windowFeatures: WKWindowFeatures
+ ) -> WKWebView? {
+ nil
+ }
+
+ /// Camera and microphone, denied without a prompt.
+ @available(iOS 15.0, *)
+ func webView(
+ _ webView: WKWebView,
+ requestMediaCapturePermissionFor origin: WKSecurityOrigin,
+ initiatedByFrame frame: WKFrameInfo,
+ type: WKMediaCaptureType,
+ decisionHandler: @escaping (WKPermissionDecision) -> Void
+ ) {
+ decisionHandler(.deny)
+ }
+}
+
+/// Answers every request the page makes, or refuses it. There is no path from
+/// here to the network.
+private class SchemeHandler: NSObject, WKURLSchemeHandler {
+ private weak var view: WebxdcPlatformView?
+
+ init(view: WebxdcPlatformView) {
+ self.view = view
+ }
+
+ func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
+ guard let view = view, view.isOwnOrigin(urlSchemeTask.request.url) else {
+ finish(urlSchemeTask, status: 403, mime: "text/plain", headers: [:], body: Data())
+ return
+ }
+ let url = urlSchemeTask.request.url!
+ let path = url.path.isEmpty ? "/" : url.path
+
+ view.askHostToServe(path: path) { [weak self] served in
+ guard let self = self else { return }
+ guard let served = served else {
+ self.finish(urlSchemeTask, status: 500, mime: "text/plain", headers: [:], body: Data())
+ return
+ }
+
+ var headers: [String: String] = [:]
+ let names = served["headerNames"] as? [String] ?? []
+ let values = served["headerValues"] as? [String] ?? []
+ for (index, name) in names.enumerated() where index < values.count {
+ headers[name] = values[index]
+ }
+
+ let body = (served["body"] as? FlutterStandardTypedData)?.data ?? Data()
+ self.finish(
+ urlSchemeTask,
+ status: served["status"] as? Int ?? 500,
+ mime: served["mime"] as? String ?? "application/octet-stream",
+ headers: headers,
+ body: body
+ )
+ }
+ }
+
+ func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {}
+
+ private func finish(
+ _ task: WKURLSchemeTask, status: Int, mime: String, headers: [String: String], body: Data
+ ) {
+ var allHeaders = headers
+ allHeaders["Content-Type"] = mime
+ allHeaders["Content-Length"] = String(body.count)
+ guard let url = task.request.url,
+ let response = HTTPURLResponse(
+ url: url, statusCode: status, httpVersion: "HTTP/1.1", headerFields: allHeaders)
+ else { return }
+ task.didReceive(response)
+ task.didReceive(body)
+ task.didFinish()
+ }
+}
+
+/// The single entry point from the page into twonly.
+private class BridgeHandler: NSObject, WKScriptMessageHandler {
+ private weak var view: WebxdcPlatformView?
+
+ init(view: WebxdcPlatformView) {
+ self.view = view
+ }
+
+ func userContentController(
+ _ userContentController: WKUserContentController, didReceive message: WKScriptMessage
+ ) {
+ guard let view = view, let body = message.body as? String else { return }
+
+ if let data = body.data(using: .utf8),
+ let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ parsed["method"] as? String == "importFiles"
+ {
+ view.startImport(parsed)
+ return
+ }
+
+ // The instance is the one this view was created for. A page cannot name a
+ // different one, whatever it puts in the message.
+ view.askHostToBridge(body) { reply in
+ view.deliverToPage(reply)
+ }
+ }
+}
diff --git a/ios/Shared/TwonlyWidgetShared.swift b/ios/Shared/TwonlyWidgetShared.swift
index 03531453..f43e9d22 100644
--- a/ios/Shared/TwonlyWidgetShared.swift
+++ b/ios/Shared/TwonlyWidgetShared.swift
@@ -44,14 +44,17 @@ struct ManifestImage: Decodable {
let mediaId: String
let path: String
let sender: String
+ /// The contact groups this image is the current one for. The app publishes
+ /// one image per group, so a widget shows the newest image whose groups meet
+ /// its own selection and never has a second one to fall back on.
let groupIds: [Int64]
- let expiresAt: Int64
+ let receivedAt: Int64
enum CodingKeys: String, CodingKey {
case path, sender
case mediaId = "media_id"
case groupIds = "group_ids"
- case expiresAt = "expires_at"
+ case receivedAt = "received_at"
}
}
@@ -114,35 +117,6 @@ enum WidgetStorage {
ids.sorted().map(String.init).joined(separator: "-")
}
- static func index(for ids: [Int64]) -> Int {
- UserDefaults(suiteName: runtimeAppGroup)?.integer(forKey: "widget-index-\(selectionKey(ids))") ?? 0
- }
-
- /// Where the rotation starts, sent back to the front whenever an image has
- /// arrived since this selection was last drawn.
- ///
- /// The manifest is newest first, so an arriving image is prepended and a
- /// stored index keeps pointing at an older one: a timeline reload alone would
- /// never show what just came in. Recording which image was newest last time
- /// is what separates an arrival from every other reason a timeline is rebuilt.
- static func startIndex(for ids: [Int64], newestMediaId: String?) -> Int {
- guard let newestMediaId else { return index(for: ids) }
- let defaults = UserDefaults(suiteName: runtimeAppGroup)
- let newestKey = "widget-newest-\(selectionKey(ids))"
- guard defaults?.string(forKey: newestKey) != newestMediaId else {
- return index(for: ids)
- }
- defaults?.set(newestMediaId, forKey: newestKey)
- defaults?.set(0, forKey: "widget-index-\(selectionKey(ids))")
- return 0
- }
-
- static func advance(_ ids: [Int64]) {
- let defaults = UserDefaults(suiteName: runtimeAppGroup)
- let key = "widget-index-\(selectionKey(ids))"
- defaults?.set((defaults?.integer(forKey: key) ?? 0) + 1, forKey: key)
- }
-
/// Records this widget's contact groups for Rust to import.
///
/// Each placed widget builds its own timeline, so this only ever knows about
diff --git a/ios/TwonlyWidget/TwonlyWidget.swift b/ios/TwonlyWidget/TwonlyWidget.swift
index bb0f7c0b..067ceefb 100644
--- a/ios/TwonlyWidget/TwonlyWidget.swift
+++ b/ios/TwonlyWidget/TwonlyWidget.swift
@@ -4,22 +4,6 @@ import OSLog
import SwiftUI
import WidgetKit
-struct AdvanceWidgetIntent: AppIntent {
- static let title: LocalizedStringResource = "Next image"
- static let openAppWhenRun = false
-
- @Parameter(title: "Contact groups") var groupIds: [String]
-
- init() { groupIds = [] }
- init(groupIds: [Int64]) { self.groupIds = groupIds.map(String.init) }
-
- func perform() async throws -> some IntentResult {
- WidgetStorage.advance(groupIds.compactMap(Int64.init))
- WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
- return .result()
- }
-}
-
/// Why the widget has nothing to show, so the placeholder can say it.
enum EmptyReason: Equatable {
case notConfigured
@@ -31,8 +15,8 @@ enum EmptyReason: Equatable {
case .notConfigured:
return "Hold to choose contact groups"
case .noMatchingImages(let total):
- // The count separates "the app never delivered anything" from "images
- // arrived but carry other groups than the ones this widget selects".
+ // The count separates "the app never delivered anything" from "an image
+ // is being held for other groups than the ones this widget selects".
return total == 0 ? "No images yet" : "\(total) shared, none in your groups"
case .fault(let fault):
return fault.summary
@@ -43,18 +27,18 @@ enum EmptyReason: Equatable {
struct TwonlyEntry: TimelineEntry {
let date: Date
let image: ManifestImage?
- let groupIds: [Int64]
let emptyReason: EmptyReason?
/// Longest edge this entry will ever be drawn at, in pixels. Decoding to the
/// size actually shown is what keeps the extension inside its memory budget.
let maxPixelSize: CGFloat
}
-/// WidgetKit renders every timeline entry up front, so a timeline is a
-/// multiplier on whatever one entry costs. Eight half-hour steps keep the
-/// rotation going for four hours on a fraction of the memory a full day took.
-private let timelineEntryCount = 8
-private let timelineStepMinutes = 30
+/// A widget shows the image that arrived last and nothing else, so its
+/// timeline is a single entry. The app pushes a reload whenever the manifest
+/// changes, which is what actually puts a new image on the home screen; this
+/// interval only keeps the widget rebuilding while nothing arrives, because a
+/// rebuild is the sole evidence that the widget is still placed.
+private let timelineRefreshHours = 4
struct TwonlyProvider: AppIntentTimelineProvider {
/// The widget is never drawn larger than its container, so this is the most
@@ -68,7 +52,6 @@ struct TwonlyProvider: AppIntentTimelineProvider {
TwonlyEntry(
date: .now,
image: nil,
- groupIds: [],
emptyReason: .noMatchingImages(total: 0),
maxPixelSize: maxPixelSize(in: context)
)
@@ -79,7 +62,6 @@ struct TwonlyProvider: AppIntentTimelineProvider {
return entry(
images: WidgetStorage.images(),
ids: ids,
- index: WidgetStorage.index(for: ids),
date: .now,
maxPixelSize: maxPixelSize(in: context)
)
@@ -88,48 +70,24 @@ struct TwonlyProvider: AppIntentTimelineProvider {
func timeline(for configuration: TwonlyWidgetIntent, in context: Context) async -> Timeline {
let ids = (configuration.groups ?? []).compactMap { Int64($0.id) }
WidgetStorage.persistSelection(ids)
- // Read once: an extension that re-reads the manifest for every entry spends
- // its whole budget on JSON.
- let available = WidgetStorage.images()
- // The rotation starts at whatever just arrived, so an image shared into
- // this widget is on the home screen as soon as the timeline is rebuilt
- // rather than whenever the rotation next comes back around to it.
- let start = WidgetStorage.startIndex(
- for: ids,
- newestMediaId: newestMatching(in: available, ids: ids)?.mediaId
+ let entry = entry(
+ images: WidgetStorage.images(),
+ ids: ids,
+ date: .now,
+ maxPixelSize: maxPixelSize(in: context)
)
- let pixels = maxPixelSize(in: context)
- let entries = (0..,
- ids: [Int64]
- ) -> ManifestImage? {
- guard !ids.isEmpty, let available = try? images.get() else { return nil }
- let selected = Set(ids)
- let now = Int64(Date().timeIntervalSince1970)
- return available.first { $0.expiresAt > now && !selected.isDisjoint(with: $0.groupIds) }
+ let next =
+ Calendar.current.date(byAdding: .hour, value: timelineRefreshHours, to: .now) ?? .now
+ widgetLog.debug("built 1 entry at \(entry.maxPixelSize, privacy: .public)px")
+ return Timeline(entries: [entry], policy: .after(next))
}
+ /// The one image this widget shows: the most recent one published for any of
+ /// its contact groups. The manifest is written newest first, so that is
+ /// simply the first match.
private func entry(
images: Result<[ManifestImage], WidgetStorage.ManifestFault>,
ids: [Int64],
- index: Int,
date: Date,
maxPixelSize: CGFloat
) -> TwonlyEntry {
@@ -139,7 +97,7 @@ struct TwonlyProvider: AppIntentTimelineProvider {
available = value
case .failure(let fault):
return TwonlyEntry(
- date: date, image: nil, groupIds: ids,
+ date: date, image: nil,
emptyReason: .fault(fault), maxPixelSize: maxPixelSize)
}
@@ -148,36 +106,31 @@ struct TwonlyProvider: AppIntentTimelineProvider {
guard !ids.isEmpty else {
widgetLog.notice("no contact group configured; nothing can match")
return TwonlyEntry(
- date: date, image: nil, groupIds: ids,
+ date: date, image: nil,
emptyReason: .notConfigured, maxPixelSize: maxPixelSize)
}
let selected = Set(ids)
- let now = Int64(date.timeIntervalSince1970)
- let unexpired = available.filter { $0.expiresAt > now }
- let matching = unexpired.filter { !selected.isDisjoint(with: $0.groupIds) }
- guard !matching.isEmpty else {
- // The two counts separate "everything aged out" from "the sender is in
- // groups this widget did not select", which look identical on screen.
+ guard let newest = available.first(where: { !selected.isDisjoint(with: $0.groupIds) }) else {
+ // The offered groups are what separates "nobody has sent anything" from
+ // "the sender is in groups this widget did not select", which look
+ // identical on screen.
widgetLog.notice(
"""
no match: selected \(ids, privacy: .public), \
\(available.count, privacy: .public) images, \
- \(unexpired.count, privacy: .public) unexpired, \
offered groups \(Set(available.flatMap(\.groupIds)).sorted(), privacy: .public)
""")
return TwonlyEntry(
date: date,
image: nil,
- groupIds: ids,
emptyReason: .noMatchingImages(total: available.count),
maxPixelSize: maxPixelSize
)
}
return TwonlyEntry(
date: date,
- image: matching[index % matching.count],
- groupIds: ids,
+ image: newest,
emptyReason: nil,
maxPixelSize: maxPixelSize
)
@@ -224,13 +177,10 @@ struct TwonlyWidgetView: View {
var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .overlay {
- Button(intent: AdvanceWidgetIntent(groupIds: entry.groupIds)) {
- Color.clear
- }
- .buttonStyle(.plain)
- .accessibilityLabel("Show next image")
- }
+ // Opens the containing app. The scheme is not registered anywhere and
+ // does not need to be: WidgetKit hands the URL to twonly itself, and the
+ // app ignores links it does not recognise.
+ .widgetURL(URL(string: "twonly://widget"))
.containerBackground(for: .widget) { brandBackground }
}
diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart
index eb104bc9..4676f9f7 100644
--- a/lib/core/bridge/api.dart
+++ b/lib/core/bridge/api.dart
@@ -475,10 +475,12 @@ class RustApi {
required String groupId,
required String messageType,
required List additionalData,
+ required bool hidden,
}) => RustLib.instance.api.crateBridgeApiRustApiInsertAndSendAdditionalData(
groupId: groupId,
messageType: messageType,
additionalData: additionalData,
+ hidden: hidden,
);
static Future insertAndSendAskAboutUser({
@@ -501,10 +503,12 @@ class RustApi {
required String groupId,
required String text,
String? quoteMessageId,
+ Uint8List? additionalMessageData,
}) => RustLib.instance.api.crateBridgeApiRustApiInsertAndSendText(
groupId: groupId,
text: text,
quoteMessageId: quoteMessageId,
+ additionalMessageData: additionalMessageData,
);
static Future ipaPurchase({
diff --git a/lib/core/bridge/webxdc.dart b/lib/core/bridge/webxdc.dart
new file mode 100644
index 00000000..6ca06dd5
--- /dev/null
+++ b/lib/core/bridge/webxdc.dart
@@ -0,0 +1,260 @@
+// This file is automatically generated, so please do not edit it.
+// @generated by `flutter_rust_bridge`@ 2.12.0.
+
+// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
+
+import '../frb_generated.dart';
+import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
+
+/// Refreshes the store listing. Metadata only; no bundle is fetched.
+Future refreshCatalog() =>
+ RustLib.instance.api.crateBridgeWebxdcRefreshCatalog();
+
+/// What the in-app store offers, newest version of each app only.
+///
+/// `languages` is what the UI prefers, most preferred first. Descriptions are
+/// cached in every language the catalog carries and picked here, so which
+/// language a user reads is never sent anywhere.
+Future> catalog({required List languages}) =>
+ RustLib.instance.api.crateBridgeWebxdcCatalog(languages: languages);
+
+/// Places an app into a chat and returns the id of the message that carries it.
+Future createInstance({
+ required String groupId,
+ required String appId,
+ required PlatformInt64 version,
+}) => RustLib.instance.api.crateBridgeWebxdcCreateInstance(
+ groupId: groupId,
+ appId: appId,
+ version: version,
+);
+
+Future instance({required String instanceId}) =>
+ RustLib.instance.api.crateBridgeWebxdcInstance(instanceId: instanceId);
+
+/// Downloads and verifies the bundle if it is not already on disk, and returns
+/// where it landed. Called when the user starts an app, never on message
+/// arrival: a message must not be able to make a device fetch anything.
+Future prepareBundle({required String instanceId}) =>
+ RustLib.instance.api.crateBridgeWebxdcPrepareBundle(instanceId: instanceId);
+
+/// Answers one request the webview made for a file inside the bundle.
+///
+/// The platform shim has already established that the request came from this
+/// instance's own origin; everything after that is decided here.
+Future serve({
+ required String instanceId,
+ required String requestPath,
+}) => RustLib.instance.api.crateBridgeWebxdcServe(
+ instanceId: instanceId,
+ requestPath: requestPath,
+);
+
+Future> updatesAfter({
+ required String instanceId,
+ required PlatformInt64 serial,
+}) => RustLib.instance.api.crateBridgeWebxdcUpdatesAfter(
+ instanceId: instanceId,
+ serial: serial,
+);
+
+Future sendUpdate({
+ required String instanceId,
+ required String payload,
+ String? info,
+ String? href,
+ String? summary,
+ String? document,
+}) => RustLib.instance.api.crateBridgeWebxdcSendUpdate(
+ instanceId: instanceId,
+ payload: payload,
+ info: info,
+ href: href,
+ summary: summary,
+ document: document,
+);
+
+/// The address the running app sees for a participant. Stable inside one
+/// instance and unrelated to the same user's address in any other.
+Future addressFor({
+ required String instanceId,
+ required PlatformInt64 userId,
+}) => RustLib.instance.api.crateBridgeWebxdcAddressFor(
+ instanceId: instanceId,
+ userId: userId,
+);
+
+/// Drops an instance and its whole update log, returning the origin whose web
+/// storage the platform layer still has to clear. The log is only half the
+/// state: an app is free to keep everything in `localStorage`, which no
+/// database delete reaches.
+Future deleteInstance({required String instanceId}) => RustLib
+ .instance
+ .api
+ .crateBridgeWebxdcDeleteInstance(instanceId: instanceId);
+
+class WebxdcInstanceInfo {
+ final String instanceId;
+ final String groupId;
+ final String appId;
+ final PlatformInt64 version;
+
+ /// The webview host this instance is served from. Unique per instance, so
+ /// the browser's own origin model keeps one app's stored data out of reach
+ /// of every other app.
+ final String originToken;
+ final String? summary;
+ final String? document;
+
+ const WebxdcInstanceInfo({
+ required this.instanceId,
+ required this.groupId,
+ required this.appId,
+ required this.version,
+ required this.originToken,
+ this.summary,
+ this.document,
+ });
+
+ @override
+ int get hashCode =>
+ instanceId.hashCode ^
+ groupId.hashCode ^
+ appId.hashCode ^
+ version.hashCode ^
+ originToken.hashCode ^
+ summary.hashCode ^
+ document.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is WebxdcInstanceInfo &&
+ runtimeType == other.runtimeType &&
+ instanceId == other.instanceId &&
+ groupId == other.groupId &&
+ appId == other.appId &&
+ version == other.version &&
+ originToken == other.originToken &&
+ summary == other.summary &&
+ document == other.document;
+}
+
+class WebxdcResponse {
+ final int status;
+ final String mime;
+ final List headerNames;
+ final List headerValues;
+ final Uint8List body;
+
+ const WebxdcResponse({
+ required this.status,
+ required this.mime,
+ required this.headerNames,
+ required this.headerValues,
+ required this.body,
+ });
+
+ @override
+ int get hashCode =>
+ status.hashCode ^
+ mime.hashCode ^
+ headerNames.hashCode ^
+ headerValues.hashCode ^
+ body.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is WebxdcResponse &&
+ runtimeType == other.runtimeType &&
+ status == other.status &&
+ mime == other.mime &&
+ headerNames == other.headerNames &&
+ headerValues == other.headerValues &&
+ body == other.body;
+}
+
+class WebxdcStoreApp {
+ final String appId;
+ final PlatformInt64 version;
+
+ /// Already in the reader's language, like the description.
+ final String name;
+
+ /// One line about the app, already in the reader's language.
+ final String? description;
+ final String? sourceCodeUrl;
+ final Uint8List? icon;
+ final PlatformInt64 bundleBytes;
+
+ const WebxdcStoreApp({
+ required this.appId,
+ required this.version,
+ required this.name,
+ this.description,
+ this.sourceCodeUrl,
+ this.icon,
+ required this.bundleBytes,
+ });
+
+ @override
+ int get hashCode =>
+ appId.hashCode ^
+ version.hashCode ^
+ name.hashCode ^
+ description.hashCode ^
+ sourceCodeUrl.hashCode ^
+ icon.hashCode ^
+ bundleBytes.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is WebxdcStoreApp &&
+ runtimeType == other.runtimeType &&
+ appId == other.appId &&
+ version == other.version &&
+ name == other.name &&
+ description == other.description &&
+ sourceCodeUrl == other.sourceCodeUrl &&
+ icon == other.icon &&
+ bundleBytes == other.bundleBytes;
+}
+
+class WebxdcUpdateEntry {
+ final PlatformInt64 serial;
+ final String payload;
+ final String? info;
+ final String? href;
+
+ /// `None` when this device sent it.
+ final PlatformInt64? senderId;
+
+ const WebxdcUpdateEntry({
+ required this.serial,
+ required this.payload,
+ this.info,
+ this.href,
+ this.senderId,
+ });
+
+ @override
+ int get hashCode =>
+ serial.hashCode ^
+ payload.hashCode ^
+ info.hashCode ^
+ href.hashCode ^
+ senderId.hashCode;
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is WebxdcUpdateEntry &&
+ runtimeType == other.runtimeType &&
+ serial == other.serial &&
+ payload == other.payload &&
+ info == other.info &&
+ href == other.href &&
+ senderId == other.senderId;
+}
diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart
index a8175f98..fbfb801c 100644
--- a/lib/core/frb_generated.dart
+++ b/lib/core/frb_generated.dart
@@ -10,6 +10,7 @@ import 'bridge/callbacks.dart';
import 'bridge/groups.dart';
import 'bridge/logging.dart';
import 'bridge/user_config.dart';
+import 'bridge/webxdc.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/app_database.dart';
import 'bridge/wrapper/backup.dart';
@@ -86,7 +87,7 @@ class RustLib extends BaseEntrypoint {
String get codegenVersion => '2.12.0';
@override
- int get rustContentHash => -1545547125;
+ int get rustContentHash => 1362207365;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -107,15 +108,32 @@ abstract class RustLibApi extends BaseApi {
required Int64List memberIds,
});
+ Future crateBridgeWebxdcAddressFor({
+ required String instanceId,
+ required PlatformInt64 userId,
+ });
+
+ Future> crateBridgeWebxdcCatalog({
+ required List languages,
+ });
+
Future crateBridgeLoggingCleanLogFile();
Future crateBridgeLoggingClearLogFile();
+ Future crateBridgeWebxdcCreateInstance({
+ required String groupId,
+ required String appId,
+ required PlatformInt64 version,
+ });
+
Future crateBridgeGroupsCreateNewGroup({
required String groupName,
required Int64List memberIds,
});
+ Future crateBridgeWebxdcDeleteInstance({required String instanceId});
+
Future crateBridgeGroupsFetchGroupState({required String groupId});
Future crateBridgeGroupsFetchGroupStatesForUnjoinedGroups();
@@ -156,6 +174,10 @@ abstract class RustLibApi extends BaseApi {
required InitConfig config,
});
+ Future crateBridgeWebxdcInstance({
+ required String instanceId,
+ });
+
Future crateBridgeGroupsLeaveGroup({required String groupId});
Future crateBridgeLoggingLoadLogFile();
@@ -166,8 +188,12 @@ abstract class RustLibApi extends BaseApi {
required bool remove,
});
+ Future crateBridgeWebxdcPrepareBundle({required String instanceId});
+
Future crateBridgeLoggingReadLastLogLines({required int lineCount});
+ Future crateBridgeWebxdcRefreshCatalog();
+
Future crateBridgeGroupsRemoveMemberFromGroup({
required String groupId,
required PlatformInt64 contactId,
@@ -301,6 +327,7 @@ abstract class RustLibApi extends BaseApi {
required String groupId,
required String messageType,
required List additionalData,
+ required bool hidden,
});
Future crateBridgeApiRustApiInsertAndSendAskAboutUser({
@@ -317,6 +344,7 @@ abstract class RustLibApi extends BaseApi {
required String groupId,
required String text,
String? quoteMessageId,
+ Uint8List? additionalMessageData,
});
Future crateBridgeApiRustApiIpaPurchase({
@@ -664,6 +692,20 @@ abstract class RustLibApi extends BaseApi {
required int threshold,
});
+ Future crateBridgeWebxdcSendUpdate({
+ required String instanceId,
+ required String payload,
+ String? info,
+ String? href,
+ String? summary,
+ String? document,
+ });
+
+ Future crateBridgeWebxdcServe({
+ required String instanceId,
+ required String requestPath,
+ });
+
Future crateBridgeGroupsUpdateChatDeletionTime({
required String groupId,
required PlatformInt64 deleteMessagesAfterMilliseconds,
@@ -674,6 +716,11 @@ abstract class RustLibApi extends BaseApi {
required String groupName,
});
+ Future> crateBridgeWebxdcUpdatesAfter({
+ required String instanceId,
+ required PlatformInt64 serial,
+ });
+
UserConfig crateBridgeUserConfigUserConfigApiClone({
required UserConfig config,
});
@@ -785,6 +832,73 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["groupId", "memberIds"],
);
+ @override
+ Future crateBridgeWebxdcAddressFor({
+ required String instanceId,
+ required PlatformInt64 userId,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ sse_encode_i_64(userId, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 3,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_String,
+ decodeErrorData: null,
+ ),
+ constMeta: kCrateBridgeWebxdcAddressForConstMeta,
+ argValues: [instanceId, userId],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcAddressForConstMeta =>
+ const TaskConstMeta(
+ debugName: "address_for",
+ argNames: ["instanceId", "userId"],
+ );
+
+ @override
+ Future> crateBridgeWebxdcCatalog({
+ required List languages,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_list_String(languages, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 4,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_list_webxdc_store_app,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcCatalogConstMeta,
+ argValues: [languages],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcCatalogConstMeta => const TaskConstMeta(
+ debugName: "catalog",
+ argNames: ["languages"],
+ );
+
@override
Future crateBridgeLoggingCleanLogFile() {
return handler.executeNormal(
@@ -794,7 +908,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 3,
+ funcId: 5,
port: port_,
);
},
@@ -824,7 +938,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 4,
+ funcId: 6,
port: port_,
);
},
@@ -845,6 +959,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: [],
);
+ @override
+ Future crateBridgeWebxdcCreateInstance({
+ required String groupId,
+ required String appId,
+ required PlatformInt64 version,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(groupId, serializer);
+ sse_encode_String(appId, serializer);
+ sse_encode_i_64(version, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 7,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_String,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcCreateInstanceConstMeta,
+ argValues: [groupId, appId, version],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcCreateInstanceConstMeta =>
+ const TaskConstMeta(
+ debugName: "create_instance",
+ argNames: ["groupId", "appId", "version"],
+ );
+
@override
Future crateBridgeGroupsCreateNewGroup({
required String groupName,
@@ -859,7 +1010,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 5,
+ funcId: 8,
port: port_,
);
},
@@ -880,6 +1031,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["groupName", "memberIds"],
);
+ @override
+ Future crateBridgeWebxdcDeleteInstance({
+ required String instanceId,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 9,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_opt_String,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcDeleteInstanceConstMeta,
+ argValues: [instanceId],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcDeleteInstanceConstMeta =>
+ const TaskConstMeta(
+ debugName: "delete_instance",
+ argNames: ["instanceId"],
+ );
+
@override
Future crateBridgeGroupsFetchGroupState({required String groupId}) {
return handler.executeNormal(
@@ -890,7 +1074,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 6,
+ funcId: 10,
port: port_,
);
},
@@ -920,7 +1104,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 7,
+ funcId: 11,
port: port_,
);
},
@@ -956,7 +1140,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 8,
+ funcId: 12,
port: port_,
);
},
@@ -994,7 +1178,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 9,
+ funcId: 13,
port: port_,
);
},
@@ -1030,7 +1214,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 10,
+ funcId: 14,
port: port_,
);
},
@@ -1073,7 +1257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 11,
+ funcId: 15,
port: port_,
);
},
@@ -1118,7 +1302,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 12,
+ funcId: 16,
port: port_,
);
},
@@ -1155,7 +1339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 13,
+ funcId: 17,
port: port_,
);
},
@@ -1188,7 +1372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 14,
+ funcId: 18,
port: port_,
);
},
@@ -1209,6 +1393,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["config"],
);
+ @override
+ Future crateBridgeWebxdcInstance({
+ required String instanceId,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 19,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_opt_box_autoadd_webxdc_instance_info,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcInstanceConstMeta,
+ argValues: [instanceId],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcInstanceConstMeta => const TaskConstMeta(
+ debugName: "instance",
+ argNames: ["instanceId"],
+ );
+
@override
Future crateBridgeGroupsLeaveGroup({required String groupId}) {
return handler.executeNormal(
@@ -1219,7 +1435,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 15,
+ funcId: 20,
port: port_,
);
},
@@ -1249,7 +1465,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 16,
+ funcId: 21,
port: port_,
);
},
@@ -1286,7 +1502,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 17,
+ funcId: 22,
port: port_,
);
},
@@ -1307,6 +1523,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["groupId", "contactId", "remove"],
);
+ @override
+ Future crateBridgeWebxdcPrepareBundle({required String instanceId}) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 23,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_String,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcPrepareBundleConstMeta,
+ argValues: [instanceId],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcPrepareBundleConstMeta =>
+ const TaskConstMeta(
+ debugName: "prepare_bundle",
+ argNames: ["instanceId"],
+ );
+
@override
Future crateBridgeLoggingReadLastLogLines({required int lineCount}) {
return handler.executeNormal(
@@ -1317,7 +1564,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 18,
+ funcId: 24,
port: port_,
);
},
@@ -1338,6 +1585,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["lineCount"],
);
+ @override
+ Future crateBridgeWebxdcRefreshCatalog() {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 25,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_unit,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcRefreshCatalogConstMeta,
+ argValues: [],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcRefreshCatalogConstMeta =>
+ const TaskConstMeta(
+ debugName: "refresh_catalog",
+ argNames: [],
+ );
+
@override
Future crateBridgeGroupsRemoveMemberFromGroup({
required String groupId,
@@ -1352,7 +1629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 19,
+ funcId: 26,
port: port_,
);
},
@@ -1383,7 +1660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 20,
+ funcId: 27,
port: port_,
);
},
@@ -1416,7 +1693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 21,
+ funcId: 28,
port: port_,
);
},
@@ -1446,7 +1723,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 22,
+ funcId: 29,
port: port_,
);
},
@@ -1474,7 +1751,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(protocol, serializer);
- return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!;
+ return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -1502,7 +1779,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 24,
+ funcId: 31,
port: port_,
);
},
@@ -1534,7 +1811,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_i_64(contactId, serializer);
sse_encode_i_64(profileCounter, serializer);
- return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
+ return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -1563,7 +1840,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 26,
+ funcId: 33,
port: port_,
);
},
@@ -1593,7 +1870,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 27,
+ funcId: 34,
port: port_,
);
},
@@ -1634,7 +1911,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 28,
+ funcId: 35,
port: port_,
);
},
@@ -1675,7 +1952,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 29,
+ funcId: 36,
port: port_,
);
},
@@ -1710,7 +1987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 30,
+ funcId: 37,
port: port_,
);
},
@@ -1742,7 +2019,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 31,
+ funcId: 38,
port: port_,
);
},
@@ -1774,7 +2051,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 32,
+ funcId: 39,
port: port_,
);
},
@@ -1804,7 +2081,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 33,
+ funcId: 40,
port: port_,
);
},
@@ -1834,7 +2111,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 34,
+ funcId: 41,
port: port_,
);
},
@@ -1867,7 +2144,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 35,
+ funcId: 42,
port: port_,
);
},
@@ -1898,7 +2175,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 36,
+ funcId: 43,
port: port_,
);
},
@@ -1928,7 +2205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(avatarSvgCompressed, serializer);
- return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
+ return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 44)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -1956,7 +2233,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 38,
+ funcId: 45,
port: port_,
);
},
@@ -1987,7 +2264,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 39,
+ funcId: 46,
port: port_,
);
},
@@ -2020,7 +2297,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 40,
+ funcId: 47,
port: port_,
);
},
@@ -2050,7 +2327,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 41,
+ funcId: 48,
port: port_,
);
},
@@ -2081,7 +2358,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 42,
+ funcId: 49,
port: port_,
);
},
@@ -2112,7 +2389,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 43,
+ funcId: 50,
port: port_,
);
},
@@ -2142,7 +2419,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 44,
+ funcId: 51,
port: port_,
);
},
@@ -2175,7 +2452,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 45,
+ funcId: 52,
port: port_,
);
},
@@ -2210,7 +2487,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 46,
+ funcId: 53,
port: port_,
);
},
@@ -2243,7 +2520,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 47,
+ funcId: 54,
port: port_,
);
},
@@ -2275,7 +2552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 48,
+ funcId: 55,
port: port_,
);
},
@@ -2305,7 +2582,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 49,
+ funcId: 56,
port: port_,
);
},
@@ -2340,7 +2617,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 50,
+ funcId: 57,
port: port_,
);
},
@@ -2370,7 +2647,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 51,
+ funcId: 58,
port: port_,
);
},
@@ -2400,7 +2677,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 52,
+ funcId: 59,
port: port_,
);
},
@@ -2430,7 +2707,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 53,
+ funcId: 60,
port: port_,
);
},
@@ -2471,7 +2748,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 54,
+ funcId: 61,
port: port_,
);
},
@@ -2518,7 +2795,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 55,
+ funcId: 62,
port: port_,
);
},
@@ -2551,7 +2828,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 56,
+ funcId: 63,
port: port_,
);
},
@@ -2584,7 +2861,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 57,
+ funcId: 64,
port: port_,
);
},
@@ -2614,7 +2891,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 58,
+ funcId: 65,
port: port_,
);
},
@@ -2654,7 +2931,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 59,
+ funcId: 66,
port: port_,
);
},
@@ -2680,6 +2957,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
required String groupId,
required String messageType,
required List additionalData,
+ required bool hidden,
}) {
return handler.executeNormal(
NormalTask(
@@ -2688,10 +2966,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(groupId, serializer);
sse_encode_String(messageType, serializer);
sse_encode_list_prim_u_8_loose(additionalData, serializer);
+ sse_encode_bool(hidden, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 60,
+ funcId: 67,
port: port_,
);
},
@@ -2700,7 +2979,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeApiRustApiInsertAndSendAdditionalDataConstMeta,
- argValues: [groupId, messageType, additionalData],
+ argValues: [groupId, messageType, additionalData, hidden],
apiImpl: this,
),
);
@@ -2710,7 +2989,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeApiRustApiInsertAndSendAdditionalDataConstMeta =>
const TaskConstMeta(
debugName: "rust_api_insert_and_send_additional_data",
- argNames: ["groupId", "messageType", "additionalData"],
+ argNames: ["groupId", "messageType", "additionalData", "hidden"],
);
@override
@@ -2727,7 +3006,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 61,
+ funcId: 68,
port: port_,
);
},
@@ -2762,7 +3041,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 62,
+ funcId: 69,
port: port_,
);
},
@@ -2788,6 +3067,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
required String groupId,
required String text,
String? quoteMessageId,
+ Uint8List? additionalMessageData,
}) {
return handler.executeNormal(
NormalTask(
@@ -2796,10 +3076,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_String(groupId, serializer);
sse_encode_String(text, serializer);
sse_encode_opt_String(quoteMessageId, serializer);
+ sse_encode_opt_list_prim_u_8_strict(
+ additionalMessageData,
+ serializer,
+ );
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 63,
+ funcId: 70,
port: port_,
);
},
@@ -2808,7 +3092,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
decodeErrorData: sse_decode_AnyhowException,
),
constMeta: kCrateBridgeApiRustApiInsertAndSendTextConstMeta,
- argValues: [groupId, text, quoteMessageId],
+ argValues: [groupId, text, quoteMessageId, additionalMessageData],
apiImpl: this,
),
);
@@ -2817,7 +3101,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateBridgeApiRustApiInsertAndSendTextConstMeta =>
const TaskConstMeta(
debugName: "rust_api_insert_and_send_text",
- argNames: ["groupId", "text", "quoteMessageId"],
+ argNames: [
+ "groupId",
+ "text",
+ "quoteMessageId",
+ "additionalMessageData",
+ ],
);
@override
@@ -2836,7 +3125,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 64,
+ funcId: 71,
port: port_,
);
},
@@ -2866,7 +3155,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 65,
+ funcId: 72,
port: port_,
);
},
@@ -2899,7 +3188,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 66,
+ funcId: 73,
port: port_,
);
},
@@ -2934,7 +3223,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 67,
+ funcId: 74,
port: port_,
);
},
@@ -2964,7 +3253,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 68,
+ funcId: 75,
port: port_,
);
},
@@ -2999,7 +3288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 69,
+ funcId: 76,
port: port_,
);
},
@@ -3029,7 +3318,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 70,
+ funcId: 77,
port: port_,
);
},
@@ -3062,7 +3351,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 71,
+ funcId: 78,
port: port_,
);
},
@@ -3092,7 +3381,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 72,
+ funcId: 79,
port: port_,
);
},
@@ -3122,7 +3411,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 73,
+ funcId: 80,
port: port_,
);
},
@@ -3152,7 +3441,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 74,
+ funcId: 81,
port: port_,
);
},
@@ -3191,7 +3480,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 75,
+ funcId: 82,
port: port_,
);
},
@@ -3226,7 +3515,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 76,
+ funcId: 83,
port: port_,
);
},
@@ -3265,7 +3554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 77,
+ funcId: 84,
port: port_,
);
},
@@ -3307,7 +3596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 78,
+ funcId: 85,
port: port_,
);
},
@@ -3338,7 +3627,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 79,
+ funcId: 86,
port: port_,
);
},
@@ -3371,7 +3660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 80,
+ funcId: 87,
port: port_,
);
},
@@ -3404,7 +3693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 81,
+ funcId: 88,
port: port_,
);
},
@@ -3439,7 +3728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 82,
+ funcId: 89,
port: port_,
);
},
@@ -3472,7 +3761,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 83,
+ funcId: 90,
port: port_,
);
},
@@ -3505,7 +3794,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 84,
+ funcId: 91,
port: port_,
);
},
@@ -3538,7 +3827,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 85,
+ funcId: 92,
port: port_,
);
},
@@ -3575,7 +3864,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 86,
+ funcId: 93,
port: port_,
);
},
@@ -3605,7 +3894,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 87,
+ funcId: 94,
port: port_,
);
},
@@ -3635,7 +3924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 88,
+ funcId: 95,
port: port_,
);
},
@@ -3665,7 +3954,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 89,
+ funcId: 96,
port: port_,
);
},
@@ -3698,7 +3987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 90,
+ funcId: 97,
port: port_,
);
},
@@ -3729,7 +4018,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 91,
+ funcId: 98,
port: port_,
);
},
@@ -3762,7 +4051,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 92,
+ funcId: 99,
port: port_,
);
},
@@ -3808,7 +4097,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 93,
+ funcId: 100,
port: port_,
);
},
@@ -3864,7 +4153,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 94,
+ funcId: 101,
port: port_,
);
},
@@ -3912,7 +4201,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 95,
+ funcId: 102,
port: port_,
);
},
@@ -3950,7 +4239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 96,
+ funcId: 103,
port: port_,
);
},
@@ -3985,7 +4274,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 97,
+ funcId: 104,
port: port_,
);
},
@@ -4020,7 +4309,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 98,
+ funcId: 105,
port: port_,
);
},
@@ -4053,7 +4342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 99,
+ funcId: 106,
port: port_,
);
},
@@ -4090,7 +4379,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 100,
+ funcId: 107,
port: port_,
);
},
@@ -4121,7 +4410,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 101,
+ funcId: 108,
port: port_,
);
},
@@ -4159,7 +4448,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 102,
+ funcId: 109,
port: port_,
);
},
@@ -4194,7 +4483,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 103,
+ funcId: 110,
port: port_,
);
},
@@ -4233,7 +4522,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 104,
+ funcId: 111,
port: port_,
);
},
@@ -4266,7 +4555,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 105,
+ funcId: 112,
port: port_,
);
},
@@ -4297,7 +4586,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 106,
+ funcId: 113,
port: port_,
);
},
@@ -4332,7 +4621,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 107,
+ funcId: 114,
port: port_,
);
},
@@ -4362,7 +4651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 108,
+ funcId: 115,
port: port_,
);
},
@@ -4395,7 +4684,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 109,
+ funcId: 116,
port: port_,
);
},
@@ -4430,7 +4719,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 110,
+ funcId: 117,
port: port_,
);
},
@@ -4463,7 +4752,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 111,
+ funcId: 118,
port: port_,
);
},
@@ -4494,7 +4783,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 112,
+ funcId: 119,
port: port_,
);
},
@@ -4531,7 +4820,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 113,
+ funcId: 120,
port: port_,
);
},
@@ -4583,7 +4872,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 114,
+ funcId: 121,
port: port_,
);
},
@@ -4636,7 +4925,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 115,
+ funcId: 122,
port: port_,
);
},
@@ -4676,7 +4965,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 116,
+ funcId: 123,
port: port_,
);
},
@@ -4709,7 +4998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 117,
+ funcId: 124,
port: port_,
);
},
@@ -4742,7 +5031,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 118,
+ funcId: 125,
port: port_,
);
},
@@ -4779,7 +5068,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 119,
+ funcId: 126,
port: port_,
);
},
@@ -4811,7 +5100,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 120,
+ funcId: 127,
port: port_,
);
},
@@ -4844,7 +5133,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 121,
+ funcId: 128,
port: port_,
);
},
@@ -4879,7 +5168,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 122,
+ funcId: 129,
port: port_,
);
},
@@ -4911,7 +5200,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 123,
+ funcId: 130,
port: port_,
);
},
@@ -4949,7 +5238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 124,
+ funcId: 131,
port: port_,
);
},
@@ -4982,7 +5271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 125,
+ funcId: 132,
port: port_,
);
},
@@ -5020,7 +5309,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 126,
+ funcId: 133,
port: port_,
);
},
@@ -5057,7 +5346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 127,
+ funcId: 134,
port: port_,
);
},
@@ -5094,7 +5383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 128,
+ funcId: 135,
port: port_,
);
},
@@ -5132,7 +5421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 129,
+ funcId: 136,
port: port_,
);
},
@@ -5170,7 +5459,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 130,
+ funcId: 137,
port: port_,
);
},
@@ -5203,7 +5492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 131,
+ funcId: 138,
port: port_,
);
},
@@ -5235,7 +5524,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 132,
+ funcId: 139,
port: port_,
);
},
@@ -5270,7 +5559,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 133,
+ funcId: 140,
port: port_,
);
},
@@ -5307,7 +5596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 134,
+ funcId: 141,
port: port_,
);
},
@@ -5340,7 +5629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 135,
+ funcId: 142,
port: port_,
);
},
@@ -5372,7 +5661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 136,
+ funcId: 143,
port: port_,
);
},
@@ -5407,7 +5696,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 137,
+ funcId: 144,
port: port_,
);
},
@@ -5446,7 +5735,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 138,
+ funcId: 145,
port: port_,
);
},
@@ -5483,7 +5772,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 139,
+ funcId: 146,
port: port_,
);
},
@@ -5513,7 +5802,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 140,
+ funcId: 147,
port: port_,
);
},
@@ -5545,7 +5834,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 141,
+ funcId: 148,
port: port_,
);
},
@@ -5580,7 +5869,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 142,
+ funcId: 149,
port: port_,
);
},
@@ -5612,7 +5901,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 143,
+ funcId: 150,
port: port_,
);
},
@@ -5650,7 +5939,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 144,
+ funcId: 151,
port: port_,
);
},
@@ -5689,7 +5978,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 145,
+ funcId: 152,
port: port_,
);
},
@@ -5724,7 +6013,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 146,
+ funcId: 153,
port: port_,
);
},
@@ -5745,6 +6034,90 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["shares", "threshold"],
);
+ @override
+ Future crateBridgeWebxdcSendUpdate({
+ required String instanceId,
+ required String payload,
+ String? info,
+ String? href,
+ String? summary,
+ String? document,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ sse_encode_String(payload, serializer);
+ sse_encode_opt_String(info, serializer);
+ sse_encode_opt_String(href, serializer);
+ sse_encode_opt_String(summary, serializer);
+ sse_encode_opt_String(document, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 154,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_unit,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcSendUpdateConstMeta,
+ argValues: [instanceId, payload, info, href, summary, document],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcSendUpdateConstMeta =>
+ const TaskConstMeta(
+ debugName: "send_update",
+ argNames: [
+ "instanceId",
+ "payload",
+ "info",
+ "href",
+ "summary",
+ "document",
+ ],
+ );
+
+ @override
+ Future crateBridgeWebxdcServe({
+ required String instanceId,
+ required String requestPath,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ sse_encode_String(requestPath, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 155,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_webxdc_response,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcServeConstMeta,
+ argValues: [instanceId, requestPath],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcServeConstMeta => const TaskConstMeta(
+ debugName: "serve",
+ argNames: ["instanceId", "requestPath"],
+ );
+
@override
Future crateBridgeGroupsUpdateChatDeletionTime({
required String groupId,
@@ -5759,7 +6132,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 147,
+ funcId: 156,
port: port_,
);
},
@@ -5794,7 +6167,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 148,
+ funcId: 157,
port: port_,
);
},
@@ -5815,6 +6188,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["groupId", "groupName"],
);
+ @override
+ Future> crateBridgeWebxdcUpdatesAfter({
+ required String instanceId,
+ required PlatformInt64 serial,
+ }) {
+ return handler.executeNormal(
+ NormalTask(
+ callFfi: (port_) {
+ final serializer = SseSerializer(generalizedFrbRustBinding);
+ sse_encode_String(instanceId, serializer);
+ sse_encode_i_64(serial, serializer);
+ pdeCallFfi(
+ generalizedFrbRustBinding,
+ serializer,
+ funcId: 158,
+ port: port_,
+ );
+ },
+ codec: SseCodec(
+ decodeSuccessData: sse_decode_list_webxdc_update_entry,
+ decodeErrorData: sse_decode_AnyhowException,
+ ),
+ constMeta: kCrateBridgeWebxdcUpdatesAfterConstMeta,
+ argValues: [instanceId, serial],
+ apiImpl: this,
+ ),
+ );
+ }
+
+ TaskConstMeta get kCrateBridgeWebxdcUpdatesAfterConstMeta =>
+ const TaskConstMeta(
+ debugName: "updates_after",
+ argNames: ["instanceId", "serial"],
+ );
+
@override
UserConfig crateBridgeUserConfigUserConfigApiClone({
required UserConfig config,
@@ -5827,7 +6235,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 149,
+ funcId: 159,
)!;
},
codec: SseCodec(
@@ -5867,7 +6275,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 150,
+ funcId: 160,
port: port_,
);
},
@@ -5912,7 +6320,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 151,
+ funcId: 161,
port: port_,
);
},
@@ -5942,7 +6350,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 152,
+ funcId: 162,
port: port_,
);
},
@@ -5975,7 +6383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 153,
+ funcId: 163,
port: port_,
);
},
@@ -6010,7 +6418,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 154,
+ funcId: 164,
port: port_,
);
},
@@ -6049,7 +6457,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return pdeCallFfi(
generalizedFrbRustBinding,
serializer,
- funcId: 155,
+ funcId: 165,
)!;
},
codec: SseCodec(
@@ -6336,6 +6744,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_user_config(raw);
}
+ @protected
+ WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ return dco_decode_webxdc_instance_info(raw);
+ }
+
@protected
double dco_decode_f_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -6653,6 +7067,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (raw as List).map(dco_decode_sql_value).toList();
}
+ @protected
+ List dco_decode_list_webxdc_store_app(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ return (raw as List).map(dco_decode_webxdc_store_app).toList();
+ }
+
+ @protected
+ List dco_decode_list_webxdc_update_entry(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ return (raw as List).map(dco_decode_webxdc_update_entry).toList();
+ }
+
@protected
LogLevel dco_decode_log_level(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -6752,6 +7178,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_box_autoadd_user_config(raw);
}
+ @protected
+ WebxdcInstanceInfo? dco_decode_opt_box_autoadd_webxdc_instance_info(
+ dynamic raw,
+ ) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ return raw == null
+ ? null
+ : dco_decode_box_autoadd_webxdc_instance_info(raw);
+ }
+
@protected
List? dco_decode_opt_list_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -6979,6 +7415,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
+ @protected
+ int dco_decode_u_16(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ return raw as int;
+ }
+
@protected
int dco_decode_u_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -7100,6 +7542,70 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dcoDecodeU64(raw);
}
+ @protected
+ WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ final arr = raw as List;
+ if (arr.length != 7)
+ throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
+ return WebxdcInstanceInfo(
+ instanceId: dco_decode_String(arr[0]),
+ groupId: dco_decode_String(arr[1]),
+ appId: dco_decode_String(arr[2]),
+ version: dco_decode_i_64(arr[3]),
+ originToken: dco_decode_String(arr[4]),
+ summary: dco_decode_opt_String(arr[5]),
+ document: dco_decode_opt_String(arr[6]),
+ );
+ }
+
+ @protected
+ WebxdcResponse dco_decode_webxdc_response(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ final arr = raw as List;
+ if (arr.length != 5)
+ throw Exception('unexpected arr length: expect 5 but see ${arr.length}');
+ return WebxdcResponse(
+ status: dco_decode_u_16(arr[0]),
+ mime: dco_decode_String(arr[1]),
+ headerNames: dco_decode_list_String(arr[2]),
+ headerValues: dco_decode_list_String(arr[3]),
+ body: dco_decode_list_prim_u_8_strict(arr[4]),
+ );
+ }
+
+ @protected
+ WebxdcStoreApp dco_decode_webxdc_store_app(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ final arr = raw as List;
+ if (arr.length != 7)
+ throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
+ return WebxdcStoreApp(
+ appId: dco_decode_String(arr[0]),
+ version: dco_decode_i_64(arr[1]),
+ name: dco_decode_String(arr[2]),
+ description: dco_decode_opt_String(arr[3]),
+ sourceCodeUrl: dco_decode_opt_String(arr[4]),
+ icon: dco_decode_opt_list_prim_u_8_strict(arr[5]),
+ bundleBytes: dco_decode_i_64(arr[6]),
+ );
+ }
+
+ @protected
+ WebxdcUpdateEntry dco_decode_webxdc_update_entry(dynamic raw) {
+ // Codec=Dco (DartCObject based), see doc to use other codecs
+ final arr = raw as List;
+ if (arr.length != 5)
+ throw Exception('unexpected arr length: expect 5 but see ${arr.length}');
+ return WebxdcUpdateEntry(
+ serial: dco_decode_i_64(arr[0]),
+ payload: dco_decode_String(arr[1]),
+ info: dco_decode_opt_String(arr[2]),
+ href: dco_decode_opt_String(arr[3]),
+ senderId: dco_decode_opt_box_autoadd_i_64(arr[4]),
+ );
+ }
+
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -7299,6 +7805,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_user_config(deserializer));
}
+ @protected
+ WebxdcInstanceInfo sse_decode_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ return (sse_decode_webxdc_instance_info(deserializer));
+ }
+
@protected
double sse_decode_f_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -7705,6 +8219,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return ans_;
}
+ @protected
+ List sse_decode_list_webxdc_store_app(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+
+ var len_ = sse_decode_i_32(deserializer);
+ var ans_ = [];
+ for (var idx_ = 0; idx_ < len_; ++idx_) {
+ ans_.add(sse_decode_webxdc_store_app(deserializer));
+ }
+ return ans_;
+ }
+
+ @protected
+ List sse_decode_list_webxdc_update_entry(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+
+ var len_ = sse_decode_i_32(deserializer);
+ var ans_ = [];
+ for (var idx_ = 0; idx_ < len_; ++idx_) {
+ ans_.add(sse_decode_webxdc_update_entry(deserializer));
+ }
+ return ans_;
+ }
+
@protected
LogLevel sse_decode_log_level(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -7872,6 +8414,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
+ @protected
+ WebxdcInstanceInfo? sse_decode_opt_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+
+ if (sse_decode_bool(deserializer)) {
+ return (sse_decode_box_autoadd_webxdc_instance_info(deserializer));
+ } else {
+ return null;
+ }
+ }
+
@protected
List? sse_decode_opt_list_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -8100,6 +8655,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
+ @protected
+ int sse_decode_u_16(SseDeserializer deserializer) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ return deserializer.buffer.getUint16();
+ }
+
@protected
int sse_decode_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -8285,6 +8846,86 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getBigUint64();
}
+ @protected
+ WebxdcInstanceInfo sse_decode_webxdc_instance_info(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ var var_instanceId = sse_decode_String(deserializer);
+ var var_groupId = sse_decode_String(deserializer);
+ var var_appId = sse_decode_String(deserializer);
+ var var_version = sse_decode_i_64(deserializer);
+ var var_originToken = sse_decode_String(deserializer);
+ var var_summary = sse_decode_opt_String(deserializer);
+ var var_document = sse_decode_opt_String(deserializer);
+ return WebxdcInstanceInfo(
+ instanceId: var_instanceId,
+ groupId: var_groupId,
+ appId: var_appId,
+ version: var_version,
+ originToken: var_originToken,
+ summary: var_summary,
+ document: var_document,
+ );
+ }
+
+ @protected
+ WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ var var_status = sse_decode_u_16(deserializer);
+ var var_mime = sse_decode_String(deserializer);
+ var var_headerNames = sse_decode_list_String(deserializer);
+ var var_headerValues = sse_decode_list_String(deserializer);
+ var var_body = sse_decode_list_prim_u_8_strict(deserializer);
+ return WebxdcResponse(
+ status: var_status,
+ mime: var_mime,
+ headerNames: var_headerNames,
+ headerValues: var_headerValues,
+ body: var_body,
+ );
+ }
+
+ @protected
+ WebxdcStoreApp sse_decode_webxdc_store_app(SseDeserializer deserializer) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ var var_appId = sse_decode_String(deserializer);
+ var var_version = sse_decode_i_64(deserializer);
+ var var_name = sse_decode_String(deserializer);
+ var var_description = sse_decode_opt_String(deserializer);
+ var var_sourceCodeUrl = sse_decode_opt_String(deserializer);
+ var var_icon = sse_decode_opt_list_prim_u_8_strict(deserializer);
+ var var_bundleBytes = sse_decode_i_64(deserializer);
+ return WebxdcStoreApp(
+ appId: var_appId,
+ version: var_version,
+ name: var_name,
+ description: var_description,
+ sourceCodeUrl: var_sourceCodeUrl,
+ icon: var_icon,
+ bundleBytes: var_bundleBytes,
+ );
+ }
+
+ @protected
+ WebxdcUpdateEntry sse_decode_webxdc_update_entry(
+ SseDeserializer deserializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ var var_serial = sse_decode_i_64(deserializer);
+ var var_payload = sse_decode_String(deserializer);
+ var var_info = sse_decode_opt_String(deserializer);
+ var var_href = sse_decode_opt_String(deserializer);
+ var var_senderId = sse_decode_opt_box_autoadd_i_64(deserializer);
+ return WebxdcUpdateEntry(
+ serial: var_serial,
+ payload: var_payload,
+ info: var_info,
+ href: var_href,
+ senderId: var_senderId,
+ );
+ }
+
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -8553,6 +9194,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_user_config(self, serializer);
}
+ @protected
+ void sse_encode_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_webxdc_instance_info(self, serializer);
+ }
+
@protected
void sse_encode_f_64(double self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -8899,6 +9549,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
+ @protected
+ void sse_encode_list_webxdc_store_app(
+ List self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_i_32(self.length, serializer);
+ for (final item in self) {
+ sse_encode_webxdc_store_app(item, serializer);
+ }
+ }
+
+ @protected
+ void sse_encode_list_webxdc_update_entry(
+ List self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_i_32(self.length, serializer);
+ for (final item in self) {
+ sse_encode_webxdc_update_entry(item, serializer);
+ }
+ }
+
@protected
void sse_encode_log_level(LogLevel self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -9059,6 +9733,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
+ @protected
+ void sse_encode_opt_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo? self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+
+ sse_encode_bool(self != null, serializer);
+ if (self != null) {
+ sse_encode_box_autoadd_webxdc_instance_info(self, serializer);
+ }
+ }
+
@protected
void sse_encode_opt_list_String(
List? self,
@@ -9251,6 +9938,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_list_prim_u_8_strict(self.encryptionKey, serializer);
}
+ @protected
+ void sse_encode_u_16(int self, SseSerializer serializer) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ serializer.buffer.putUint16(self);
+ }
+
@protected
void sse_encode_u_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -9374,4 +10067,60 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putBigUint64(self);
}
+
+ @protected
+ void sse_encode_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_String(self.instanceId, serializer);
+ sse_encode_String(self.groupId, serializer);
+ sse_encode_String(self.appId, serializer);
+ sse_encode_i_64(self.version, serializer);
+ sse_encode_String(self.originToken, serializer);
+ sse_encode_opt_String(self.summary, serializer);
+ sse_encode_opt_String(self.document, serializer);
+ }
+
+ @protected
+ void sse_encode_webxdc_response(
+ WebxdcResponse self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_u_16(self.status, serializer);
+ sse_encode_String(self.mime, serializer);
+ sse_encode_list_String(self.headerNames, serializer);
+ sse_encode_list_String(self.headerValues, serializer);
+ sse_encode_list_prim_u_8_strict(self.body, serializer);
+ }
+
+ @protected
+ void sse_encode_webxdc_store_app(
+ WebxdcStoreApp self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_String(self.appId, serializer);
+ sse_encode_i_64(self.version, serializer);
+ sse_encode_String(self.name, serializer);
+ sse_encode_opt_String(self.description, serializer);
+ sse_encode_opt_String(self.sourceCodeUrl, serializer);
+ sse_encode_opt_list_prim_u_8_strict(self.icon, serializer);
+ sse_encode_i_64(self.bundleBytes, serializer);
+ }
+
+ @protected
+ void sse_encode_webxdc_update_entry(
+ WebxdcUpdateEntry self,
+ SseSerializer serializer,
+ ) {
+ // Codec=Sse (Serialization based), see doc to use other codecs
+ sse_encode_i_64(self.serial, serializer);
+ sse_encode_String(self.payload, serializer);
+ sse_encode_opt_String(self.info, serializer);
+ sse_encode_opt_String(self.href, serializer);
+ sse_encode_opt_box_autoadd_i_64(self.senderId, serializer);
+ }
}
diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart
index 1d72cf5d..7df08c07 100644
--- a/lib/core/frb_generated.io.dart
+++ b/lib/core/frb_generated.io.dart
@@ -10,6 +10,7 @@ import 'bridge/callbacks.dart';
import 'bridge/groups.dart';
import 'bridge/logging.dart';
import 'bridge/user_config.dart';
+import 'bridge/webxdc.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/app_database.dart';
import 'bridge/wrapper/backup.dart';
@@ -125,6 +126,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
+ @protected
+ WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
+
@protected
double dco_decode_f_64(dynamic raw);
@@ -234,6 +238,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
List dco_decode_list_sql_value(dynamic raw);
+ @protected
+ List dco_decode_list_webxdc_store_app(dynamic raw);
+
+ @protected
+ List dco_decode_list_webxdc_update_entry(dynamic raw);
+
@protected
LogLevel dco_decode_log_level(dynamic raw);
@@ -281,6 +291,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
+ @protected
+ WebxdcInstanceInfo? dco_decode_opt_box_autoadd_webxdc_instance_info(
+ dynamic raw,
+ );
+
@protected
List? dco_decode_opt_list_String(dynamic raw);
@@ -348,6 +363,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
+ @protected
+ int dco_decode_u_16(dynamic raw);
+
@protected
int dco_decode_u_32(dynamic raw);
@@ -372,6 +390,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
BigInt dco_decode_usize(dynamic raw);
+ @protected
+ WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw);
+
+ @protected
+ WebxdcResponse dco_decode_webxdc_response(dynamic raw);
+
+ @protected
+ WebxdcStoreApp dco_decode_webxdc_store_app(dynamic raw);
+
+ @protected
+ WebxdcUpdateEntry dco_decode_webxdc_update_entry(dynamic raw);
+
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@@ -475,6 +505,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
+ @protected
+ WebxdcInstanceInfo sse_decode_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@@ -606,6 +641,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
List sse_decode_list_sql_value(SseDeserializer deserializer);
+ @protected
+ List sse_decode_list_webxdc_store_app(
+ SseDeserializer deserializer,
+ );
+
+ @protected
+ List sse_decode_list_webxdc_update_entry(
+ SseDeserializer deserializer,
+ );
+
@protected
LogLevel sse_decode_log_level(SseDeserializer deserializer);
@@ -661,6 +706,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseDeserializer deserializer,
);
+ @protected
+ WebxdcInstanceInfo? sse_decode_opt_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
@protected
List? sse_decode_opt_list_String(SseDeserializer deserializer);
@@ -738,6 +788,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
+ @protected
+ int sse_decode_u_16(SseDeserializer deserializer);
+
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@@ -762,6 +815,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
+ @protected
+ WebxdcInstanceInfo sse_decode_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
+ @protected
+ WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer);
+
+ @protected
+ WebxdcStoreApp sse_decode_webxdc_store_app(SseDeserializer deserializer);
+
+ @protected
+ WebxdcUpdateEntry sse_decode_webxdc_update_entry(
+ SseDeserializer deserializer,
+ );
+
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -903,6 +972,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@@ -1068,6 +1143,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
void sse_encode_list_sql_value(List self, SseSerializer serializer);
+ @protected
+ void sse_encode_list_webxdc_store_app(
+ List self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_list_webxdc_update_entry(
+ List self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_log_level(LogLevel self, SseSerializer serializer);
@@ -1137,6 +1224,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_opt_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo? self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_opt_list_String(List? self, SseSerializer serializer);
@@ -1236,6 +1329,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_u_16(int self, SseSerializer serializer);
+
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@@ -1259,6 +1355,30 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
+
+ @protected
+ void sse_encode_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_response(
+ WebxdcResponse self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_store_app(
+ WebxdcStoreApp self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_update_entry(
+ WebxdcUpdateEntry self,
+ SseSerializer serializer,
+ );
}
// Section: wire_class
diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart
index 1f9d6bcb..6ba83798 100644
--- a/lib/core/frb_generated.web.dart
+++ b/lib/core/frb_generated.web.dart
@@ -13,6 +13,7 @@ import 'bridge/callbacks.dart';
import 'bridge/groups.dart';
import 'bridge/logging.dart';
import 'bridge/user_config.dart';
+import 'bridge/webxdc.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/app_database.dart';
import 'bridge/wrapper/backup.dart';
@@ -127,6 +128,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
+ @protected
+ WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
+
@protected
double dco_decode_f_64(dynamic raw);
@@ -236,6 +240,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
List dco_decode_list_sql_value(dynamic raw);
+ @protected
+ List dco_decode_list_webxdc_store_app(dynamic raw);
+
+ @protected
+ List dco_decode_list_webxdc_update_entry(dynamic raw);
+
@protected
LogLevel dco_decode_log_level(dynamic raw);
@@ -283,6 +293,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
+ @protected
+ WebxdcInstanceInfo? dco_decode_opt_box_autoadd_webxdc_instance_info(
+ dynamic raw,
+ );
+
@protected
List? dco_decode_opt_list_String(dynamic raw);
@@ -350,6 +365,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
+ @protected
+ int dco_decode_u_16(dynamic raw);
+
@protected
int dco_decode_u_32(dynamic raw);
@@ -374,6 +392,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
BigInt dco_decode_usize(dynamic raw);
+ @protected
+ WebxdcInstanceInfo dco_decode_webxdc_instance_info(dynamic raw);
+
+ @protected
+ WebxdcResponse dco_decode_webxdc_response(dynamic raw);
+
+ @protected
+ WebxdcStoreApp dco_decode_webxdc_store_app(dynamic raw);
+
+ @protected
+ WebxdcUpdateEntry dco_decode_webxdc_update_entry(dynamic raw);
+
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@@ -477,6 +507,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
+ @protected
+ WebxdcInstanceInfo sse_decode_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
@protected
double sse_decode_f_64(SseDeserializer deserializer);
@@ -608,6 +643,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
List sse_decode_list_sql_value(SseDeserializer deserializer);
+ @protected
+ List sse_decode_list_webxdc_store_app(
+ SseDeserializer deserializer,
+ );
+
+ @protected
+ List sse_decode_list_webxdc_update_entry(
+ SseDeserializer deserializer,
+ );
+
@protected
LogLevel sse_decode_log_level(SseDeserializer deserializer);
@@ -663,6 +708,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseDeserializer deserializer,
);
+ @protected
+ WebxdcInstanceInfo? sse_decode_opt_box_autoadd_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
@protected
List? sse_decode_opt_list_String(SseDeserializer deserializer);
@@ -740,6 +790,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
+ @protected
+ int sse_decode_u_16(SseDeserializer deserializer);
+
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@@ -764,6 +817,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
+ @protected
+ WebxdcInstanceInfo sse_decode_webxdc_instance_info(
+ SseDeserializer deserializer,
+ );
+
+ @protected
+ WebxdcResponse sse_decode_webxdc_response(SseDeserializer deserializer);
+
+ @protected
+ WebxdcStoreApp sse_decode_webxdc_store_app(SseDeserializer deserializer);
+
+ @protected
+ WebxdcUpdateEntry sse_decode_webxdc_update_entry(
+ SseDeserializer deserializer,
+ );
+
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -905,6 +974,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_f_64(double self, SseSerializer serializer);
@@ -1070,6 +1145,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
void sse_encode_list_sql_value(List self, SseSerializer serializer);
+ @protected
+ void sse_encode_list_webxdc_store_app(
+ List self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_list_webxdc_update_entry(
+ List self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_log_level(LogLevel self, SseSerializer serializer);
@@ -1139,6 +1226,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_opt_box_autoadd_webxdc_instance_info(
+ WebxdcInstanceInfo? self,
+ SseSerializer serializer,
+ );
+
@protected
void sse_encode_opt_list_String(List? self, SseSerializer serializer);
@@ -1238,6 +1331,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
SseSerializer serializer,
);
+ @protected
+ void sse_encode_u_16(int self, SseSerializer serializer);
+
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@@ -1261,6 +1357,30 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl {
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
+
+ @protected
+ void sse_encode_webxdc_instance_info(
+ WebxdcInstanceInfo self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_response(
+ WebxdcResponse self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_store_app(
+ WebxdcStoreApp self,
+ SseSerializer serializer,
+ );
+
+ @protected
+ void sse_encode_webxdc_update_entry(
+ WebxdcUpdateEntry self,
+ SseSerializer serializer,
+ );
}
// Section: wire_class
diff --git a/lib/main.dart b/lib/main.dart
index 6580ef72..1e8dede2 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -28,6 +28,7 @@ import 'package:twonly/src/services/migrations.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/native.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
+import 'package:twonly/src/services/webxdc/webxdc_host.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/startup_guard.dart';
import 'package:twonly/src/visual/themes/light.dart';
@@ -190,6 +191,9 @@ Future _startup(SettingsChangeProvider settings) async {
await notificationSetup;
NativeNotificationService.init();
+ // Claims the channel the native webview talks to. Nothing runs in it until
+ // a user starts an app from a chat.
+ WebxdcHost.initialize();
// The theme was read before the user was known, so re-read it now that it
// is. Repainting the splash costs nothing; the app UI is built after this.
@@ -289,7 +293,7 @@ Future postStartupTasks() async {
await twonlyDB.messagesDao.purgeMessageTable();
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
unawaited(MediaFileService.purgeTempFolder());
- unawaited(HomeWidgetService.purgeExpiredMedia());
+ unawaited(HomeWidgetService.pruneSupersededMedia());
unawaited(HomeWidgetService.syncPermissions());
// 2. Service initializations
diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart
index c9035292..3113d38a 100644
--- a/lib/src/constants/routes.keys.dart
+++ b/lib/src/constants/routes.keys.dart
@@ -37,7 +37,6 @@ class Routes {
static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery';
static const String settingsNotification = '/settings/notification';
- static const String settingsWidgets = '/settings/widgets';
static const String settingsStorage = '/settings/storage_data';
static const String settingsStorageManage = '/settings/storage_data/manage';
static const String settingsStorageImport = '/settings/storage_data/import';
diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart
index 3d71806d..72e63240 100644
--- a/lib/src/database/daos/messages.dao.dart
+++ b/lib/src/database/daos/messages.dao.dart
@@ -140,7 +140,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin {
// so ensuring that this message is not shown in the messages anymore
(messages.openedAt.isBiggerThanValue(deletionTime) |
messages.openedAt.isNull() |
- messages.mediaStored.equals(true)) &
+ messages.mediaStored.equals(true) |
+ messages.type.equals(MessageType.webxdcApp.name)) &
(mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name)
.not() |
@@ -208,7 +209,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin {
messages.groupId.equals(groupId) &
(messages.openedAt.isBiggerThanValue(deletionTime) |
messages.openedAt.isNull() |
- messages.mediaStored.equals(true)) &
+ messages.mediaStored.equals(true) |
+ messages.type.equals(MessageType.webxdcApp.name)) &
(messages.isDeletedFromSender.equals(true) |
(messages.type.equals(MessageType.text.name).not() &
messages.type.equals(MessageType.media.name).not()) |
@@ -256,7 +258,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin {
))) &
(messages.openedAt.isBiggerThanValue(deletionTime) |
messages.openedAt.isNull() |
- messages.mediaStored.equals(true)) &
+ messages.mediaStored.equals(true) |
+ messages.type.equals(MessageType.webxdcApp.name)) &
(messages.isDeletedFromSender.equals(true) |
(messages.type.equals(MessageType.text.name).not() &
messages.type.equals(MessageType.media.name).not()) |
@@ -310,6 +313,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin {
await (delete(messages)..where(
(m) =>
m.groupId.isIn(groupIds) &
+ // An app card owns a webxdc instance, and the instance owns
+ // its update log by foreign key. Sweeping the card away on
+ // the chat's timer would take an app's whole state with it
+ // without ever clearing the storage its origin holds, so
+ // apps leave only when somebody deletes them.
+ m.type.equals(MessageType.webxdcApp.name).not() &
((m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true)) |
m.mediaStored.equals(false)) &
diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart
index f1ed06bd..00a2e675 100644
--- a/lib/src/database/tables/messages.table.dart
+++ b/lib/src/database/tables/messages.table.dart
@@ -3,7 +3,14 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
-enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
+enum MessageType {
+ media,
+ text,
+ contacts,
+ restoreFlameCounter,
+ askAboutUser,
+ webxdcApp,
+}
@DataClassName('Message')
@TableIndex(
diff --git a/lib/src/database/tables/webxdc.table.dart b/lib/src/database/tables/webxdc.table.dart
new file mode 100644
index 00000000..4a904a9a
--- /dev/null
+++ b/lib/src/database/tables/webxdc.table.dart
@@ -0,0 +1,82 @@
+import 'package:drift/drift.dart';
+import 'package:twonly/src/database/tables/groups.table.dart';
+import 'package:twonly/src/database/tables/messages.table.dart';
+
+/// Store metadata mirrored from the API.
+///
+/// A cache, but a refresh keeps the rows an instance on this device runs even
+/// once the store stops offering them: an unpublished app keeps working where
+/// it was already downloaded. `published` says whether the last catalog still
+/// carried the version, which is what the store list goes by.
+@DataClassName('WebxdcApp')
+class WebxdcApps extends Table {
+ TextColumn get appId => text()();
+ IntColumn get version => integer()();
+
+ /// What the app is called where no translation fits. Every row has one,
+ /// which is what the store orders by.
+ TextColumn get name => text()();
+
+ /// The name per language, as a JSON object keyed by language tag, in the same
+ /// shape as [description]. The chat card picks from it; the store list is
+ /// built in Rust, which picks there.
+ TextColumn get nameTranslations => text().withDefault(const Constant('{}'))();
+
+ /// One short line per language, as a JSON object keyed by language tag. Rust
+ /// picks the one to show; nothing in Dart reads inside it.
+ TextColumn get description => text().withDefault(const Constant('{}'))();
+ TextColumn get sourceCodeUrl => text().nullable()();
+ BlobColumn get icon => blob().nullable()();
+ TextColumn get bundleSha256 => text()();
+ IntColumn get bundleBytes => integer()();
+ BoolColumn get published => boolean().withDefault(const Constant(true))();
+ IntColumn get cachedAt => integer()();
+
+ @override
+ Set get primaryKey => {appId, version};
+}
+
+/// One app placed into a chat. The row's id is the id of the message that
+/// carries it, so the chat card and the instance are the same object.
+@DataClassName('WebxdcInstance')
+class WebxdcInstances extends Table {
+ TextColumn get instanceId => text().references(
+ Messages,
+ #messageId,
+ onDelete: KeyAction.cascade,
+ )();
+ TextColumn get groupId =>
+ text().references(Groups, #groupId, onDelete: KeyAction.cascade)();
+ TextColumn get appId => text()();
+ IntColumn get version => integer()();
+ TextColumn get bundleSha256 => text().nullable()();
+ TextColumn get originToken => text()();
+ TextColumn get summary => text().nullable()();
+ TextColumn get document => text().nullable()();
+ IntColumn get createdAt => integer()();
+ IntColumn get lastUpdateAt => integer()();
+
+ @override
+ Set get primaryKey => {instanceId};
+}
+
+/// The update log. Read-only from Dart: serials are assigned in Rust, and the
+/// chat's deletion timer deliberately does not reach this table.
+@DataClassName('WebxdcUpdate')
+class WebxdcUpdates extends Table {
+ TextColumn get instanceId => text().references(
+ WebxdcInstances,
+ #instanceId,
+ onDelete: KeyAction.cascade,
+ )();
+ IntColumn get serial => integer()();
+ TextColumn get messageId => text()();
+ IntColumn get senderId => integer().nullable()();
+ TextColumn get payload => text()();
+ TextColumn get info => text().nullable()();
+ TextColumn get href => text().nullable()();
+ IntColumn get receivedAt => integer()();
+
+ @override
+ Set get primaryKey => {instanceId, serial};
+}
diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart
index 1369c5e7..12784df7 100644
--- a/lib/src/database/twonly.db.dart
+++ b/lib/src/database/twonly.db.dart
@@ -20,6 +20,7 @@ import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
+import 'package:twonly/src/database/tables/webxdc.table.dart';
import 'package:twonly/src/database/twonly.db.steps.dart';
import 'package:twonly/src/utils/log.dart';
@@ -48,6 +49,9 @@ part 'twonly.db.g.dart';
UserDiscoveryShares,
ContactGroups,
ContactGroupMembers,
+ WebxdcApps,
+ WebxdcInstances,
+ WebxdcUpdates,
],
daos: [
MessagesDao,
diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart
index a8f21b27..8f554af8 100644
--- a/lib/src/database/twonly.db.g.dart
+++ b/lib/src/database/twonly.db.g.dart
@@ -12536,6 +12536,1849 @@ class ContactGroupMembersCompanion extends UpdateCompanion {
}
}
+class $WebxdcAppsTable extends WebxdcApps
+ with TableInfo<$WebxdcAppsTable, WebxdcApp> {
+ @override
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $WebxdcAppsTable(this.attachedDatabase, [this._alias]);
+ static const VerificationMeta _appIdMeta = const VerificationMeta('appId');
+ @override
+ late final GeneratedColumn appId = GeneratedColumn(
+ 'app_id',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ static const VerificationMeta _versionMeta = const VerificationMeta(
+ 'version',
+ );
+ @override
+ late final GeneratedColumn version = GeneratedColumn(
+ 'version',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: true,
+ );
+ static const VerificationMeta _nameMeta = const VerificationMeta('name');
+ @override
+ late final GeneratedColumn name = GeneratedColumn(
+ 'name',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ static const VerificationMeta _nameTranslationsMeta = const VerificationMeta(
+ 'nameTranslations',
+ );
+ @override
+ late final GeneratedColumn nameTranslations = GeneratedColumn(
+ 'name_translations',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ defaultValue: const Constant('{}'),
+ );
+ static const VerificationMeta _descriptionMeta = const VerificationMeta(
+ 'description',
+ );
+ @override
+ late final GeneratedColumn description = GeneratedColumn(
+ 'description',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ defaultValue: const Constant('{}'),
+ );
+ static const VerificationMeta _sourceCodeUrlMeta = const VerificationMeta(
+ 'sourceCodeUrl',
+ );
+ @override
+ late final GeneratedColumn sourceCodeUrl = GeneratedColumn(
+ 'source_code_url',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ static const VerificationMeta _iconMeta = const VerificationMeta('icon');
+ @override
+ late final GeneratedColumn icon = GeneratedColumn(
+ 'icon',
+ aliasedName,
+ true,
+ type: DriftSqlType.blob,
+ requiredDuringInsert: false,
+ );
+ static const VerificationMeta _bundleSha256Meta = const VerificationMeta(
+ 'bundleSha256',
+ );
+ @override
+ late final GeneratedColumn bundleSha256 = GeneratedColumn(
+ 'bundle_sha256',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ static const VerificationMeta _bundleBytesMeta = const VerificationMeta(
+ 'bundleBytes',
+ );
+ @override
+ late final GeneratedColumn bundleBytes = GeneratedColumn(
+ 'bundle_bytes',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: true,
+ );
+ static const VerificationMeta _publishedMeta = const VerificationMeta(
+ 'published',
+ );
+ @override
+ late final GeneratedColumn published = GeneratedColumn(
+ 'published',
+ aliasedName,
+ false,
+ type: DriftSqlType.bool,
+ requiredDuringInsert: false,
+ defaultConstraints: GeneratedColumn.constraintIsAlways(
+ 'CHECK ("published" IN (0, 1))',
+ ),
+ defaultValue: const Constant(true),
+ );
+ static const VerificationMeta _cachedAtMeta = const VerificationMeta(
+ 'cachedAt',
+ );
+ @override
+ late final GeneratedColumn cachedAt = GeneratedColumn(
+ 'cached_at',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: true,
+ );
+ @override
+ List get $columns => [
+ appId,
+ version,
+ name,
+ nameTranslations,
+ description,
+ sourceCodeUrl,
+ icon,
+ bundleSha256,
+ bundleBytes,
+ published,
+ cachedAt,
+ ];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'webxdc_apps';
+ @override
+ VerificationContext validateIntegrity(
+ Insertable instance, {
+ bool isInserting = false,
+ }) {
+ final context = VerificationContext();
+ final data = instance.toColumns(true);
+ if (data.containsKey('app_id')) {
+ context.handle(
+ _appIdMeta,
+ appId.isAcceptableOrUnknown(data['app_id']!, _appIdMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_appIdMeta);
+ }
+ if (data.containsKey('version')) {
+ context.handle(
+ _versionMeta,
+ version.isAcceptableOrUnknown(data['version']!, _versionMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_versionMeta);
+ }
+ if (data.containsKey('name')) {
+ context.handle(
+ _nameMeta,
+ name.isAcceptableOrUnknown(data['name']!, _nameMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_nameMeta);
+ }
+ if (data.containsKey('name_translations')) {
+ context.handle(
+ _nameTranslationsMeta,
+ nameTranslations.isAcceptableOrUnknown(
+ data['name_translations']!,
+ _nameTranslationsMeta,
+ ),
+ );
+ }
+ if (data.containsKey('description')) {
+ context.handle(
+ _descriptionMeta,
+ description.isAcceptableOrUnknown(
+ data['description']!,
+ _descriptionMeta,
+ ),
+ );
+ }
+ if (data.containsKey('source_code_url')) {
+ context.handle(
+ _sourceCodeUrlMeta,
+ sourceCodeUrl.isAcceptableOrUnknown(
+ data['source_code_url']!,
+ _sourceCodeUrlMeta,
+ ),
+ );
+ }
+ if (data.containsKey('icon')) {
+ context.handle(
+ _iconMeta,
+ icon.isAcceptableOrUnknown(data['icon']!, _iconMeta),
+ );
+ }
+ if (data.containsKey('bundle_sha256')) {
+ context.handle(
+ _bundleSha256Meta,
+ bundleSha256.isAcceptableOrUnknown(
+ data['bundle_sha256']!,
+ _bundleSha256Meta,
+ ),
+ );
+ } else if (isInserting) {
+ context.missing(_bundleSha256Meta);
+ }
+ if (data.containsKey('bundle_bytes')) {
+ context.handle(
+ _bundleBytesMeta,
+ bundleBytes.isAcceptableOrUnknown(
+ data['bundle_bytes']!,
+ _bundleBytesMeta,
+ ),
+ );
+ } else if (isInserting) {
+ context.missing(_bundleBytesMeta);
+ }
+ if (data.containsKey('published')) {
+ context.handle(
+ _publishedMeta,
+ published.isAcceptableOrUnknown(data['published']!, _publishedMeta),
+ );
+ }
+ if (data.containsKey('cached_at')) {
+ context.handle(
+ _cachedAtMeta,
+ cachedAt.isAcceptableOrUnknown(data['cached_at']!, _cachedAtMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_cachedAtMeta);
+ }
+ return context;
+ }
+
+ @override
+ Set get $primaryKey => {appId, version};
+ @override
+ WebxdcApp map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return WebxdcApp(
+ appId: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}app_id'],
+ )!,
+ version: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}version'],
+ )!,
+ name: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}name'],
+ )!,
+ nameTranslations: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}name_translations'],
+ )!,
+ description: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}description'],
+ )!,
+ sourceCodeUrl: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}source_code_url'],
+ ),
+ icon: attachedDatabase.typeMapping.read(
+ DriftSqlType.blob,
+ data['${effectivePrefix}icon'],
+ ),
+ bundleSha256: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}bundle_sha256'],
+ )!,
+ bundleBytes: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}bundle_bytes'],
+ )!,
+ published: attachedDatabase.typeMapping.read(
+ DriftSqlType.bool,
+ data['${effectivePrefix}published'],
+ )!,
+ cachedAt: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}cached_at'],
+ )!,
+ );
+ }
+
+ @override
+ $WebxdcAppsTable createAlias(String alias) {
+ return $WebxdcAppsTable(attachedDatabase, alias);
+ }
+}
+
+class WebxdcApp extends DataClass implements Insertable {
+ final String appId;
+ final int version;
+
+ /// What the app is called where no translation fits. Every row has one,
+ /// which is what the store orders by.
+ final String name;
+
+ /// The name per language, as a JSON object keyed by language tag, in the same
+ /// shape as [description]. The chat card picks from it; the store list is
+ /// built in Rust, which picks there.
+ final String nameTranslations;
+
+ /// One short line per language, as a JSON object keyed by language tag. Rust
+ /// picks the one to show; nothing in Dart reads inside it.
+ final String description;
+ final String? sourceCodeUrl;
+ final Uint8List? icon;
+ final String bundleSha256;
+ final int bundleBytes;
+ final bool published;
+ final int cachedAt;
+ const WebxdcApp({
+ required this.appId,
+ required this.version,
+ required this.name,
+ required this.nameTranslations,
+ required this.description,
+ this.sourceCodeUrl,
+ this.icon,
+ required this.bundleSha256,
+ required this.bundleBytes,
+ required this.published,
+ required this.cachedAt,
+ });
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['app_id'] = Variable(appId);
+ map['version'] = Variable(version);
+ map['name'] = Variable(name);
+ map['name_translations'] = Variable(nameTranslations);
+ map['description'] = Variable(description);
+ if (!nullToAbsent || sourceCodeUrl != null) {
+ map['source_code_url'] = Variable(sourceCodeUrl);
+ }
+ if (!nullToAbsent || icon != null) {
+ map['icon'] = Variable(icon);
+ }
+ map['bundle_sha256'] = Variable(bundleSha256);
+ map['bundle_bytes'] = Variable(bundleBytes);
+ map['published'] = Variable(published);
+ map['cached_at'] = Variable(cachedAt);
+ return map;
+ }
+
+ WebxdcAppsCompanion toCompanion(bool nullToAbsent) {
+ return WebxdcAppsCompanion(
+ appId: Value(appId),
+ version: Value(version),
+ name: Value(name),
+ nameTranslations: Value(nameTranslations),
+ description: Value(description),
+ sourceCodeUrl: sourceCodeUrl == null && nullToAbsent
+ ? const Value.absent()
+ : Value(sourceCodeUrl),
+ icon: icon == null && nullToAbsent ? const Value.absent() : Value(icon),
+ bundleSha256: Value(bundleSha256),
+ bundleBytes: Value(bundleBytes),
+ published: Value(published),
+ cachedAt: Value(cachedAt),
+ );
+ }
+
+ factory WebxdcApp.fromJson(
+ Map json, {
+ ValueSerializer? serializer,
+ }) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return WebxdcApp(
+ appId: serializer.fromJson(json['appId']),
+ version: serializer.fromJson(json['version']),
+ name: serializer.fromJson(json['name']),
+ nameTranslations: serializer.fromJson(json['nameTranslations']),
+ description: serializer.fromJson(json['description']),
+ sourceCodeUrl: serializer.fromJson(json['sourceCodeUrl']),
+ icon: serializer.fromJson(json['icon']),
+ bundleSha256: serializer.fromJson(json['bundleSha256']),
+ bundleBytes: serializer.fromJson(json['bundleBytes']),
+ published: serializer.fromJson(json['published']),
+ cachedAt: serializer.fromJson(json['cachedAt']),
+ );
+ }
+ @override
+ Map toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return {
+ 'appId': serializer.toJson(appId),
+ 'version': serializer.toJson(version),
+ 'name': serializer.toJson(name),
+ 'nameTranslations': serializer.toJson(nameTranslations),
+ 'description': serializer.toJson(description),
+ 'sourceCodeUrl': serializer.toJson(sourceCodeUrl),
+ 'icon': serializer.toJson(icon),
+ 'bundleSha256': serializer.toJson(bundleSha256),
+ 'bundleBytes': serializer.toJson(bundleBytes),
+ 'published': serializer.toJson(published),
+ 'cachedAt': serializer.toJson(cachedAt),
+ };
+ }
+
+ WebxdcApp copyWith({
+ String? appId,
+ int? version,
+ String? name,
+ String? nameTranslations,
+ String? description,
+ Value sourceCodeUrl = const Value.absent(),
+ Value icon = const Value.absent(),
+ String? bundleSha256,
+ int? bundleBytes,
+ bool? published,
+ int? cachedAt,
+ }) => WebxdcApp(
+ appId: appId ?? this.appId,
+ version: version ?? this.version,
+ name: name ?? this.name,
+ nameTranslations: nameTranslations ?? this.nameTranslations,
+ description: description ?? this.description,
+ sourceCodeUrl: sourceCodeUrl.present
+ ? sourceCodeUrl.value
+ : this.sourceCodeUrl,
+ icon: icon.present ? icon.value : this.icon,
+ bundleSha256: bundleSha256 ?? this.bundleSha256,
+ bundleBytes: bundleBytes ?? this.bundleBytes,
+ published: published ?? this.published,
+ cachedAt: cachedAt ?? this.cachedAt,
+ );
+ WebxdcApp copyWithCompanion(WebxdcAppsCompanion data) {
+ return WebxdcApp(
+ appId: data.appId.present ? data.appId.value : this.appId,
+ version: data.version.present ? data.version.value : this.version,
+ name: data.name.present ? data.name.value : this.name,
+ nameTranslations: data.nameTranslations.present
+ ? data.nameTranslations.value
+ : this.nameTranslations,
+ description: data.description.present
+ ? data.description.value
+ : this.description,
+ sourceCodeUrl: data.sourceCodeUrl.present
+ ? data.sourceCodeUrl.value
+ : this.sourceCodeUrl,
+ icon: data.icon.present ? data.icon.value : this.icon,
+ bundleSha256: data.bundleSha256.present
+ ? data.bundleSha256.value
+ : this.bundleSha256,
+ bundleBytes: data.bundleBytes.present
+ ? data.bundleBytes.value
+ : this.bundleBytes,
+ published: data.published.present ? data.published.value : this.published,
+ cachedAt: data.cachedAt.present ? data.cachedAt.value : this.cachedAt,
+ );
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('WebxdcApp(')
+ ..write('appId: $appId, ')
+ ..write('version: $version, ')
+ ..write('name: $name, ')
+ ..write('nameTranslations: $nameTranslations, ')
+ ..write('description: $description, ')
+ ..write('sourceCodeUrl: $sourceCodeUrl, ')
+ ..write('icon: $icon, ')
+ ..write('bundleSha256: $bundleSha256, ')
+ ..write('bundleBytes: $bundleBytes, ')
+ ..write('published: $published, ')
+ ..write('cachedAt: $cachedAt')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ appId,
+ version,
+ name,
+ nameTranslations,
+ description,
+ sourceCodeUrl,
+ $driftBlobEquality.hash(icon),
+ bundleSha256,
+ bundleBytes,
+ published,
+ cachedAt,
+ );
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is WebxdcApp &&
+ other.appId == this.appId &&
+ other.version == this.version &&
+ other.name == this.name &&
+ other.nameTranslations == this.nameTranslations &&
+ other.description == this.description &&
+ other.sourceCodeUrl == this.sourceCodeUrl &&
+ $driftBlobEquality.equals(other.icon, this.icon) &&
+ other.bundleSha256 == this.bundleSha256 &&
+ other.bundleBytes == this.bundleBytes &&
+ other.published == this.published &&
+ other.cachedAt == this.cachedAt);
+}
+
+class WebxdcAppsCompanion extends UpdateCompanion {
+ final Value appId;
+ final Value version;
+ final Value name;
+ final Value nameTranslations;
+ final Value description;
+ final Value sourceCodeUrl;
+ final Value icon;
+ final Value bundleSha256;
+ final Value bundleBytes;
+ final Value published;
+ final Value cachedAt;
+ final Value rowid;
+ const WebxdcAppsCompanion({
+ this.appId = const Value.absent(),
+ this.version = const Value.absent(),
+ this.name = const Value.absent(),
+ this.nameTranslations = const Value.absent(),
+ this.description = const Value.absent(),
+ this.sourceCodeUrl = const Value.absent(),
+ this.icon = const Value.absent(),
+ this.bundleSha256 = const Value.absent(),
+ this.bundleBytes = const Value.absent(),
+ this.published = const Value.absent(),
+ this.cachedAt = const Value.absent(),
+ this.rowid = const Value.absent(),
+ });
+ WebxdcAppsCompanion.insert({
+ required String appId,
+ required int version,
+ required String name,
+ this.nameTranslations = const Value.absent(),
+ this.description = const Value.absent(),
+ this.sourceCodeUrl = const Value.absent(),
+ this.icon = const Value.absent(),
+ required String bundleSha256,
+ required int bundleBytes,
+ this.published = const Value.absent(),
+ required int cachedAt,
+ this.rowid = const Value.absent(),
+ }) : appId = Value(appId),
+ version = Value(version),
+ name = Value(name),
+ bundleSha256 = Value(bundleSha256),
+ bundleBytes = Value(bundleBytes),
+ cachedAt = Value(cachedAt);
+ static Insertable custom({
+ Expression? appId,
+ Expression? version,
+ Expression? name,
+ Expression? nameTranslations,
+ Expression? description,
+ Expression? sourceCodeUrl,
+ Expression? icon,
+ Expression