Compare commits

..

No commits in common. "main" and "v0.1.8" have entirely different histories.
main ... v0.1.8

615 changed files with 19139 additions and 144652 deletions

View file

@ -31,5 +31,5 @@ jobs:
- name: flutter analyze
run: flutter analyze
# - name: flutter test
# run: flutter test
- name: flutter test
run: flutter test

62
.github/workflows/release_github.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Publish on Github
on:
workflow_dispatch: {}
push:
branches:
- main
paths:
- pubspec.yaml
jobs:
build_and_publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Cloning sub-repos
run: git submodule update --init --recursive
# - name: Check flutter code
# run: |
# flutter pub get
# flutter analyze
# flutter test
- name: Check flutter code
run: flutter pub get
- name: Create key.properties file
run: |
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> ./android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> ./android/key.properties
echo "keyAlias=github-releases-signature" >> ./android/key.properties
echo "storeFile=./keystore.jks" >> ./android/key.properties
- name: Create keystore file
env:
KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
run: echo $KEYSTORE_FILE | base64 --decode > ./android/app/keystore.jks
- name: Build Android APK
run: flutter build apk --release --split-per-abi
- name: Extract pubspec version
run: |
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
- name: Upload Release Binaries (stable)
uses: ncipollo/release-action@v1.18.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: v${{ env.PUBSPEC_VERSION }}
allowUpdates: true
artifacts: build/app/outputs/flutter-apk/*.apk

9
.gitignore vendored
View file

@ -10,14 +10,8 @@
.history
.svn/
.swiftpm/
*.sqlite
*.sqlite-shm
*.sqlite-wal
migrate_working_dir/
fastlane/report.xml
fastlane/README.md
# IntelliJ related
*.iml
*.ipr
@ -53,6 +47,3 @@ app.*.map.json
/android/app/.cxx/
android/.kotlin/
devtools_options.yaml
rust/target
rust_dependencies/target
fastlane/repo/status/running.json

View file

@ -1,88 +1,5 @@
# Changelog
## 0.2.26
- New: Import images from the gallery
- Improved: Media files are now stored in the dedicated "twonly" album
- Improved: UI components adapt to native styling (iOS/Android)
- Fix: Migration issue that resulted in a corrupted backup mechanism
- Fix: Database issues causing messages to be lost or the database to be corrupted
- Fix: Permission view did not disappear after they were granted
## 0.2.23
- Improved: Smaller UI changes
- Fix: Some messages were not marked as opened.
## 0.2.20
- New: Adds an "Ask a Friend" button to new contact suggestions.
- New: Adds security profiles.
- Improved: Onboarding flow for new users.
- Improved: Flame restore experience.
- Improved: The blue verification checkmark now displays the total number of verifications.
- Fix: Issue with receiving messages when user closed app while decrypting
- Fix: Background message fetching reliability.
- Fix: Issue with focus changing when taking a picture
- Fix: Issues with the camera initialization
## 0.2.16
- Fix: Images not shown after opening due to cleanup
## 0.2.15
- Fix: Issue with opening directly in chats
- Fix: Multiple smaller issues
## 0.2.13
- New: Tutorial on how to use zoom.
- New: Manage storage view.
- Improved: Media thumbnails for faster loading.
- Fix: Some messages were not marked as opened.
## 0.2.12
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
- Fix: Reliability of receiving media files.
## 0.2.11
- New: Create custom shortcuts to quickly share images with pre-selected groups
- New: Seamless recovery for iOS reinstallations
- Improved: Redesigned snackbar notifications
- Improved: New backup mechanism to allow larger backup files
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
## 0.2.10
- Fix: Issue with push notifications on Android
## 0.2.9
- Improved: Make contact avatars clickable
- Fix: Messages occasionally not received until app restart
- Fix: Complete setup would sometimes get stuck
## 0.2.8
- Fix: App did not launch sometimes on Android
## 0.2.0
- New: Feature to find friends without a phone number
- New: The verification state is now transferred to the scanned user
- New: Registration setup to configure the most important configurations
- Improved: Show ⌛ instead of the flame icon when it is about to expire
- Improved: FAQ is now in the app rather than opening in the browser
- Improved: Videos can now be paused
- Improved: Lock to record hands-free
- Fix: Many smaller issues
## 0.1.8
- Improved: Typos and grammar issues thanks to @AlbertUnruh

View file

@ -1,16 +1,10 @@
# twonly
<a href="https://twonly.eu" rel="some text"><img src="metadata/en-US/images/featureGraphic.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
<a href="https://twonly.eu" rel="some text"><img src="docs/header.webp" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
This repository contains the complete source code of the [twonly](https://twonly.eu) app. twonly is a replacement for Snapchat, but its purpose is not to replace instant messaging apps, as there are already [many fantastic alternatives](https://www.messenger-matrix.de/messenger-matrix-en.html) out there. It was started because I liked the basic features of Snapchat, such as opening with the camera, the easy-to-use image editor, and the focus on sending fun pictures to friends. But I was annoyed by Snapchat's forced AI chat, receiving random messages to follow strangers, and not knowing how my sent images/text messages were encrypted, if at all. I am also very critical of the direction in which the US is currently moving and therefore try to avoid US providers wherever possible.
<p align="center">
<img src="metadata/en-US/images/phoneScreenshots/01_share_moments.png" width="30%" alt="Share moments" />
<img src="metadata/en-US/images/phoneScreenshots/02_chat_list.png" width="30%" alt="Chat list" />
<img src="metadata/en-US/images/phoneScreenshots/03_groups.png" width="30%" alt="Groups" />
</p>
<div align="center" style="margin: 10px 20px 10px 20px">
<div style="margin: 10px 20px 10px 20px">
<a href="https://apps.apple.com/de/app/twonly/id6743774441">
<img alt="Get it on App Store button" src="https://twonly.eu/assets/buttons/download-on-the-app-store.svg"
width="100px" />

View file

@ -16,12 +16,9 @@ analyzer:
- "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**"
- "lib/generated/**"
- "lib/core/**"
- "lib/src/localization/**"
- "rust_builder/"
- "dependencies/**"
- "pubspec.yaml"
- "**.arb"
- "*.arb"
- "test/drift/**"
- "**.g.dart"

2
android/.gitignore vendored
View file

@ -9,7 +9,5 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
key.github.properties
key.properties.backup
**/*.keystore
**/*.jks

View file

@ -73,5 +73,4 @@ flutter {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.otaliastudios:transcoder:0.11.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
}

View file

@ -8,7 +8,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View file

@ -6,38 +6,8 @@ import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownP
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP
import io.flutter.embedding.engine.FlutterEngine
import android.content.Context
import io.crates.keyring.Keyring
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import android.os.Bundle
import android.net.Uri
import java.io.InputStream
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity() {
private val CHANNEL = "eu.twonly/photo_picker"
private var pendingResult: MethodChannel.Result? = null
private lateinit var pickMultipleMedia: ActivityResultLauncher<PickVisualMediaRequest>
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
if (uris.isNotEmpty()) {
val uriStrings = uris.map { it.toString() }
pendingResult?.success(uriStrings)
} else {
pendingResult?.success(emptyList<String>())
}
pendingResult = null
}
super.onCreate(savedInstanceState)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
@ -54,38 +24,7 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Keyring.initializeNdkContext(applicationContext)
MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"pickImages" -> {
pendingResult = result
pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
"getUriBytes" -> {
val uriString = call.argument<String>("uri")
if (uriString != null) {
try {
val uri = Uri.parse(uriString)
val inputStream: InputStream? = contentResolver.openInputStream(uri)
if (inputStream != null) {
val bytes = inputStream.readBytes()
inputStream.close()
result.success(bytes)
} else {
result.error("UNAVAILABLE", "Could not open InputStream", null)
}
} catch (e: Exception) {
result.error("ERROR", e.message, null)
}
} else {
result.error("INVALID_ARGUMENT", "URI string is null", null)
}
}
else -> result.notImplemented()
}
}
}
}

View file

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

View file

@ -3,12 +3,10 @@ package eu.twonly
import io.flutter.app.FlutterApplication
import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler
import io.crates.keyring.Keyring
class MyApplication : FlutterApplication() {
override fun onCreate() {
super.onCreate()
Keyring.initializeNdkContext(this)
// This enables the internal plugin logging to Logcat
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
}

View file

@ -1,14 +0,0 @@
package io.crates.keyring
import android.content.Context
class Keyring {
companion object {
init {
// Replace with the name of your compiled Rust library
System.loadLibrary("rust_lib_twonly")
}
// The underlying Rust crate provides the implementation for this
external fun initializeNdkContext(context: Context)
}
}

View file

@ -1,69 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="100dp"
android:height="100dp"
android:viewportWidth="640"
android:viewportHeight="640">
<!-- Wrap everything in a scaling group to add padding and prevent splash screen circular cropping -->
<group
android:pivotX="320"
android:pivotY="320"
android:scaleX="0.6"
android:scaleY="0.6">
<!-- Link One pivots around its visual center (approx X=416, Y=288) -->
<group
android:name="link_one_group"
android:pivotX="416"
android:pivotY="288">
<path
android:name="link_one_path"
android:fillColor="#fff"
android:pathData="M451.5 160C434.9 160 418.8 164.5 404.7 172.7C388.9 156.7 370.5 143.3 350.2 133.2C378.4 109.2 414.3 96 451.5 96C537.9 96 608 166 608 252.5C608 294 591.5 333.8 562.2 363.1L491.1 434.2C461.8 463.5 422 480 380.5 480C294.1 480 224 410 224 323.5C224 322 224 320.5 224.1 319C224.6 301.3 239.3 287.4 257 287.9C274.7 288.4 288.6 303.1 288.1 320.8C288.1 321.7 288.1 322.6 288.1 323.4C288.1 374.5 329.5 415.9 380.6 415.9C405.1 415.9 428.6 406.2 446 388.8L517.1 317.7C534.4 300.4 544.2 276.8 544.2 252.3C544.2 201.2 502.8 159.8 451.7 159.8z" />
</group>
<!-- Link Two pivots around its visual center (approx X=224, Y=352) -->
<group
android:name="link_two_group"
android:pivotX="224"
android:pivotY="352">
<path
android:name="link_two_path"
android:fillColor="#ffffff"
android:pathData="M307.2 237.3C305.3 236.5 303.4 235.4 301.7 234.2C289.1 227.7 274.7 224 259.6 224C235.1 224 211.6 233.7 194.2 251.1L123.1 322.2C105.8 339.5 96 363.1 96 387.6C96 438.7 137.4 480.1 188.5 480.1C205 480.1 221.1 475.7 235.2 467.5C251 483.5 269.4 496.9 289.8 507C261.6 530.9 225.8 544.2 188.5 544.2C102.1 544.2 32 474.2 32 387.7C32 346.2 48.5 306.4 77.8 277.1L148.9 206C178.2 176.7 218 160.2 259.5 160.2C346.1 160.2 416 230.8 416 317.1C416 318.4 416 319.7 416 321C415.6 338.7 400.9 352.6 383.2 352.2C365.5 351.8 351.6 337.1 352 319.4C352 318.6 352 317.9 352 317.1C352 283.4 334 253.8 307.2 237.5z" />
</group>
</group>
</vector>
</aapt:attr>
<!-- Rotate Link One smoothly back and forth -->
<target android:name="link_one_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="800"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:repeatCount="-1"
android:repeatMode="reverse"
android:valueFrom="-3"
android:valueTo="3" />
</aapt:attr>
</target>
<!-- Rotate Link Two smoothly in the opposite direction to create the opening/closing effect -->
<target android:name="link_two_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="800"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:repeatCount="-1"
android:repeatMode="reverse"
android:valueFrom="3"
android:valueTo="-3" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="Theme.SplashScreen">
<!-- Configure the Androidx Splash Screen API parameters -->
<item name="windowSplashScreenBackground">#FF57CC99</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/link_animated</item>
<item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View file

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="Theme.SplashScreen">
<!-- Configure the Androidx Splash Screen API parameters -->
<item name="windowSplashScreenBackground">#FF57CC99</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/link_animated</item>
<item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View file

@ -1,8 +0,0 @@
# Example signing credentials configuration for GitHub Releases.
# Copy this file to 'key.github.properties' and fill in your actual credentials.
# Do not commit the actual 'key.github.properties' file to version control.
storePassword=YOUR_GITHUB_RELEASE_STORE_PASSWORD
keyPassword=YOUR_GITHUB_RELEASE_KEY_PASSWORD
keyAlias=github-releases-signature
storeFile=/absolute/path/to/your/github-release-keystore.jks

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM8.107 4.000L9.339 4.000L9.339 12.000L7.832 12.000L7.832 6.246L7.817 6.232L7.339 6.638L6.948 6.899L6.455 7.159L5.861 7.391L5.861 6.014L6.165 5.899L6.499 5.725L6.861 5.493L7.296 5.159L7.643 4.812L7.832 4.565L8.049 4.174L8.107 4.000Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 732 B

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.899 4.000L8.391 4.000L8.768 4.043L9.029 4.101L9.362 4.217L9.623 4.348L9.942 4.580L10.145 4.783L10.304 4.986L10.406 5.145L10.551 5.464L10.652 5.899L10.667 6.406L10.594 6.884L10.478 7.246L10.290 7.638L10.043 8.029L9.812 8.333L9.507 8.667L8.406 9.696L7.928 10.174L7.754 10.391L7.638 10.580L10.667 10.594L10.667 12.000L5.333 12.000L5.391 11.609L5.522 11.159L5.710 10.725L6.000 10.246L6.232 9.942L6.638 9.478L7.464 8.652L8.072 8.087L8.594 7.551L8.870 7.203L9.029 6.899L9.087 6.739L9.145 6.449L9.145 6.174L9.101 5.928L8.986 5.667L8.768 5.435L8.652 5.362L8.522 5.304L8.246 5.246L7.971 5.246L7.783 5.275L7.652 5.319L7.406 5.464L7.232 5.652L7.101 5.913L7.029 6.203L7.000 6.493L5.493 6.348L5.565 5.870L5.725 5.362L5.957 4.942L6.275 4.594L6.638 4.348L6.942 4.203L7.377 4.072L7.899 4.000Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.730 4.000L8.142 4.000L8.555 4.057L8.854 4.142L9.224 4.313L9.523 4.527L9.794 4.797L10.064 5.196L10.206 5.580L10.249 5.851L10.235 6.263L10.149 6.591L10.007 6.875L9.836 7.103L9.566 7.359L9.125 7.644L9.409 7.730L9.708 7.872L9.950 8.043L10.164 8.256L10.335 8.498L10.477 8.797L10.548 9.039L10.591 9.338L10.577 9.836L10.448 10.377L10.249 10.776L10.093 11.004L9.893 11.231L9.523 11.544L9.125 11.772L8.911 11.858L8.598 11.943L8.171 12.000L7.587 11.986L7.146 11.900L6.733 11.744L6.320 11.488L5.950 11.132L5.680 10.733L5.509 10.320L5.409 9.865L5.409 9.808L6.847 9.637L6.861 9.765L6.947 10.064L7.004 10.192L7.160 10.420L7.416 10.633L7.630 10.733L7.843 10.776L8.171 10.762L8.327 10.719L8.498 10.633L8.797 10.363L8.968 10.064L9.039 9.822L9.068 9.609L9.053 9.167L8.954 8.826L8.769 8.541L8.512 8.327L8.214 8.214L7.872 8.199L7.331 8.299L7.488 7.132L7.929 7.089L8.256 6.975L8.384 6.890L8.569 6.705L8.683 6.505L8.754 6.221L8.754 5.979L8.698 5.737L8.598 5.552L8.427 5.381L8.242 5.281L8.000 5.224L7.772 5.224L7.445 5.324L7.317 5.409L7.132 5.594L7.032 5.751L6.947 5.964L6.890 6.278L5.523 6.036L5.623 5.623L5.794 5.181L5.950 4.911L6.235 4.584L6.591 4.327L6.961 4.157L7.302 4.057L7.730 4.000Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@ -1 +1 @@
Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe
Subproject commit 24d048b4abbe5c266b09965cc6f3ebdf83f97855

BIN
docs/header.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -1,2 +0,0 @@
json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json")
package_name("eu.twonly") # Your application ID

View file

@ -1,144 +0,0 @@
default_platform(:android)
platform :android do
desc "Submit a new App Bundle to the Google Play Internal Track"
lane :internal do
# This lane assumes that `flutter build appbundle` has already been run from the flutter root.
upload_to_play_store(
track: 'internal',
aab: 'build/app/outputs/bundle/release/app-release.aab',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Build the application locally and upload it as a new GitHub release"
lane :release_github do
# Read pubspec.yaml to get the version
pubspec_path = File.expand_path("../pubspec.yaml", __dir__)
unless File.exist?(pubspec_path)
UI.user_error!("Could not find pubspec.yaml at #{pubspec_path}")
end
pubspec_content = File.read(pubspec_path)
version_match = pubspec_content.match(/^version:\s*([^+]+)/)
unless version_match
UI.user_error!("Could not extract version from pubspec.yaml")
end
version = version_match[1].strip
tag_name = "v#{version}"
UI.message("Extracted version: #{version} (tag: #{tag_name})")
# Load release notes from CHANGELOG.md
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
release_notes = "Automated local release via Fastlane"
if File.exist?(changelog_path)
changelog_content = File.read(changelog_path)
escaped_version = Regexp.escape(version)
pattern = /##\s*\[?#{escaped_version}\]?(.*?)(?=##\s*|\z)/m
match = changelog_content.match(pattern)
if match
release_notes = match[1].strip
UI.message("Loaded release notes from CHANGELOG.md:\n#{release_notes}")
else
UI.important("Could not find release notes for version #{version} in CHANGELOG.md. Using default description.")
end
else
UI.important("CHANGELOG.md not found at #{changelog_path}. Using default description.")
end
# Handle key.properties swapping if key.github.properties exists
key_properties_path = File.expand_path("../android/key.properties", __dir__)
github_properties_path = File.expand_path("../android/key.github.properties", __dir__)
backup_properties_path = File.expand_path("../android/key.properties.backup", __dir__)
swapped_properties = false
if File.exist?(github_properties_path)
UI.message("Found key.github.properties. Swapping in for the build...")
if File.exist?(key_properties_path)
FileUtils.cp(key_properties_path, backup_properties_path)
end
FileUtils.cp(github_properties_path, key_properties_path)
swapped_properties = true
else
UI.message("No key.github.properties found. Building with default key.properties...")
end
begin
# Build the Android application
UI.message("Building Android APK...")
Dir.chdir(File.expand_path("..", __dir__)) do
sh("flutter build apk --release --split-per-abi")
end
ensure
# Restore original key.properties if swapped
if swapped_properties
UI.message("Restoring original key.properties...")
if File.exist?(backup_properties_path)
FileUtils.cp(backup_properties_path, key_properties_path)
FileUtils.rm(backup_properties_path)
else
FileUtils.rm_f(key_properties_path)
end
end
end
# Find built APKs
apk_glob = File.expand_path("../build/app/outputs/flutter-apk/*-release.apk", __dir__)
apks = Dir.glob(apk_glob)
if apks.empty?
UI.user_error!("No release APKs found matching #{apk_glob}")
end
UI.message("Found APKs to upload: #{apks.join(', ')}")
# Retrieve GitHub Token (fall back to gh auth token)
github_token = ENV["GITHUB_TOKEN"]
if github_token.nil? || github_token.empty?
UI.message("GITHUB_TOKEN env variable not set. Retrieving token via GitHub CLI (gh auth token)...")
begin
github_token = sh("gh auth token").strip
rescue => e
UI.user_error!("Failed to retrieve token from gh CLI. Make sure gh is installed and authenticated, or GITHUB_TOKEN environment variable is set. Error: #{e}")
end
end
UI.message("Creating GitHub Release #{tag_name}...")
set_github_release(
repository_name: "twonlyapp/twonly-app",
api_token: github_token,
tag_name: tag_name,
name: "Release #{tag_name}",
description: release_notes,
upload_assets: apks
)
UI.success("Successfully uploaded release #{tag_name} to GitHub!")
# F-Droid deployment
fdroid_repo_dir = "/Users/tobi/Documents/drive/twonly/F-Droid/repo"
UI.message("Starting F-Droid deployment...")
FileUtils.mkdir_p(fdroid_repo_dir)
apks.each do |apk_path|
basename = File.basename(apk_path)
new_name = "eu.twonly_v#{version}-#{basename}"
dest_path = File.join(fdroid_repo_dir, new_name)
UI.message("Copying APK to F-Droid repo: #{dest_path}")
FileUtils.cp(apk_path, dest_path)
end
fdroid_dir = "/Users/tobi/Documents/drive/twonly/F-Droid"
update_script = File.join(fdroid_dir, "update.sh")
if File.exist?(update_script)
UI.message("Executing F-Droid update script...")
Dir.chdir(fdroid_dir) do
sh("chmod +x ./update.sh && ./update.sh")
end
else
UI.important("F-Droid update script not found at #{update_script}")
end
end
end

View file

@ -1,3 +0,0 @@
rust_input: crate::bridge
rust_root: rust
dart_output: lib/core

View file

@ -1,31 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async => RustLib.init());
test('Can initialize twonlyDB and connect to api server', () async {
// Initialize global variables
await initBackgroundExecution();
// Try to connect to the API server
final connected = await apiService.connect();
// Print out the result or test it
expect(connected, isA<bool>());
// We can also check if it's connected
// Depending on your test environment, this might be true or false
// if the server is unreachable without further setup
// expect(apiService.isConnected, isA<bool>());
// Close the connection after the test
if (apiService.isConnected) {
await apiService.close(() {});
}
});
}

View file

@ -230,12 +230,12 @@ struct PushKey: Sendable {
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension PushKind: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0REACTION\0\u{1}RESPONSE\0\u{1}TEXT\0\u{1}VIDEO\0\u{1}TWONLY\0\u{1}IMAGE\0\u{1}CONTACT_REQUEST\0\u{1}ACCEPT_REQUEST\0\u{1}STORED_MEDIA_FILE\0\u{1}TEST_NOTIFICATION\0\u{1}REOPENED_MEDIA\0\u{1}REACTION_TO_VIDEO\0\u{1}REACTION_TO_TEXT\0\u{1}REACTION_TO_IMAGE\0\u{1}REACTION_TO_AUDIO\0\u{1}ADDED_TO_GROUP\0\u{1}AUDIO\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}reactionToAudio\0\u{1}addedToGroup\0\u{1}audio\0")
}
extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "EncryptedPushNotification"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}keyId\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -280,7 +280,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa
extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushNotification"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}additionalContent\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -354,7 +354,7 @@ extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushUser"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}userId\0\u{1}displayName\0\u{1}blocked\0\u{1}lastMessageId\0\u{1}pushKeys\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -408,7 +408,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushKey"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{1}createdAtUnixTimestamp\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {

View file

@ -231,8 +231,6 @@ PODS:
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- integration_test (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
@ -276,24 +274,17 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- no_screenshot (0.10.0):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- pro_video_editor (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- restart_app (1.7.3):
- Flutter
- rust_lib_twonly (0.0.1):
- Flutter
- screen_protector (1.5.1):
- Flutter
- ScreenProtectorKit (= 1.5.1)
- ScreenProtectorKit (1.5.1)
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
@ -313,6 +304,31 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.52.0):
- sqlite3/common (= 3.52.0)
- sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.52.0):
- sqlite3/common
- sqlite3/fts5 (3.52.0):
- sqlite3/common
- sqlite3/math (3.52.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common
- sqlite3/rtree (3.52.0):
- sqlite3/common
- sqlite3/session (3.52.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
- SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
@ -354,19 +370,17 @@ DEPENDENCIES:
- GoogleUtilities
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
- restart_app (from `.symlinks/plugins/restart_app/ios`)
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
@ -398,10 +412,10 @@ SPEC REPOS:
- MLKitVision
- nanopb
- PromisesObjC
- ScreenProtectorKit
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- sqlite3
- SwiftProtobuf
- SwiftyGif
@ -456,24 +470,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
no_screenshot:
:path: ".symlinks/plugins/no_screenshot/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/darwin"
pro_video_editor:
:path: ".symlinks/plugins/pro_video_editor/ios"
restart_app:
:path: ".symlinks/plugins/restart_app/ios"
rust_lib_twonly:
:path: ".symlinks/plugins/rust_lib_twonly/ios"
screen_protector:
:path: ".symlinks/plugins/screen_protector/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
@ -482,6 +490,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
@ -530,7 +540,6 @@ SPEC CHECKSUMS:
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@ -540,15 +549,12 @@ SPEC CHECKSUMS:
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 03c8ac6586f9652cd45e3d12d74e5992256403ac
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
@ -556,6 +562,8 @@ SPEC CHECKSUMS:
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

View file

@ -20,10 +20,6 @@ import workmanager_apple
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "eu.twonly.periodic_task",
frequency: NSNumber(value: 20 * 60)
@ -36,18 +32,16 @@ import workmanager_apple
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) {
return sharingIntent.application(app, open: url, options: options)
}
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) {
return sharingIntent.application(app, open: url, options: options)
}
// Proceed url handling for other Flutter libraries like app_links
return super.application(app, open: url, options: options)
}
// Proceed url handling for other Flutter libraries like app_links
return super.application(app, open: url, options:options)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
@ -60,8 +54,7 @@ import workmanager_apple
NSLog(
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
response.notification.request.content.userInfo)
super.userNotificationCenter(
center, didReceive: response, withCompletionHandler: completionHandler)
//...
}
override func userNotificationCenter(

View file

@ -19,7 +19,7 @@
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="0.341176" green="0.8" blue="0.6" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>

View file

@ -1,121 +1,118 @@
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/themes/dark.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/visual/components/app_outdated.comp.dart';
import 'package:twonly/src/visual/themes/dark.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/critical_error.view.dart';
import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/recovery.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart';
class App extends StatefulWidget {
const App({
required this.storageError,
required this.recoveryPossible,
super.key,
});
final bool storageError;
final bool recoveryPossible;
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> with WidgetsBindingObserver {
bool _wasPaused = false;
bool wasPaused = false;
@override
void initState() {
super.initState();
AppState.isAppInBackground = false;
globalIsAppInBackground = false;
WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required isConnected}) async {
await context.read<CustomChangeProvider>().updateConnectionState(
isConnected,
);
await setUserPlan();
};
globalCallbackUpdatePlan = (plan) {
context.read<PurchasesProvider>().updatePlan(plan);
};
unawaited(initAsync());
}
Future<void> setUserPlan() async {
final user = await getUser();
if (user != null && mounted) {
if (mounted) {
context.read<PurchasesProvider>().updatePlan(
planFromString(user.subscriptionPlan),
);
}
}
}
Future<void> initAsync() async {
await setUserPlan();
await apiService.connect();
await apiService.listenToNetworkChanges();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (_wasPaused) {
AppState.isAppInBackground = false;
if (wasPaused) {
globalIsAppInBackground = false;
twonlyDB.markUpdated();
unawaited(apiService.connect());
}
} else if (state == AppLifecycleState.paused) {
_wasPaused = true;
AppState.isAppInBackground = true;
wasPaused = true;
globalIsAppInBackground = true;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = ({required isConnected}) {};
globalCallbackUpdatePlan = (planId) {};
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: context.read<SettingsChangeProvider>(),
listenable: context.watch<SettingsChangeProvider>(),
builder: (context, child) {
const localizationsDelegates = [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
];
const supportedLocales = [
Locale('en', ''),
Locale('de', ''),
];
if (widget.storageError) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
home: const CriticalErrorView(),
);
}
if (widget.recoveryPossible) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
home: const RecoveryView(),
);
}
return MaterialApp.router(
routerConfig: routerProvider,
localizationsDelegates: localizationsDelegates,
scaffoldMessengerKey: globalRootScaffoldMessengerKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
supportedLocales: const [
Locale('en', ''),
Locale('de', ''),
],
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
themeMode: context.watch<SettingsChangeProvider>().themeMode,
);
},
);
@ -133,46 +130,41 @@ class AppMainWidget extends StatefulWidget {
}
class _AppMainWidgetState extends State<AppMainWidget> {
bool _isUserCreated = false;
bool _showDatabaseMigration = false;
bool _showOnboarding = true;
bool _isLoaded = false;
bool _skipBackup = false;
bool _isTwonlyLocked = true;
bool _wasLogged = true;
late int _initialPage;
(Future<int>?, bool) _proofOfWork = (null, false);
@override
void initState() {
super.initState();
_initialPage = widget.initialPage;
Log.info('AppWidgetState: initState started');
initAsync();
super.initState();
}
Future<void> initAsync() async {
Log.info('AppWidgetState: initAsync started');
if (userService.isUserCreated) {
if (_initialPage != 0) {
final count = await twonlyDB.contactsDao.getContactsCount();
if (count == 0) {
_initialPage = 0;
}
}
try {
unawaited(FirebaseMessaging.instance.requestPermission());
} catch (e) {
Log.error(e);
}
_isUserCreated = await isUserCreated();
if (_isUserCreated) {
if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
_isTwonlyLocked = gUser.screenLockEnabled;
}
} else {
if (gUser.appVersion < 62) {
_showDatabaseMigration = true;
}
}
if (!_isUserCreated && !_showDatabaseMigration) {
// This means the user is in the onboarding screen, so start with the Proof of Work.
final (proof, disabled) = await apiService.getProofOfWork();
if (proof != null) {
Log.info('Starting with proof of work calculation.');
// Starting with the proof of work.
_proofOfWork = (
calculatePoW(proof.prefix, proof.difficulty.toInt()),
false,
@ -189,35 +181,31 @@ class _AppMainWidgetState extends State<AppMainWidget> {
@override
Widget build(BuildContext context) {
if (!_wasLogged) {
Log.info('AppWidgetState: build started (_isLoaded: $_isLoaded)');
if (_isLoaded) {
_wasLogged = true;
}
}
if (!_isLoaded) {
return Center(child: Container());
}
late Widget child;
if (userService.isUserCreated) {
if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
// This will only be shown in case the user have not skipped
child = SetupView(
onUpdate: () => setState(() {
// userService.currentUser has updated...
}),
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView(
callBack: () {
_skipBackup = true;
setState(() {});
},
);
} else {
child = HomeView(
initialPage: _initialPage,
initialPage: widget.initialPage,
);
}
} else if (_showOnboarding) {
@ -236,7 +224,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
return Stack(
children: [
child,
const AppOutdatedComp(),
const AppOutdated(),
],
);
}

View file

@ -1,29 +0,0 @@
// 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 '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

View file

@ -1,97 +0,0 @@
// 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';
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
class AnnouncedUser {
final PlatformInt64 userId;
final Uint8List publicKey;
final PlatformInt64 publicId;
const AnnouncedUser({
required this.userId,
required this.publicKey,
required this.publicId,
});
@override
int get hashCode => userId.hashCode ^ publicKey.hashCode ^ publicId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnnouncedUser &&
runtimeType == other.runtimeType &&
userId == other.userId &&
publicKey == other.publicKey &&
publicId == other.publicId;
}
class InitConfig {
final String databaseDir;
final String dataDir;
const InitConfig({
required this.databaseDir,
required this.dataDir,
});
@override
int get hashCode => databaseDir.hashCode ^ dataDir.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databaseDir == other.databaseDir &&
dataDir == other.dataDir;
}
class OtherPromotion {
final int promotionId;
final PlatformInt64 publicId;
final PlatformInt64 fromContactId;
final int threshold;
final Uint8List announcementShare;
final PlatformInt64? publicKeyVerifiedTimestamp;
const OtherPromotion({
required this.promotionId,
required this.publicId,
required this.fromContactId,
required this.threshold,
required this.announcementShare,
this.publicKeyVerifiedTimestamp,
});
@override
int get hashCode =>
promotionId.hashCode ^
publicId.hashCode ^
fromContactId.hashCode ^
threshold.hashCode ^
announcementShare.hashCode ^
publicKeyVerifiedTimestamp.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OtherPromotion &&
runtimeType == other.runtimeType &&
promotionId == other.promotionId &&
publicId == other.publicId &&
fromContactId == other.fromContactId &&
threshold == other.threshold &&
announcementShare == other.announcementShare &&
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
}

View file

@ -1,61 +0,0 @@
// 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 '../bridge.dart';
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
Future<void> initFlutterCallbacks({
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
userDiscoveryVerifySignature,
required FutureOr<bool> Function(PlatformInt64, Uint8List)
userDiscoveryVerifyStoredPubkey,
required FutureOr<bool> Function(List<Uint8List>) userDiscoverySetShares,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetShareForContact,
required FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
userDiscoveryPushOwnPromotionAndClearOldVersion,
required FutureOr<List<Uint8List>?> Function(PlatformInt64)
userDiscoveryGetOwnPromotionsAfterVersion,
required FutureOr<bool> Function(OtherPromotion)
userDiscoveryStoreOtherPromotion,
required FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
userDiscoveryGetOtherPromotionsByPublicId,
required FutureOr<AnnouncedUser?> Function(PlatformInt64)
userDiscoveryGetAnnouncedUserByPublicId,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactVersion,
required FutureOr<bool> Function(PlatformInt64, Uint8List)
userDiscoverySetContactVersion,
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
userDiscoveryPushNewUserRelation,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactPromotion,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
loggingGetStreamSink: loggingGetStreamSink,
userDiscoverySignData: userDiscoverySignData,
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
userDiscoveryVerifyStoredPubkey: userDiscoveryVerifyStoredPubkey,
userDiscoverySetShares: userDiscoverySetShares,
userDiscoveryGetShareForContact: userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotionAndClearOldVersion:
userDiscoveryPushOwnPromotionAndClearOldVersion,
userDiscoveryGetOwnPromotionsAfterVersion:
userDiscoveryGetOwnPromotionsAfterVersion,
userDiscoveryStoreOtherPromotion: userDiscoveryStoreOtherPromotion,
userDiscoveryGetOtherPromotionsByPublicId:
userDiscoveryGetOtherPromotionsByPublicId,
userDiscoveryGetAnnouncedUserByPublicId:
userDiscoveryGetAnnouncedUserByPublicId,
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion,
);

View file

@ -1,87 +0,0 @@
// 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 '../../keys/backup_password_keys.dart';
import '../../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustBackupArchive {
const RustBackupArchive();
static Future<(String, String)> createBackupArchive() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive();
static Future<String?> getBackupDownloadToken() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken();
static Future<void> restoreBackupArchive({required String filePath}) =>
RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive(
filePath: filePath,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupArchive && runtimeType == other.runtimeType;
}
class RustBackupIdentity {
const RustBackupIdentity();
static Future<String?> getBackupId() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupId();
static Future<BackupPasswordKeys> getBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys(
userId: userId,
password: password,
);
static Future<Uint8List> getIdentityBackupBytes() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes();
static Future<void> importBackupPasswordKeys({
required List<int> backupId,
required List<int> encryptionKey,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys(
backupId: backupId,
encryptionKey: encryptionKey,
);
static Future<void> restoreIdentityBackup({
required BackupPasswordKeys keys,
required List<int> encryptedBytes,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup(
keys: keys,
encryptedBytes: encryptedBytes,
);
static Future<void> setBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys(
userId: userId,
password: password,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupIdentity && runtimeType == other.runtimeType;
}

View file

@ -1,77 +0,0 @@
// 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';
class RustKeyManager {
const RustKeyManager();
static Future<Uint8List> getLoginToken() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();
static Future<(Uint8List, PlatformInt64)> getSignalIdentity() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
static Future<void> importSignalIdentity({
required List<int> identityKeyPairStructure,
required PlatformInt64 registrationId,
required Map<PlatformInt64, Uint8List> signedPreKeyStore,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity(
identityKeyPairStructure: identityKeyPairStructure,
registrationId: registrationId,
signedPreKeyStore: signedPreKeyStore,
);
static Future<Uint8List?> loadSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<Map<PlatformInt64, Uint8List>> loadSignedPrekeys() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
static Future<void> removeKeyManager() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
static Future<void> removeSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId);
static Future<void> storeSignedPrekey({
required PlatformInt64 signedPreKeyId,
required List<int> record,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustKeyManager && runtimeType == other.runtimeType;
}

View file

@ -1,73 +0,0 @@
// 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';
class FlutterUserDiscovery {
const FlutterUserDiscovery();
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
static Future<List<Uint8List>> getNewMessages({
required PlatformInt64 contactId,
required List<int> receivedVersion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
contactId: contactId,
receivedVersion: receivedVersion,
);
static Future<void> handleNewMessages({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
messages: messages,
);
static Future<void> initializeOrUpdate({
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
required bool sharePromotion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
threshold: threshold,
userId: userId,
publicKey: publicKey,
sharePromotion: sharePromotion,
);
static Future<Uint8List?> shouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
contactId: contactId,
version: version,
);
static Future<void> updateVerificationStateForUser({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlutterUserDiscovery && runtimeType == other.runtimeType;
}

View file

@ -1,28 +0,0 @@
// 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';
class InitConfig {
final String databasePath;
final String dataDirectory;
const InitConfig({
required this.databasePath,
required this.dataDirectory,
});
@override
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databasePath == other.databasePath &&
dataDirectory == other.dataDirectory;
}

File diff suppressed because it is too large Load diff

View file

@ -1,669 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
dynamic raw,
);
@protected
FutureOr<AnnouncedUser?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<Uint8List>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(List<Uint8List>)
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(OtherPromotion)
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
dynamic raw,
);
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion>? dco_decode_opt_list_other_promotion(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
SseDeserializer deserializer,
);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(List<Uint8List>) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
FutureOr<bool> Function(OtherPromotion) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_announced_user(
AnnouncedUser self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_other_promotion(
List<OtherPromotion>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
}

View file

@ -1,669 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
// Static analysis wrongly picks the IO variant, thus ignore this
// ignore_for_file: argument_type_not_assignable
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
dynamic raw,
);
@protected
FutureOr<AnnouncedUser?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<Uint8List>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(List<Uint8List>)
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(OtherPromotion)
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
dynamic raw,
);
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion>? dco_decode_opt_list_other_promotion(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
SseDeserializer deserializer,
);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(List<Uint8List>) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
FutureOr<bool> Function(OtherPromotion) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_announced_user(
AnnouncedUser self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_other_promotion(
List<OtherPromotion>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
}
@JS('wasm_bindgen')
external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {}

View file

@ -1,29 +0,0 @@
// 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 '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

View file

@ -1,20 +0,0 @@
// 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:collection/collection.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class U8Array32 extends NonGrowableListView<int> {
static const arraySize = 32;
@internal
Uint8List get inner => _inner;
final Uint8List _inner;
U8Array32(this._inner) : assert(_inner.length == arraySize), super(_inner);
U8Array32.init() : this(Uint8List(arraySize));
}

View file

@ -1,37 +1,43 @@
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
class AppEnvironment {
static late String cacheDir;
static late String supportDir;
late ApiService apiService;
static bool _isInitialized = false;
// uses for background notification
late TwonlyDB twonlyDB;
// will be loaded in the main_camera_controller.dart
static List<CameraDescription> cameras = [];
List<CameraDescription> gCameras = <CameraDescription>[];
static Future<void> init() async {
if (_isInitialized) return;
cacheDir = (await getApplicationCacheDirectory()).path;
supportDir = (await getApplicationSupportDirectory()).path;
Log.init();
_isInitialized = true;
}
// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called,
// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart
late UserData gUser;
static void initTesting({String? customCacheDir, String? customSupportDir}) {
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
supportDir = customSupportDir ?? '/tmp/twonly_support';
_isInitialized = true;
}
}
// The following global function can be called from anywhere to update
// the UI when something changed. The callbacks will be set by
// App widget.
class AppState {
static bool isAppInBackground = true;
static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false;
static int latestAppVersionId = 116;
static bool hasCameraPermissions = false;
}
// This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState =
({
required isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
void Function() globalCallbackNewDeviceRegistered = () {};
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true;
bool globalIsInBackgroundTask = false;
bool globalAllowErrorTrackingViaSentry = false;
bool globalGotMessageFromServer = false;
late String globalApplicationCacheDirectory;
late String globalApplicationSupportDirectory;
final GlobalKey<ScaffoldMessengerState> globalRootScaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();

View file

@ -1,17 +0,0 @@
import 'package:get_it/get_it.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
final GetIt locator = GetIt.instance;
void setupLocator() {
locator
..registerLazySingleton<UserService>(UserService.new)
..registerLazySingleton<ApiService>(ApiService.new)
..registerLazySingleton<TwonlyDB>(TwonlyDB.new);
}
UserService get userService => locator<UserService>();
ApiService get apiService => locator<ApiService>();
TwonlyDB get twonlyDB => locator<TwonlyDB>();

View file

@ -1,116 +1,59 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/memories/memories.service.dart';
import 'package:twonly/src/services/migrations.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/startup_guard.dart';
final _initMutex = Mutex();
/// This function is used to initialized the absolute minimum so it
/// can also be used by the backend without the UI was loaded.
Future<bool> twonlyMinimumInitialization() async {
Log.info('twonlyMinimumInitialization: called');
final hasStorageError = await exclusiveAccess(
lockName: 'init',
mutex: _initMutex,
action: () async {
Log.info('twonlyMinimumInitialization: started');
setupLocator();
Log.info('twonlyMinimumInitialization: RustLib.init()');
await RustLib.init();
Log.info('twonlyMinimumInitialization: initFlutterCallbacksForRust()');
await initFlutterCallbacksForRust();
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
try {
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: AppEnvironment.supportDir,
dataDir: AppEnvironment.supportDir,
),
);
} catch (e) {
Log.error(e);
return true;
}
Log.info('twonlyMinimumInitialization: finished');
return false;
},
);
return hasStorageError;
}
import 'package:twonly/src/utils/storage.dart';
void main() async {
final binding = SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final stopwatch = Stopwatch()..start();
SentryWidgetsFlutterBinding.ensureInitialized();
unawaited(StartupGuard.markAppStartup());
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
var storageError = await twonlyMinimumInitialization();
initLogger();
await initFCMService();
var userExists = false;
var user = await getUser();
var recoveryPossible = false;
if (!storageError) {
try {
userExists = await userService.tryInit();
} catch (e) {
Log.error('Failed to initialize user session due to storage error: $e');
storageError = true;
if (Platform.isIOS && user != null) {
final db = File('$globalApplicationSupportDirectory/twonly.sqlite');
if (!db.existsSync()) {
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
await const FlutterSecureStorage().deleteAll();
user = await getUser();
}
}
if (!userExists && !storageError) {
try {
final userId = await RustKeyManager.getUserId();
if (userId != null) {
recoveryPossible = true;
}
} catch (e) {
Log.error('Could not check KeyManager userId for iOS recovery: $e');
}
}
if (user != null) {
gUser = user;
Log.info('User loaded.');
final settingsController = SettingsChangeProvider()..loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
unawaited(initFileDownloader());
if (userExists) {
if (userService.currentUser.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true;
if (user.allowErrorTrackingViaSentry) {
globalAllowErrorTrackingViaSentry = true;
await SentryFlutter.init(
(options) => options
..dsn =
@ -120,23 +63,52 @@ void main() async {
);
}
await runMigrations();
// We wait for the first frame to be rendered before starting heavy tasks.
// This ensures the splash screen is dismissed on Android immediately.
binding.addPostFrameCallback((_) async {
await Future.delayed(const Duration(seconds: 1));
unawaited(postStartupTasks());
unawaited(apiService.connect());
});
unawaited(performTwonlySafeBackup());
unawaited(initializeBackgroundTaskManager());
} else {
Log.info('User is not yet register. Ensure all local data is removed.');
await deleteLocalUserData();
}
await apiService.listenToNetworkChanges();
final settingsController = SettingsChangeProvider();
stopwatch.stop();
await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
Log.info(
'Initialization finished after ${stopwatch.elapsed}. Calling runApp...',
);
unawaited(setupPushNotification());
gCameras = await availableCameras();
apiService = ApiService();
twonlyDB = TwonlyDB();
if (user != null) {
if (gUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await updateUserdata((u) {
u.appVersion = 90;
return u;
});
}
if (gUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91();
await updateUserdata((u) {
u.appVersion = 91;
return u;
});
}
}
await twonlyDB.messagesDao.purgeMessageTable();
await twonlyDB.receiptsDao.purgeReceivedReceipts();
unawaited(MediaFileService.purgeTempFolder());
await initFileDownloader();
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
runApp(
MultiProvider(
@ -146,34 +118,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
],
child: App(
storageError: storageError,
recoveryPossible: recoveryPossible,
),
child: const App(),
),
);
}
Future<void> postStartupTasks() async {
Log.info('Post startup started.');
unawaited(MemoriesService.prewarmCache());
// 1. Immediate background cleanup (Non-blocking for UI)
await twonlyDB.messagesDao.purgeMessageTable();
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
unawaited(MediaFileService.purgeTempFolder());
// 2. Service initializations
unawaited(setupPushNotification());
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
unawaited(UserDiscoveryService.verifyInitializationOnStartup());
await Future.delayed(const Duration(seconds: 10));
unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle)
await Future.delayed(const Duration(minutes: 2));
unawaited(BackupService.makeBackup());
unawaited(cleanLogFile());
}

View file

@ -1,31 +0,0 @@
import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks(
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
userDiscoveryGetShareForContact:
UserDiscoveryCallbacks.userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotionAndClearOldVersion:
UserDiscoveryCallbacks.userDiscoveryPushOwnPromotionAndClearOldVersion,
userDiscoveryPushNewUserRelation:
UserDiscoveryCallbacks.pushNewUserRelation,
userDiscoveryGetOwnPromotionsAfterVersion:
UserDiscoveryCallbacks.getOwnPromotionsAfterVersion,
userDiscoveryStoreOtherPromotion:
UserDiscoveryCallbacks.storeOtherPromotion,
userDiscoveryGetOtherPromotionsByPublicId:
UserDiscoveryCallbacks.getOtherPromotionsByPublicId,
userDiscoveryGetAnnouncedUserByPublicId:
UserDiscoveryCallbacks.getAnnouncedUserByPublicId,
userDiscoveryGetContactVersion: UserDiscoveryCallbacks.getContactVersion,
userDiscoverySetContactVersion: UserDiscoveryCallbacks.setContactVersion,
userDiscoverySignData: UserDiscoveryCallbacks.signData,
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
userDiscoveryGetContactPromotion:
UserDiscoveryCallbacks.getContactPromotion,
);
}

View file

@ -1,33 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:twonly/src/utils/log.dart';
class LoggingCallbacks {
static Future<RustStreamSink<String>> getStreamSink() async {
final dartLogSink = RustStreamSink<String>();
Timer.periodic(const Duration(milliseconds: 100), (timer) {
try {
dartLogSink.stream.listen(
(log) {
if (log.contains('INFO ')) {
Log.info(log.split('INFO ')[1]);
} else if (log.contains('DEBUG ')) {
Log.info(log.split('DEBUG ')[1]);
} else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) {
// ignore: avoid_print
print(log);
}
},
);
timer.cancel();
} catch (e) {
// stream not yet initialized
}
});
return dartLogSink;
}
}

View file

@ -1,326 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show Curve, IdentityKey;
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:twonly/core/bridge.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class UserDiscoveryCallbacks {
static Future<Uint8List?> signData(
Uint8List inputData,
) async {
Log.info('UserDiscoveryCallbacks: signData started');
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) {
Log.error('UserDiscoveryCallbacks: signData failed, privKey is null');
return null;
}
final random = getRandomUint8List(32);
final signature = sign(
privKey.serialize(),
inputData,
random,
);
privKey = null;
Log.info('UserDiscoveryCallbacks: signData finished');
return signature;
}
static Future<bool> verifySignature(
Uint8List inputData,
Uint8List pubKey,
Uint8List signature,
) async {
try {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
} catch (_) {
return false;
}
}
static Future<bool> verifyStoredPubKey(
int contactId,
Uint8List pubKey,
) async {
try {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey);
} else {
return false;
}
} catch (_) {
return false;
}
}
static Future<bool> setShares(List<Uint8List> shares) async {
try {
// First remove all old shares then insert all the new shares
await twonlyDB.delete(twonlyDB.userDiscoveryShares).go();
await twonlyDB.batch((b) {
b.insertAll(
twonlyDB.userDiscoveryShares,
shares
.map((s) => UserDiscoverySharesCompanion(share: Value(s)))
.toList(),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<Uint8List?> userDiscoveryGetShareForContact(
int contactId,
) async {
return twonlyDB.transaction(() async {
// 1. Check if this contact already has a share assigned
final existing =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
if (existing != null) {
return existing.share;
}
// 2. No share found. Find an available one (where contactId is null)
final available =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.isNull())
..limit(1))
.getSingleOrNull();
if (available != null) {
// 3. Assign the contactId to this available share
await (twonlyDB.update(
twonlyDB.userDiscoveryShares,
)..where((tbl) => tbl.shareId.equals(available.shareId))).write(
UserDiscoverySharesCompanion(
contactId: Value(contactId),
),
);
return available.share;
}
return null; // 4. No existing or available shares found
});
}
static Future<bool> userDiscoveryPushOwnPromotionAndClearOldVersion(
int contactId,
int version,
Uint8List promotion,
) async {
try {
// Old promotions from this users should be removed...
await (twonlyDB.update(
twonlyDB.userDiscoveryOwnPromotions,
)..where((t) => t.contactId.equals(contactId))).write(
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
);
await twonlyDB
.into(twonlyDB.userDiscoveryOwnPromotions)
.insert(
UserDiscoveryOwnPromotionsCompanion.insert(
contactId: contactId,
promotion: promotion,
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<Uint8List>> getOwnPromotionsAfterVersion(
int version,
) async {
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
..where((tbl) => tbl.versionId.isBiggerThanValue(version));
final rows = await query.get();
return rows.map((r) => r.promotion).toList();
}
static Future<bool> storeOtherPromotion(
OtherPromotion promotion,
) async {
try {
await twonlyDB
.into(twonlyDB.userDiscoveryOtherPromotions)
.insertOnConflictUpdate(
UserDiscoveryOtherPromotionsCompanion(
promotionId: Value(promotion.promotionId),
publicId: Value(promotion.publicId),
fromContactId: Value(promotion.fromContactId),
threshold: Value(promotion.threshold),
announcementShare: Value(promotion.announcementShare),
publicKeyVerifiedTimestamp: Value(
promotion.publicKeyVerifiedTimestamp == null
? null
: DateTime.fromMillisecondsSinceEpoch(
promotion.publicKeyVerifiedTimestamp!,
),
),
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<OtherPromotion>> getOtherPromotionsByPublicId(
int publicId,
) async {
final rows = await (twonlyDB.select(
twonlyDB.userDiscoveryOtherPromotions,
)..where((tbl) => tbl.publicId.equals(publicId))).get();
return rows
.map(
(row) => OtherPromotion(
promotionId: row.promotionId,
publicId: row.publicId,
fromContactId: row.fromContactId,
threshold: row.threshold,
announcementShare: row.announcementShare,
publicKeyVerifiedTimestamp:
row.publicKeyVerifiedTimestamp?.millisecondsSinceEpoch,
),
)
.toList();
}
static Future<AnnouncedUser?> getAnnouncedUserByPublicId(
int publicId,
) async {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.publicId.equals(publicId))).getSingleOrNull();
if (row == null) return null;
return AnnouncedUser(
userId: row.announcedUserId,
publicKey: row.announcedPublicKey,
publicId: row.publicId,
);
}
static Future<bool> pushNewUserRelation(
int fromContactId,
AnnouncedUser announcedUser,
int? publicKeyVerifiedTimestamp,
) async {
try {
await twonlyDB.transaction(() async {
// 1. Ensure the user exists in the AnnouncedUsers table
await twonlyDB
.into(twonlyDB.userDiscoveryAnnouncedUsers)
.insertOnConflictUpdate(
UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: Value(announcedUser.userId),
announcedPublicKey: Value(announcedUser.publicKey),
publicId: Value(announcedUser.publicId),
),
);
// 2. Insert or update the relation
await twonlyDB
.into(twonlyDB.userDiscoveryUserRelations)
.insertOnConflictUpdate(
UserDiscoveryUserRelationsCompanion.insert(
announcedUserId: announcedUser.userId,
fromContactId: fromContactId,
publicKeyVerifiedTimestamp: Value(
publicKeyVerifiedTimestamp != null
? DateTime.fromMillisecondsSinceEpoch(
publicKeyVerifiedTimestamp,
)
: null,
),
),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
// static Future<Map<AnnouncedUser, List<(int, DateTime?)>>>
// getAllAnnouncedUsers() async {
// final query = twonlyDB.select(twonlyDB.userDiscoveryAnnouncedUsers).join([
// innerJoin(
// twonlyDB.userDiscoveryUserRelations,
// twonlyDB.userDiscoveryUserRelations.announcedUserId.equalsExp(
// twonlyDB.userDiscoveryAnnouncedUsers.announcedUserId,
// ),
// ),
// ]);
// final results = await query.get();
// final map = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{};
// for (final row in results) {
// final user = row.readTable(twonlyDB.userDiscoveryAnnouncedUsers);
// final relation = row.readTable(twonlyDB.userDiscoveryUserRelations);
// map.putIfAbsent(user, () => []).add(
// (relation.fromContactId, relation.publicKeyVerifiedTimestamp),
// );
// }
// return map;
// }
static Future<Uint8List?> getContactVersion(int contactId) async {
final row = await (twonlyDB.select(
twonlyDB.contacts,
)..where((tbl) => tbl.userId.equals(contactId))).getSingleOrNull();
return row?.userDiscoveryVersion;
}
static Future<bool> setContactVersion(int contactId, Uint8List update) async {
try {
await (twonlyDB.update(twonlyDB.contacts)
..where((tbl) => tbl.userId.equals(contactId)))
.write(ContactsCompanion(userDiscoveryVersion: Value(update)));
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<Uint8List?> getContactPromotion(int contactId) async {
try {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryOwnPromotions,
)..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull();
return row?.promotion;
} catch (e) {
Log.error(e);
return null;
}
}
}

View file

@ -1,6 +1,4 @@
class KeyValueKeys {
static const String lastPeriodicTaskExecution =
'last_periodic_task_execution';
static const String currentBackupState = 'current_backup_state';
static const String backupRecoveryState = 'backup_recovery_state';
}

View file

@ -25,6 +25,7 @@ class Routes {
static const String settingsAccount = '/settings/account';
static const String settingsSubscription = '/settings/subscription';
static const String settingsBackup = '/settings/backup';
static const String settingsBackupServer = '/settings/backup/server';
static const String settingsBackupRecovery = '/settings/backup/recovery';
static const String settingsBackupSetup = '/settings/backup/setup';
static const String settingsAppearance = '/settings/appearance';
@ -33,16 +34,9 @@ class Routes {
static const String settingsPrivacy = '/settings/privacy';
static const String settingsPrivacyBlockUsers =
'/settings/privacy/block_users';
static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery';
static const String settingsPrivacyProfileSelection =
'/settings/privacy/profile_selection';
static const String settingsNotification = '/settings/notification';
static const String settingsStorage = '/settings/storage_data';
static const String settingsStorageManage = '/settings/storage_data/manage';
static const String settingsStorageImport = '/settings/storage_data/import';
static const String settingsStorageImportGallery =
'/settings/storage_data/import_gallery';
static const String settingsStorageExport = '/settings/storage_data/export';
static const String settingsHelp = '/settings/help';
static const String settingsHelpFaq = '/settings/help/faq';

View file

@ -1,15 +1,11 @@
class SecureStorageKeys {
@Deprecated('Use the secure storage in rust')
static const String signalIdentity = 'signal_identity';
@Deprecated('Use the secure storage in rust')
static const String signalSignedPreKey = 'signed_pre_key_store';
@Deprecated('Use the login token')
static const String apiAuthToken = 'api_auth_token';
@Deprecated('Use user.json file')
static const String googleFcm = 'google_fcm';
static const String userData = 'userData';
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
// Not required for backup...
static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending';
}

View file

@ -1,5 +1,5 @@
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
part 'contacts.dao.g.dart';
@DriftAccessor(tables: [Contacts, KeyVerifications])
@DriftAccessor(tables: [Contacts])
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -103,13 +103,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return select(contacts).get();
}
Future<int> getContactsCount() async {
final count = contacts.userId.count();
final query = selectOnly(contacts)..addColumns([count]);
final result = await query.map((row) => row.read(count)).getSingle();
return result ?? 0;
}
Stream<int?> watchContactsBlocked() {
final count = contacts.userId.count();
final query = selectOnly(contacts)
@ -141,44 +134,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.watch();
}
Stream<List<Contact>> watchContactsAnnouncedViaUserDiscovery() {
return (select(contacts)..where((t) {
var expr =
t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.accountDeleted.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
}))
.watch();
}
Future<List<Contact>> getContactsAnnouncedViaUserDiscovery() async {
return (select(contacts)..where((t) {
var expr =
t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.accountDeleted.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
}))
.get();
}
Stream<List<Contact>> watchAllContacts() {
return select(contacts).watch();
}

View file

@ -5,8 +5,6 @@ part of 'contacts.dao.dart';
// ignore_for_file: type=lint
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
ContactsDaoManager get managers => ContactsDaoManager(this);
}
@ -15,9 +13,4 @@ class ContactsDaoManager {
ContactsDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
}

View file

@ -1,7 +1,6 @@
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart';
@ -114,10 +113,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int contactId,
GroupsCompanion group,
) async {
final groupIdDirectChat = getUUIDforDirectChat(
contactId,
userService.currentUser.userId,
);
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId);
final insertGroup = group.copyWith(
groupId: Value(groupIdDirectChat),
isDirectChat: const Value(true),
@ -140,10 +136,15 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
}
Future<Group?> _insertGroup(GroupsCompanion group) async {
await into(groups).insertOnConflictUpdate(group);
return (select(
groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull();
try {
await into(groups).insert(group);
return await (select(
groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
} catch (e) {
Log.error('Could not insert group: $e');
return null;
}
}
Future<List<Contact>> getGroupContact(String groupId) async {
@ -208,10 +209,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
}
Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(
contactId,
userService.currentUser.userId,
);
final groupId = getUUIDforDirectChat(contactId, gUser.userId);
return (select(
groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
@ -237,7 +235,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
}
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
Stream<int> watchFlameCounter(String groupId) {
return (select(groups)..where(
(u) =>
u.groupId.equals(groupId) &
@ -245,7 +243,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
u.lastMessageSend.isNotNull(),
))
.watchSingleOrNull()
.map(getFlameCounterFromGroup);
.asyncMap(getFlameCounterFromGroup);
}
Future<List<Group>> getAllDirectChats() {
@ -273,7 +271,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groups.groupId.equalsExp(groupMembers.groupId),
),
],
)..where(groups.isDirectChat.equals(false)));
)..where(groups.isDirectChat.isNull()));
return query.map((row) => row.readTable(groupMembers)).get();
} catch (e) {
Log.error(e);
@ -293,27 +291,6 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return query.map((row) => row.readTable(groups)).getSingleOrNull();
}
Future<Group?> createOrGetDirectChat(int contactId) async {
var directChat = await getDirectChat(contactId);
if (directChat == null) {
final contact = await attachedDatabase.contactsDao.getContactById(
contactId,
);
if (contact == null) {
Log.error('Contact $contactId not found, cannot create direct chat');
return null;
}
await createNewDirectChat(
contactId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
directChat = await getDirectChat(contactId);
}
return directChat;
}
Stream<int> watchSumTotalMediaCounter() {
final query = selectOnly(groups)
..addColumns([groups.totalMediaCounter.sum()]);
@ -334,33 +311,4 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
))
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
}
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
final query =
select(groups).join([
innerJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])..where(
groups.isDirectChat.equals(false) &
groupMembers.contactId.equals(contactId),
);
return query.map((row) => row.readTable(groups)).watch();
}
Future<List<Group>> getGroupsForMember(int contactId) {
final query =
select(groups).join([
innerJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])..where(
groupMembers.contactId.equals(contactId),
);
return query.map((row) => row.readTable(groups)).get();
}
}

View file

@ -1,261 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'key_verification.dao.g.dart';
enum VerificationStatus { trusted, partialTrusted, notTrusted }
@DriftAccessor(
tables: [
Contacts,
VerificationTokens,
KeyVerifications,
GroupMembers,
UserDiscoveryUserRelations,
],
)
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
with _$KeyVerificationDaoMixin {
// ignore: matching_super_parameters
KeyVerificationDao(super.db);
Future<List<VerificationToken>> getRecentVerificationTokens() {
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
return (select(
verificationTokens,
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
}
Future<int> insertVerificationToken(Uint8List token) {
return into(verificationTokens).insert(
VerificationTokensCompanion.insert(token: token),
);
}
/// Returns a map of contactId the verification type of the earliest
/// [KeyVerification] row for that contact.
Future<Map<int, VerificationType>>
getFirstVerificationTypeByContacts() async {
final rows = await (select(
keyVerifications,
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
final result = <int, VerificationType>{};
for (final row in rows) {
result.putIfAbsent(row.contactId, () => row.type);
}
return result;
}
Future<bool> isContactVerified(int contactId) async {
final row =
await (select(keyVerifications)
..where((kv) => kv.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
return row != null;
}
Stream<List<KeyVerification>> watchContactVerification(int contactId) {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).watch();
}
Future<List<KeyVerification>> getContactVerification(int contactId) async {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).get();
}
Stream<List<(Contact, DateTime)>> watchTransferredTrustVerifications(
int contactId,
) {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query =
(select(contacts)..where((u) => u.userId.equals(contactId).not())).join(
[
innerJoin(
ur,
ur.fromContactId.equalsExp(contacts.userId),
),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
],
)
..where(
ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(),
)
..groupBy([contacts.userId]);
return query.watch().map((rows) {
return rows.map((row) {
final contact = row.readTable(contacts);
final timestamp = row.readTable(ur).publicKeyVerifiedTimestamp!;
return (contact, timestamp);
}).toList();
});
}
Future<int> getTransferredTrustVerificationsCount() async {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query = selectOnly(ur, distinct: true)
..addColumns([ur.announcedUserId])
..join([
innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
])
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
return rows.length;
}
Future<int> getCountOfContactsWithVerificationBadge() async {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query = selectOnly(ur, distinct: true)
..addColumns([ur.announcedUserId])
..join([
innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
])
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
final transferredIds = rows.map((r) => r.read(ur.announcedUserId)!).toSet();
final directVerifications = await select(kv).get();
final directIds = directVerifications.map((v) => v.contactId).toSet();
// Reduce transferred contacts where announcedUserId is already in KeyVerifications
transferredIds.removeWhere(directIds.contains);
// Add count of all users who are in the KeyVerification table
return transferredIds.length + directIds.length;
}
Stream<VerificationStatus> watchAllGroupMembersVerified(String groupId) {
final gm = groupMembers;
final directKv = alias(keyVerifications, 'directKv');
final ur = userDiscoveryUserRelations;
final verifierKv = alias(keyVerifications, 'verifierKv');
final query = select(gm).join([
leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
leftOuterJoin(
ur,
ur.announcedUserId.equalsExp(gm.contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.fromContactId.equalsExp(gm.contactId).not(),
),
leftOuterJoin(
verifierKv,
verifierKv.contactId.equalsExp(ur.fromContactId),
),
])..where(gm.groupId.equals(groupId));
return query.watch().map((rows) {
if (rows.isEmpty) return VerificationStatus.notTrusted;
final memberTrustMap = <int, ({bool direct, bool partial})>{};
for (final row in rows) {
final contactId = row.readTable(gm).contactId;
final isDirect = row.readTableOrNull(directKv) != null;
final isPartial = row.readTableOrNull(verifierKv) != null;
final current =
memberTrustMap[contactId] ?? (direct: false, partial: false);
memberTrustMap[contactId] = (
direct: current.direct || isDirect,
partial: current.partial || isPartial,
);
}
final allDirect = memberTrustMap.values.every((m) => m.direct);
if (allDirect) return VerificationStatus.trusted;
final allAtLeastPartial = memberTrustMap.values.every(
(m) => m.direct || m.partial,
);
if (allAtLeastPartial) return VerificationStatus.partialTrusted;
return VerificationStatus.notTrusted;
});
}
Future<void> addKeyVerification(int contactId, VerificationType type) async {
try {
await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion(
contactId: Value(contactId),
type: Value(type),
),
);
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
);
}
} catch (e) {
Log.error(e);
}
}
Future<void> deleteKeyVerification(int contactId) async {
try {
await (delete(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).go();
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
Future<void> deleteKeyVerificationById(
int verificationId,
int contactId,
) async {
try {
await (delete(
keyVerifications,
)..where((kv) => kv.verificationId.equals(verificationId))).go();
final remaining = await getContactVerification(contactId);
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
}

View file

@ -1,52 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'key_verification.dao.dart';
// ignore_for_file: type=lint
mixin _$KeyVerificationDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$VerificationTokensTable get verificationTokens =>
attachedDatabase.verificationTokens;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
$GroupsTable get groups => attachedDatabase.groups;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers =>
attachedDatabase.userDiscoveryAnnouncedUsers;
$UserDiscoveryUserRelationsTable get userDiscoveryUserRelations =>
attachedDatabase.userDiscoveryUserRelations;
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
}
class KeyVerificationDaoManager {
final _$KeyVerificationDaoMixin _db;
KeyVerificationDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$VerificationTokensTableTableManager get verificationTokens =>
$$VerificationTokensTableTableManager(
_db.attachedDatabase,
_db.verificationTokens,
);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$GroupMembersTableTableManager get groupMembers =>
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
$$UserDiscoveryAnnouncedUsersTableTableManager
get userDiscoveryAnnouncedUsers =>
$$UserDiscoveryAnnouncedUsersTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryAnnouncedUsers,
);
$$UserDiscoveryUserRelationsTableTableManager
get userDiscoveryUserRelations =>
$$UserDiscoveryUserRelationsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryUserRelations,
);
}

View file

@ -65,10 +65,6 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
)..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull();
}
Future<List<MediaFile>> getMediaFilesByIds(List<String> mediaIds) async {
return (select(mediaFiles)..where((t) => t.mediaId.isIn(mediaIds))).get();
}
Future<MediaFile?> getDraftMediaFile() async {
final medias = await (select(
mediaFiles,
@ -114,15 +110,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.get();
}
Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
return (select(mediaFiles)..where(
(t) =>
t.stored.equals(true) &
(t.storedFileHash.isNull() |
t.hasCropAnalyzed.equals(false) |
(t.hasThumbnail.equals(false) &
t.type.equals(MediaType.audio.name).not()) |
t.sizeInBytes.isNull()),
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
))
.get();
}
@ -164,43 +154,4 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
),
);
}
Future<List<String>> getMessageIdsByMediaHash(
Uint8List hash,
int senderId,
) async {
final query =
select(db.messages).join([
innerJoin(
mediaFiles,
mediaFiles.mediaId.equalsExp(db.messages.mediaId),
),
])..where(
mediaFiles.storedFileHash.equals(hash) &
db.messages.senderId.equals(senderId) &
db.messages.openedAt.isNull(),
);
final rows = await query.get();
return rows.map((row) => row.readTable(db.messages).messageId).toList();
}
Future<List<MediaFile>> getMediaByHash(Uint8List hash) async {
final query = select(db.mediaFiles)
..where((t) => t.storedFileHash.equals(hash));
return query.get();
}
Future<Map<MediaType, int>> getStorageStats() async {
final rows = await select(mediaFiles).get();
final stats = <MediaType, int>{};
for (final row in rows) {
final type = row.type;
final size = row.sizeInBytes ?? 0;
stats[type] = (stats[type] ?? 0) + size;
}
return stats;
}
}

View file

@ -1,7 +1,7 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -102,7 +102,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
t.openedAt.isNull() |
t.mediaStored.equals(true)) &
(t.isDeletedFromSender.equals(true) |
(t.type.equals(MessageType.text.name).not() &
(t.type.equals(MessageType.text.name).not() |
t.type.equals(MessageType.media.name).not()) |
(t.type.equals(MessageType.text.name) &
t.content.isNotNull()) |
@ -140,41 +140,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
Future<void> purgeMessageTable() async {
final allGroups = await select(groups).get();
final groupedByTime = <int, List<String>>{};
for (final g in allGroups) {
groupedByTime
.putIfAbsent(g.deleteMessagesAfterMilliseconds, () => [])
.add(g.groupId);
}
for (final entry in groupedByTime.entries) {
for (final group in allGroups) {
final deletionTime = clock.now().subtract(
Duration(milliseconds: entry.key),
Duration(
milliseconds: group.deleteMessagesAfterMilliseconds,
),
);
final groupIds = entry.value;
final deletedCount =
await (delete(messages)..where(
(m) =>
m.groupId.isIn(groupIds) &
((m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true)) |
m.mediaStored.equals(false)) &
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
(m.openedByAll.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
if (deletedCount > 0) {
Log.info(
'Deleted $deletedCount messages for groups $groupIds due to retention policy.',
);
}
await (delete(messages)..where(
(m) =>
m.groupId.equals(group.groupId) &
(m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true) |
m.mediaStored.equals(false)) &
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
(m.openedByAll.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
}
}
Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(clock.now()));
return (update(messages)..where(
(t) =>
t.groupId.equals(groupId) &
t.senderId.isNotNull() &
t.openedAt.isNull() &
t.type.equals(MessageType.text.name),
))
.write(updates);
}
Future<void> handleMessageDeletion(
int? contactId,
String messageId,
@ -186,35 +184,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return;
}
if (msg.mediaId != null && contactId != null) {
final otherMessagesWithSameMedia =
await (select(messages)..where(
(t) =>
t.mediaId.equals(msg.mediaId!) &
t.messageId.equals(messageId).not(),
))
.get();
// contactId -> When a image is send to multiple and one message is delete the image should be still available...
await (delete(
mediaFiles,
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
if (otherMessagesWithSameMedia.isEmpty) {
await (delete(
mediaFiles,
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) {
mediaService.fullMediaRemoval();
}
} else {
Log.info(
'Media ${msg.mediaId} is still used by ${otherMessagesWithSameMedia.length} other messages. Skipping physical deletion.',
);
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) {
mediaService.fullMediaRemoval();
}
}
await (delete(
messageHistories,
)..where((t) => t.messageId.equals(messageId))).go();
await twonlyDB.receiptsDao.deleteReceiptsByMessageId(messageId);
await (update(messages)..where(
(t) => t.messageId.equals(messageId),
))
@ -256,70 +239,41 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
}
Future<void> handleMessagesOpened(
Value<int> contactId,
int contactId,
List<String> messageIds,
DateTime timestamp,
) async {
for (final messageId in messageIds) {
try {
var actionTimestamp = timestamp;
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
Log.warn(
'Receiver clock skew detected for message $messageId. '
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
'Clamping to creation time.',
);
actionTimestamp = msg.createdAt;
}
final ts = actionTimestamp;
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: contactId,
type: const Value(MessageActionType.openedAt),
actionAt: Value(ts),
),
);
final isOpenedByAll = await haveAllMembers(
messageId,
MessageActionType.openedAt,
);
await (update(
messages,
)..where((tbl) => tbl.messageId.equals(messageId))).write(
MessagesCompanion(
openedAt: Value(ts),
openedByAll: Value(isOpenedByAll ? ts : null),
),
);
});
// Read-back verification: confirm the write was persisted.
final verified = await getMessageById(messageId).getSingleOrNull();
if (verified != null && verified.openedAt == null) {
Log.warn(
'handleMessagesOpened read-back failed for $messageId, retrying',
);
await (update(
messages,
)..where((tbl) => tbl.messageId.equals(messageId))).write(
MessagesCompanion(
openedAt: Value(actionTimestamp),
),
);
}
Log.info(
'handleMessagesOpened completed for message $messageId',
await batch((batch) async {
for (final messageId in messageIds) {
batch.insert(
messageActions,
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.openedAt),
actionAt: Value(timestamp),
),
mode: InsertMode.insertOrReplace,
);
} catch (e) {
Log.error('handleMessagesOpened failed for $messageId: $e');
}
}
for (final messageId in messageIds) {
final isOpenedByAll = await haveAllMembers(
messageId,
MessageActionType.openedAt,
);
final now = clock.now();
batch.update(
twonlyDB.messages,
MessagesCompanion(
openedAt: Value(now),
openedByAll: Value(isOpenedByAll ? now : null),
),
where: (tbl) => tbl.messageId.equals(messageId),
);
}
});
}
Future<void> handleMessageAckByServer(
@ -327,67 +281,57 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId,
DateTime timestamp,
) async {
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.ackByServerAt),
actionAt: Value(timestamp),
),
);
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(ackByServer: Value(timestamp)),
);
});
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.ackByServerAt),
actionAt: Value(timestamp),
),
);
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(ackByServer: Value(clock.now())),
);
}
Future<bool> haveAllMembers(
String messageId,
MessageActionType action,
) async {
try {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null) return true;
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
message.groupId,
);
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null) return true;
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
message.groupId,
);
final actions =
await (select(messageActions)..where(
(t) =>
t.type.equals(action.name) & t.messageId.equals(messageId),
))
.get();
final actions =
await (select(messageActions)..where(
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
))
.get();
return members.length == actions.length;
} catch (e) {
Log.error(e);
return true;
}
return members.length == actions.length;
}
Future<void> updateMessageId(
String messageId,
MessagesCompanion updatedValues,
) async {
final count = await (update(
await (update(
messages,
)..where((c) => c.messageId.equals(messageId))).write(updatedValues);
Log.info('Updated $count message(s) with messageId $messageId');
}
Future<void> updateMessagesByMediaId(
String mediaId,
MessagesCompanion updatedValues,
) async {
final count = await (update(
) {
return (update(
messages,
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
Log.info('Updated $count message(s) with mediaId $mediaId');
}
Future<Message?> insertMessage(MessagesCompanion message) async {
@ -400,7 +344,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
await into(messages).insertOnConflictUpdate(insertMessage);
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
await twonlyDB.groupsDao.updateGroup(
message.groupId.value,
@ -421,11 +365,9 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
final messageId = insertMessage.messageId.value;
return await (select(
messages,
)..where((t) => t.messageId.equals(messageId))).getSingle();
)..where((t) => t.rowId.equals(rowId))).getSingle();
} catch (e) {
Log.error('Could not insert message: $e');
return null;
@ -457,10 +399,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get();
}
Future<List<Message>> getMessagesByMediaIds(List<String> mediaIds) async {
return (select(messages)..where((t) => t.mediaId.isIn(mediaIds))).get();
}
Stream<List<(MessageAction, Contact)>> watchMessageActions(String messageId) {
final query = (select(messageActions).join([
leftOuterJoin(

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
part 'reactions.dao.g.dart';
@ -29,14 +29,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
final msg = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (msg == null) {
Log.error('updateReaction: Message $messageId not found!');
return;
}
if (msg.groupId != groupId) {
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId');
return;
}
if (msg == null || msg.groupId != groupId) return;
try {
if (remove) {

View file

@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart';
@ -54,13 +54,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.go();
}
Future<void> deleteReceiptsByMessageId(String messageId) async {
await (delete(receipts)..where(
(t) => t.messageId.equals(messageId),
))
.go();
}
Future<void> deleteReceiptForUser(int contactId) async {
await (delete(receipts)..where(
(t) => t.contactId.equals(contactId),
@ -98,11 +91,10 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
receiptId: Value(uuid.v4()),
);
}
await into(receipts).insert(insertEntry);
final receiptId = insertEntry.receiptId.value;
final id = await into(receipts).insert(insertEntry);
return await (select(
receipts,
)..where((t) => t.receiptId.equals(receiptId))).getSingle();
)..where((t) => t.rowId.equals(id))).getSingle();
} catch (e) {
// ignore error, receipts is already in the database...
return null;
@ -191,23 +183,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
}
Future<Receipt?> rotateReceiptId(String oldReceiptId) async {
final newReceiptId = uuid.v4();
await updateReceipt(
oldReceiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
),
);
final updatedReceipt = await getReceiptById(newReceiptId);
if (updatedReceipt == null) {
Log.error(
'Tried to change the receipt ID, but could not get the updated receipt...',
);
}
return updatedReceipt;
}
Future<void> updateReceiptByContactAndMessageId(
int contactId,
String messageId,

View file

@ -1,79 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'shortcuts.dao.g.dart';
@DriftAccessor(
tables: [
Shortcuts,
ShortcutMembers,
],
)
class ShortcutsDao extends DatabaseAccessor<TwonlyDB> with _$ShortcutsDaoMixin {
ShortcutsDao(super.db);
Stream<List<Shortcut>> watchAllShortcuts() {
return select(shortcuts).watch();
}
Future<Shortcut?> getShortcutByEmoji(String emoji) {
return (select(
shortcuts,
)..where((t) => t.emoji.equals(emoji))).getSingleOrNull();
}
Future<void> createShortcut(String emoji) async {
try {
await into(shortcuts).insert(
ShortcutsCompanion.insert(emoji: emoji),
);
// ignore: empty_catches
} catch (e) {}
}
Future<void> addShortcutMembers(int shortcutId, List<String> groupIds) async {
await batch((b) {
b.insertAll(
shortcutMembers,
groupIds.map(
(gId) => ShortcutMembersCompanion.insert(
shortcutId: shortcutId,
groupId: gId,
),
),
);
});
}
Future<List<ShortcutMember>> getShortcutMembers(int shortcutId) {
return (select(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).get();
}
Future<void> incrementUsage(int shortcutId) async {
await customStatement(
'UPDATE shortcuts SET usage_counter = usage_counter + 1 WHERE id = ?',
[shortcutId],
);
// Notify updates to trigger streams
notifyUpdates({TableUpdate.onTable(shortcuts, kind: UpdateKind.update)});
}
Future<void> updateShortcut(int shortcutId, String emoji) async {
await (update(shortcuts)..where((t) => t.id.equals(shortcutId))).write(
ShortcutsCompanion(emoji: Value(emoji)),
);
}
Future<void> deleteShortcutMembers(int shortcutId) async {
await (delete(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).go();
}
Future<void> deleteShortcut(int shortcutId) async {
await (delete(shortcuts)..where((t) => t.id.equals(shortcutId))).go();
}
}

View file

@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shortcuts.dao.dart';
// ignore_for_file: type=lint
mixin _$ShortcutsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ShortcutsTable get shortcuts => attachedDatabase.shortcuts;
$GroupsTable get groups => attachedDatabase.groups;
$ShortcutMembersTable get shortcutMembers => attachedDatabase.shortcutMembers;
ShortcutsDaoManager get managers => ShortcutsDaoManager(this);
}
class ShortcutsDaoManager {
final _$ShortcutsDaoMixin _db;
ShortcutsDaoManager(this._db);
$$ShortcutsTableTableManager get shortcuts =>
$$ShortcutsTableTableManager(_db.attachedDatabase, _db.shortcuts);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$ShortcutMembersTableTableManager get shortcutMembers =>
$$ShortcutMembersTableTableManager(
_db.attachedDatabase,
_db.shortcutMembers,
);
}

View file

@ -1,251 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'user_discovery.dao.g.dart';
typedef AnnouncedUsersWithRelations =
Map<UserDiscoveryAnnouncedUser, List<(Contact, DateTime?)>>;
@DriftAccessor(
tables: [
UserDiscoveryAnnouncedUsers,
UserDiscoveryUserRelations,
UserDiscoveryOwnPromotions,
UserDiscoveryOtherPromotions,
UserDiscoveryShares,
Contacts,
],
)
class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
with _$UserDiscoveryDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
// ignore: matching_super_parameters
UserDiscoveryDao(super.db);
/// 1. Get count for contacts which are in announced but not in the contacts table
/// Returns all users which are not yet in the contacts table but have no data loaded (e.g. Avatar, username and display name)
Future<List<UserDiscoveryAnnouncedUser>>
getNewAnnouncementsWithoutData() async {
final query =
select(userDiscoveryAnnouncedUsers).join([
leftOuterJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])
// Apply filters:
// 1. The user must NOT exist in the contacts table
// 2. The username must be null
..where(
contacts.userId.isNull() &
userDiscoveryAnnouncedUsers.username.isNull(),
);
return (await query.get())
.map((row) => row.readTable(userDiscoveryAnnouncedUsers))
.toList();
}
Future<AnnouncedUsersWithRelations>
getAllAnnouncedUsersWithRelations() async {
final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
])..where(userDiscoveryAnnouncedUsers.username.isNotNull());
final rows = await query.get();
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
}
Stream<AnnouncedUsersWithRelations> watchAllAnnouncedUsersWithRelations() {
final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
])..where(userDiscoveryAnnouncedUsers.username.isNotNull());
return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
Log.info('results = ${results.length}');
return results;
});
}
Stream<AnnouncedUsersWithRelations> watchNewAnnouncedUsersWithRelations() {
final announcedContact = alias(contacts, 'announcedContact');
final query =
select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
leftOuterJoin(
announcedContact,
announcedContact.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])..where(
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.isHidden.equals(false) &
(announcedContact.userId.isNull() |
announcedContact.deletedByUser.equals(true)),
);
return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
});
}
Stream<int> watchNewAnnouncementsWithDataCount() {
final countExp = userDiscoveryAnnouncedUsers.announcedUserId.count();
final query = selectOnly(userDiscoveryAnnouncedUsers)
..addColumns([countExp])
..where(
// Filters: Has a username AND has not been shown to the user yet
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
userDiscoveryAnnouncedUsers.isHidden.equals(false),
);
return query.watchSingle().map((row) => row.read(countExp) ?? 0);
}
Future<void> markAllValidAnnouncedUsersAsShown() async {
await (update(userDiscoveryAnnouncedUsers)..where(
(t) =>
t.username.isNotNull() &
t.wasShownToTheUser.equals(false) &
t.isHidden.equals(false),
))
.write(
const UserDiscoveryAnnouncedUsersCompanion(
wasShownToTheUser: Value(true),
),
);
}
Future<void> updateAnnouncedUser(
int announcedUserId,
UserDiscoveryAnnouncedUsersCompanion updatedValues,
) async {
await (update(
userDiscoveryAnnouncedUsers,
)..where((c) => c.announcedUserId.equals(announcedUserId))).write(
updatedValues,
);
}
Future<UserDiscoveryAnnouncedUser?> getAnnouncedUserById(int id) async {
return (select(
userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull();
}
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
select(userDiscoveryAnnouncedUsers).watch();
Stream<List<UserDiscoveryUserRelation>> watchAllUserRelations() =>
select(userDiscoveryUserRelations).watch();
Stream<List<UserDiscoveryOwnPromotion>> watchAllOwnPromotions() =>
select(userDiscoveryOwnPromotions).watch();
Stream<List<UserDiscoveryOtherPromotion>> watchAllOtherPromotions() =>
select(userDiscoveryOtherPromotions).watch();
Stream<List<UserDiscoveryShare>> watchAllShares() =>
select(userDiscoveryShares).watch();
}

View file

@ -1,55 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_discovery.dao.dart';
// ignore_for_file: type=lint
mixin _$UserDiscoveryDaoMixin on DatabaseAccessor<TwonlyDB> {
$UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers =>
attachedDatabase.userDiscoveryAnnouncedUsers;
$ContactsTable get contacts => attachedDatabase.contacts;
$UserDiscoveryUserRelationsTable get userDiscoveryUserRelations =>
attachedDatabase.userDiscoveryUserRelations;
$UserDiscoveryOwnPromotionsTable get userDiscoveryOwnPromotions =>
attachedDatabase.userDiscoveryOwnPromotions;
$UserDiscoveryOtherPromotionsTable get userDiscoveryOtherPromotions =>
attachedDatabase.userDiscoveryOtherPromotions;
$UserDiscoverySharesTable get userDiscoveryShares =>
attachedDatabase.userDiscoveryShares;
UserDiscoveryDaoManager get managers => UserDiscoveryDaoManager(this);
}
class UserDiscoveryDaoManager {
final _$UserDiscoveryDaoMixin _db;
UserDiscoveryDaoManager(this._db);
$$UserDiscoveryAnnouncedUsersTableTableManager
get userDiscoveryAnnouncedUsers =>
$$UserDiscoveryAnnouncedUsersTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryAnnouncedUsers,
);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$UserDiscoveryUserRelationsTableTableManager
get userDiscoveryUserRelations =>
$$UserDiscoveryUserRelationsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryUserRelations,
);
$$UserDiscoveryOwnPromotionsTableTableManager
get userDiscoveryOwnPromotions =>
$$UserDiscoveryOwnPromotionsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryOwnPromotions,
);
$$UserDiscoveryOtherPromotionsTableTableManager
get userDiscoveryOtherPromotions =>
$$UserDiscoveryOtherPromotionsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryOtherPromotions,
);
$$UserDiscoverySharesTableTableManager get userDiscoveryShares =>
$$UserDiscoverySharesTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryShares,
);
}

View file

@ -1,164 +0,0 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/utils/log.dart';
class DriftLoggingInterceptor extends QueryInterceptor {
bool get _isEnabled {
try {
if (!userService.isUserCreated) return false;
return userService.currentUser.enableDatabaseLogging;
} catch (_) {
return false;
}
}
List<String> _findUuids(dynamic value) {
if (value == null) return const [];
final uuids = <String>[];
final uuidRegex = RegExp(
'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',
);
if (value is String) {
for (final match in uuidRegex.allMatches(value)) {
uuids.add(match.group(0)!);
}
} else if (value is Iterable) {
for (final element in value) {
uuids.addAll(_findUuids(element));
}
} else if (value is Map) {
for (final element in value.values) {
uuids.addAll(_findUuids(element));
}
} else {
final str = value.toString();
for (final match in uuidRegex.allMatches(str)) {
uuids.add(match.group(0)!);
}
}
return uuids.toSet().toList();
}
Future<T> _run<T>(
String operation,
String statement,
List<Object?> args,
Future<T> Function() query,
) async {
if (!_isEnabled) {
return query();
}
final stopwatch = Stopwatch()..start();
try {
final result = await query();
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = _findUuids(args);
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement"',
);
}
return result;
} catch (e) {
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = _findUuids(args);
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement"',
);
}
rethrow;
}
}
@override
Future<int> runInsert(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('INSERT', statement, args, () => executor.runInsert(statement, args));
}
@override
Future<int> runUpdate(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('UPDATE', statement, args, () => executor.runUpdate(statement, args));
}
@override
Future<int> runDelete(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('DELETE', statement, args, () => executor.runDelete(statement, args));
}
@override
Future<void> runCustom(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('CUSTOM', statement, args, () => executor.runCustom(statement, args));
}
@override
Future<void> runBatched(
QueryExecutor executor,
BatchedStatements statements,
) async {
if (!_isEnabled) {
return executor.runBatched(statements);
}
final stopwatch = Stopwatch()..start();
try {
await executor.runBatched(statements);
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = <String>[];
for (final batchArg in statements.arguments) {
uuids.addAll(_findUuids(batchArg.arguments));
}
final statementsStr = statements.statements.join('; ');
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr"',
);
}
} catch (e) {
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = <String>[];
for (final batchArg in statements.arguments) {
uuids.addAll(_findUuids(batchArg.arguments));
}
final statementsStr = statements.statements.join('; ');
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr"',
);
}
rethrow;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
class SignalIdentityKeyStore extends IdentityKeyStore {
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
class ConnectIdentityKeyStore extends IdentityKeyStore {
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
final IdentityKeyPair identityKeyPair;
final int localRegistrationId;

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
class SignalPreKeyStore extends PreKeyStore {
class ConnectPreKeyStore extends PreKeyStore {
@override
Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
class SignalSenderKeyStore extends SenderKeyStore {
class ConnectSenderKeyStore extends SenderKeyStore {
@override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity =

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
class SignalSessionStore extends SessionStore {
class ConnectSessionStore extends SessionStore {
@override
Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions =

View file

@ -1,23 +1,23 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/signal_identity_key_store.dart';
import 'package:twonly/src/database/signal/signal_pre_key_store.dart';
import 'package:twonly/src/database/signal/signal_session_store.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
import 'package:twonly/src/database/signal/connect_identity_key_store.dart';
import 'package:twonly/src/database/signal/connect_pre_key_store.dart';
import 'package:twonly/src/database/signal/connect_session_store.dart';
import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart';
class SignalSignalProtocolStore implements SignalProtocolStore {
SignalSignalProtocolStore(
class ConnectSignalProtocolStore implements SignalProtocolStore {
ConnectSignalProtocolStore(
IdentityKeyPair identityKeyPair,
int registrationId,
) {
_identityKeyStore = SignalIdentityKeyStore(
_identityKeyStore = ConnectIdentityKeyStore(
identityKeyPair,
registrationId,
);
}
final preKeyStore = SignalPreKeyStore();
final sessionStore = SignalSessionStore();
final signedPreKeyStore = SignalSignedPreKeyStore();
final preKeyStore = ConnectPreKeyStore();
final sessionStore = ConnectSessionStore();
final signedPreKeyStore = ConnectSignedPreKeyStore();
late IdentityKeyStore _identityKeyStore;

View file

@ -0,0 +1,80 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
class ConnectSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async {
const storage = FlutterSecureStorage();
final storeSerialized = await storage.read(
key: SecureStorageKeys.signalSignedPreKey,
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized) as List<dynamic>;
for (final item in storeHashMap) {
// ignore: avoid_dynamic_calls
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
Future<void> safeStore(HashMap<int, Uint8List> store) async {
const storage = FlutterSecureStorage();
final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]);
}
final storeSerialized = json.encode(storeHashMap);
await storage.write(
key: SecureStorageKeys.signalSignedPreKey,
value: storeSerialized,
);
}
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
if (!store.containsKey(signedPreKeyId)) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final store = await getStore();
final results = <SignedPreKeyRecord>[];
for (final serialized in store.values) {
results.add(SignedPreKeyRecord.fromSerialized(serialized));
}
return results;
}
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final store = await getStore();
store[signedPreKeyId] = record.serialize();
await safeStore(store);
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
(await getStore()).containsKey(signedPreKeyId);
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
store.remove(signedPreKeyId);
await safeStore(store);
}
}

View file

@ -1,83 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read(
key: 'signed_pre_key_store',
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized) as List<dynamic>;
for (final item in storeHashMap) {
// ignore: avoid_dynamic_calls
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
class SignalSignedPreKeyStore extends SignedPreKeyStore {
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final record = await (twonlyDB.select(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
if (record.isEmpty) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(record.first.signedPreKey);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final records = await twonlyDB
.select(twonlyDB.signalSignedPreKeyStores)
.get();
return records
.map((r) => SignedPreKeyRecord.fromSerialized(r.signedPreKey))
.toList();
}
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(signedPreKeyId),
signedPreKey: Value(record.serialize()),
);
try {
await twonlyDB
.into(twonlyDB.signalSignedPreKeyStores)
.insert(companion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
}
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async {
final record = await (twonlyDB.select(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
return record.isNotEmpty;
}
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
await (twonlyDB.delete(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).go();
}
}

View file

@ -1,6 +1,5 @@
import 'package:drift/drift.dart';
@DataClassName('Contact')
class Contacts extends Table {
IntColumn get userId => integer()();
@ -23,46 +22,6 @@ class Contacts extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
// contact_versions: HashMap<UserID, Vec<u8>>,
BlobColumn get userDiscoveryVersion => blob().nullable()();
BoolColumn get userDiscoveryExcluded =>
boolean().withDefault(const Constant(false))();
BoolColumn get userDiscoveryManualApproved =>
boolean().nullable().withDefault(const Constant(false))();
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
IntColumn get mediaReceivedCounter =>
integer().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {userId};
}
enum VerificationType {
migratedFromOldVersion,
qrScanned,
link,
secretQrToken,
contactSharedByVerified,
}
@DataClassName('KeyVerification')
class KeyVerifications extends Table {
IntColumn get verificationId => integer().autoIncrement()();
IntColumn get contactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
TextColumn get type => textEnum<VerificationType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
@DataClassName('VerificationToken')
class VerificationTokens extends Table {
IntColumn get tokenId => integer().autoIncrement()();
BlobColumn get token => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View file

@ -83,8 +83,6 @@ enum GroupActionType {
demoteToMember,
updatedGroupName,
changeDisplayMaxTime,
updatedContactUsername,
updatedContactDisplayName,
}
@DataClassName('GroupHistory')

View file

@ -50,9 +50,6 @@ class MediaFiles extends Table {
BoolColumn get stored => boolean().withDefault(const Constant(false))();
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
BoolColumn get hasCropAnalyzed =>
boolean().withDefault(const Constant(false))();
IntColumn get preProgressingProcess => integer().nullable()();
@ -69,14 +66,7 @@ class MediaFiles extends Table {
BlobColumn get storedFileHash => blob().nullable()();
BoolColumn get hasThumbnail =>
boolean().withDefault(const Constant(false))();
IntColumn get sizeInBytes => integer().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get createdAtMonth => text().nullable()();
@override
Set<Column> get primaryKey => {mediaId};

View file

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

View file

@ -1,26 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
@DataClassName('Shortcut')
class Shortcuts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get emoji => text().unique()();
IntColumn get usageCounter => integer().withDefault(const Constant(0))();
}
@DataClassName('ShortcutMember')
class ShortcutMembers extends Table {
IntColumn get shortcutId => integer().references(
Shortcuts,
#id,
onDelete: KeyAction.cascade,
)();
TextColumn get groupId => text().references(
Groups,
#groupId,
onDelete: KeyAction.cascade,
)();
@override
Set<Column> get primaryKey => {shortcutId, groupId};
}

View file

@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSignedPreKeyStore')
class SignalSignedPreKeyStores extends Table {
IntColumn get signedPreKeyId => integer()();
BlobColumn get signedPreKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {signedPreKeyId};
}

View file

@ -1,89 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
// config: Option<Vec<u8>>,
// announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
@DataClassName('UserDiscoveryAnnouncedUser')
class UserDiscoveryAnnouncedUsers extends Table {
IntColumn get announcedUserId => integer()();
BlobColumn get announcedPublicKey => blob()();
IntColumn get publicId => integer().unique()();
// When a new user got announced this data will be requested without adding the users to the contacts...
TextColumn get username => text().nullable()();
BoolColumn get wasShownToTheUser =>
boolean().withDefault(const Constant(false))();
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
BoolColumn get wasAskedFriends =>
boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {announcedUserId};
}
// announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
@DataClassName('UserDiscoveryUserRelation')
class UserDiscoveryUserRelations extends Table {
IntColumn get announcedUserId => integer().references(
UserDiscoveryAnnouncedUsers,
#announcedUserId,
onDelete: KeyAction.cascade,
)();
IntColumn get fromContactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
DateTimeColumn get publicKeyVerifiedTimestamp => dateTime().nullable()();
@override
Set<Column> get primaryKey => {announcedUserId, fromContactId};
}
// own_promotions: Vec<(UserID, Vec<u8>)>,
@DataClassName('UserDiscoveryOwnPromotion')
class UserDiscoveryOwnPromotions extends Table {
IntColumn get versionId => integer().autoIncrement()();
IntColumn get contactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
BlobColumn get promotion => blob()();
}
// other_promotions: Vec<OtherPromotion>,
@DataClassName('UserDiscoveryOtherPromotion')
class UserDiscoveryOtherPromotions extends Table {
IntColumn get fromContactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
IntColumn get promotionId => integer()();
IntColumn get publicId => integer()();
IntColumn get threshold => integer()();
BlobColumn get announcementShare => blob()();
DateTimeColumn get publicKeyVerifiedTimestamp => dateTime().nullable()();
@override
Set<Column> get primaryKey => {fromContactId, publicId};
}
// unused_shares: Vec<Vec<u8>>,
// used_shares: HashMap<UserID, Vec<u8>>,
@DataClassName('UserDiscoveryShare')
class UserDiscoveryShares extends Table {
IntColumn get shareId => integer().autoIncrement()();
BlobColumn get share => blob()();
IntColumn get contactId => integer().nullable().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
}

View file

@ -1,31 +1,24 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/drift_logging_interceptor.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
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/shortcuts.table.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
import 'package:twonly/src/database/tables/signal_signed_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.steps.dart';
import 'package:twonly/src/utils/log.dart';
@ -47,18 +40,8 @@ part 'twonly.db.g.dart';
SignalPreKeyStores,
SignalSenderKeyStores,
SignalSessionStores,
SignalSignedPreKeyStores,
MessageActions,
GroupHistories,
KeyVerifications,
VerificationTokens,
UserDiscoveryAnnouncedUsers,
UserDiscoveryUserRelations,
UserDiscoveryOtherPromotions,
UserDiscoveryOwnPromotions,
UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
],
daos: [
MessagesDao,
@ -67,9 +50,6 @@ part 'twonly.db.g.dart';
GroupsDao,
ReactionsDao,
MediaFilesDao,
UserDiscoveryDao,
KeyVerificationDao,
ShortcutsDao,
],
)
class TwonlyDB extends _$TwonlyDB {
@ -82,29 +62,16 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 17;
int get schemaVersion => 11;
static QueryExecutor _openConnection() {
final connection = driftDatabase(
return driftDatabase(
name: 'twonly',
native: DriftNativeOptions(
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
shareAcrossIsolates: true,
setup: (rawDb) {
rawDb
..execute('PRAGMA journal_mode=DELETE;')
..execute('PRAGMA synchronous=FULL;')
..execute('PRAGMA busy_timeout=5000;');
},
),
);
try {
if (userService.isUserCreated &&
userService.currentUser.enableDatabaseLogging) {
return connection.interceptWith(DriftLoggingInterceptor());
}
} catch (_) {}
return connection;
}
@override
@ -126,6 +93,7 @@ class TwonlyDB extends _$TwonlyDB {
},
from3To4: (m, schema) async {
await m.alterTable(
// ignore: experimental_member_use
TableMigration(
schema.groupHistories,
columnTransformer: {
@ -158,7 +126,9 @@ class TwonlyDB extends _$TwonlyDB {
await m.deleteTable('signal_contact_pre_keys');
await m.deleteTable('signal_contact_signed_pre_keys');
// For message_actions
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageHistories));
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageActions));
},
from8To9: (m, schema) async {
@ -183,56 +153,6 @@ class TwonlyDB extends _$TwonlyDB {
schema.groupMembers.lastTypeIndicator,
);
},
from11To12: (m, schema) async {
await m.createTable(schema.verificationTokens);
await m.createTable(schema.keyVerifications);
await m.createTable(schema.userDiscoveryAnnouncedUsers);
await m.createTable(schema.userDiscoveryOwnPromotions);
await m.createTable(schema.userDiscoveryOtherPromotions);
await m.createTable(schema.userDiscoveryShares);
await m.createTable(schema.userDiscoveryUserRelations);
final columns = [
schema.contacts.userDiscoveryVersion,
schema.contacts.mediaReceivedCounter,
schema.contacts.mediaSendCounter,
schema.contacts.userDiscoveryExcluded,
schema.contacts.userDiscoveryManualApproved,
];
for (final column in columns) {
await m.addColumn(schema.contacts, column);
}
},
from12To13: (m, schema) async {
await m.createTable(schema.shortcuts);
await m.createTable(schema.shortcutMembers);
},
from13To14: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.createdAtMonth,
);
await m.addColumn(schema.mediaFiles, schema.mediaFiles.isFavorite);
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.hasCropAnalyzed,
);
},
from14To15: (m, schema) async {
await m.createTable(schema.signalSignedPreKeyStores);
},
from15To16: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.hasThumbnail,
);
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
},
from16To17: (m, schema) async {
await m.addColumn(
schema.userDiscoveryAnnouncedUsers,
schema.userDiscoveryAnnouncedUsers.wasAskedFriends,
);
},
)(m, from, to);
},
);
@ -254,4 +174,38 @@ class TwonlyDB extends _$TwonlyDB {
Log.info('Table: $tableName, Size: $tableSize bytes');
}
}
Future<void> deleteDataForTwonlySafe() async {
await (delete(messages)..where(
(t) =>
(t.mediaStored.equals(false) &
t.isDeletedFromSender.equals(false)),
))
.go();
await update(messages).write(
const MessagesCompanion(
downloadToken: Value(null),
),
);
await (delete(mediaFiles)..where(
(t) => (t.stored.equals(false)),
))
.go();
await delete(receipts).go();
await delete(receivedReceipts).go();
await update(contacts).write(
const ContactsCompanion(
avatarSvgCompressed: Value(null),
senderProfileCounter: Value(0),
),
);
await (delete(signalPreKeyStores)..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 25),
),
)),
))
.go();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

@ -1 +1 @@
Subproject commit 189bf8f4dbe2bee4f19a15b9640b8826e4f2e235
Subproject commit 57ec512977e514fca6413622bb4a7e03701f09a0

View file

@ -1,51 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'backup.model.g.dart';
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class CurrentBackupStatus {
CurrentBackupStatus();
factory CurrentBackupStatus.fromJson(Map<String, dynamic> json) =>
_$CurrentBackupStatusFromJson(json);
LastBackupUploadState identityState = LastBackupUploadState.none;
DateTime? identityLastSuccessFull;
int? identitySize;
LastBackupUploadState archiveState = LastBackupUploadState.none;
DateTime? archiveLastSuccessFull;
int? archiveSize;
Map<String, dynamic> toJson() => _$CurrentBackupStatusToJson(this);
}
enum BackupRecoveryState {
// The userId was loaded from the server and the user is asked to enter his password.
identityBackupStarted,
// -> Download identity, replace keymanager
// Identity was downloaded and Keymanager was updated
archiveBackupStarted,
// -> Download archive, replace files, restart app
}
@JsonSerializable()
class BackupRecovery {
BackupRecovery({
required this.username,
required this.password,
required this.userId,
});
factory BackupRecovery.fromJson(Map<String, dynamic> json) =>
_$BackupRecoveryFromJson(json);
String username;
String password;
int userId;
BackupRecoveryState state = BackupRecoveryState.identityBackupStarted;
Map<String, dynamic> toJson() => _$BackupRecoveryToJson(this);
}

View file

@ -1,65 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CurrentBackupStatus _$CurrentBackupStatusFromJson(Map<String, dynamic> json) =>
CurrentBackupStatus()
..identityState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['identityState'],
)
..identityLastSuccessFull = json['identityLastSuccessFull'] == null
? null
: DateTime.parse(json['identityLastSuccessFull'] as String)
..identitySize = (json['identitySize'] as num?)?.toInt()
..archiveState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['archiveState'],
)
..archiveLastSuccessFull = json['archiveLastSuccessFull'] == null
? null
: DateTime.parse(json['archiveLastSuccessFull'] as String)
..archiveSize = (json['archiveSize'] as num?)?.toInt();
Map<String, dynamic> _$CurrentBackupStatusToJson(
CurrentBackupStatus instance,
) => <String, dynamic>{
'identityState': _$LastBackupUploadStateEnumMap[instance.identityState]!,
'identityLastSuccessFull': instance.identityLastSuccessFull
?.toIso8601String(),
'identitySize': instance.identitySize,
'archiveState': _$LastBackupUploadStateEnumMap[instance.archiveState]!,
'archiveLastSuccessFull': instance.archiveLastSuccessFull?.toIso8601String(),
'archiveSize': instance.archiveSize,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupRecovery _$BackupRecoveryFromJson(Map<String, dynamic> json) =>
BackupRecovery(
username: json['username'] as String,
password: json['password'] as String,
userId: (json['userId'] as num).toInt(),
)..state = $enumDecode(_$BackupRecoveryStateEnumMap, json['state']);
Map<String, dynamic> _$BackupRecoveryToJson(BackupRecovery instance) =>
<String, dynamic>{
'username': instance.username,
'password': instance.password,
'userId': instance.userId,
'state': _$BackupRecoveryStateEnumMap[instance.state]!,
};
const _$BackupRecoveryStateEnumMap = {
BackupRecoveryState.identityBackupStarted: 'identityBackupStarted',
BackupRecoveryState.archiveBackupStarted: 'archiveBackupStarted',
};

View file

@ -1,89 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'faq.model.g.dart';
@JsonSerializable()
class FaqData {
const FaqData({required this.languages});
factory FaqData.fromJson(Map<String, dynamic> json) {
return FaqData(
languages: json.map(
(key, value) => MapEntry(
key,
(value as Map<String, dynamic>).map(
(catKey, catValue) => MapEntry(
catKey,
FaqCategory.fromJson(catValue as Map<String, dynamic>),
),
),
),
),
);
}
final Map<String, Map<String, FaqCategory>> languages;
Map<String, dynamic> toJson() => languages.map(
(key, value) => MapEntry(
key,
value.map((catKey, catValue) => MapEntry(catKey, catValue.toJson())),
),
);
}
@JsonSerializable()
class FaqCategory {
const FaqCategory({
required this.meta,
required this.questions,
});
factory FaqCategory.fromJson(Map<String, dynamic> json) =>
_$FaqCategoryFromJson(json);
final FaqMeta meta;
final List<FaqQuestion> questions;
Map<String, dynamic> toJson() => _$FaqCategoryToJson(this);
}
@JsonSerializable()
class FaqMeta {
const FaqMeta({
required this.title,
required this.desc,
this.priority = 0,
});
factory FaqMeta.fromJson(Map<String, dynamic> json) =>
_$FaqMetaFromJson(json);
final String title;
final String desc;
@JsonKey(defaultValue: 0)
final int priority;
Map<String, dynamic> toJson() => _$FaqMetaToJson(this);
}
@JsonSerializable()
class FaqQuestion {
const FaqQuestion({
required this.id,
required this.title,
required this.body,
required this.path,
});
factory FaqQuestion.fromJson(Map<String, dynamic> json) =>
_$FaqQuestionFromJson(json);
final String id;
final String title;
final String body;
final String path;
Map<String, dynamic> toJson() => _$FaqQuestionToJson(this);
}

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