mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-02 22:52:12 +00:00
Compare commits
219 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6501590dd7 | |||
| d03c42659c | |||
| 849f748968 | |||
| 9e28bb82a2 | |||
| 9beb8ef9d7 | |||
| a688954d76 | |||
| 358f93979e | |||
| dc0ef25d73 | |||
| 7559434f86 | |||
| 62457f1f48 | |||
| 0602f043d2 | |||
| 0c32b41dd0 | |||
| c10dc19342 | |||
| 872592af21 | |||
| c7826ad6dd | |||
| 25c826bff3 | |||
| 789bcda34f | |||
| 874cf5fecc | |||
| 34607e05d1 | |||
| 3499a08155 | |||
| a50c2ba7d7 | |||
| fae5ca3d25 | |||
| d6432677df | |||
| 00cb615e56 | |||
| 1ad304ec2e | |||
| cd5409d021 | |||
| b7c4832ee2 | |||
| f42a49cadf | |||
| 2d6a2e436f | |||
| c0e45cfe1f | |||
| d9da953f77 | |||
| b788146beb | |||
| 6f8f1efe81 | |||
| 304190387d | |||
| 65d188c4f2 | |||
| d32e319c49 | |||
| 2cb51d668a | |||
| 927589a505 | |||
| fc5c74eaed | |||
| d7e4da0e55 | |||
| bfde01cbc5 | |||
| 7614da00b1 | |||
| dec79f3463 | |||
| 236d94622c | |||
| 5bcb3b3efe | |||
| c77c369212 | |||
| 5fb51b20d7 | |||
| df974cd9f7 | |||
| 0204a41d43 | |||
| 11c0ad908e | |||
| 7283852ba5 | |||
| 805d7a66b3 | |||
| ea41158872 | |||
| 32231d11c2 | |||
| 68c99c271f | |||
| 91eedc76b0 | |||
| d0eee1893e | |||
| fe2dd06213 | |||
| 102d2579ce | |||
| 190be5b694 | |||
| c2ac706239 | |||
| e9b550023f | |||
| 5556532879 | |||
| f3b64646f5 | |||
| 0a9c74f515 | |||
| 07bc47062c | |||
| 8c84d802fd | |||
| ebc643cbe4 | |||
| d52f1eefea | |||
| 78cdb9244c | |||
| 4b9180c3c7 | |||
| ed0b7160b9 | |||
| 697e9a99f8 | |||
| a5fc97c648 | |||
| 5ec8afc01f | |||
| 7be9acb379 | |||
| fe0bb01f49 | |||
| 23004acbed | |||
| f7211fed08 | |||
| 9bb2ea2825 | |||
| d5642896a8 | |||
| 93ee6e60dd | |||
| dda3677907 | |||
| 0818fd0a75 | |||
| a1ca45c2b9 | |||
| 3d1b38192e | |||
| f45638c58d | |||
| 09129639e1 | |||
| f2b27e19f2 | |||
| ba06126a3c | |||
| 9941c6e870 | |||
| 1e6ce639cf | |||
| d7dffa82ff | |||
| 0a91e34348 | |||
| e6b549e897 | |||
| 4d39eb0bf4 | |||
| 7634177191 | |||
| 61979aedcb | |||
| 4dbc369003 | |||
| f735070a7c | |||
| 3e49e293f4 | |||
| 4d9c356400 | |||
| 105129023a | |||
| 64b304d99e | |||
| 5fa253ec32 | |||
| f323bc03eb | |||
| e6a468c065 | |||
| 149478df11 | |||
| d976737942 | |||
| d86252d800 | |||
| 8c15a95165 | |||
| dc044ee0d2 | |||
| 8898395d72 | |||
| d8a3c4a4d7 | |||
| 52bc628752 | |||
| 28fffbfce5 | |||
| 3acd207de6 | |||
| 0a972d023f | |||
| cdababa3c8 | |||
| c9eb270324 | |||
| 6a611767fc | |||
| 7f7aba8e08 | |||
| f553713ff8 | |||
| 281014133a | |||
| 171a3d7f5e | |||
| 2d7b516e54 | |||
| b00fdd0938 | |||
| c85d862726 | |||
| ddc7c00c7d | |||
| 42a676491c | |||
| 8f7346dfba | |||
| 6e95b977ac | |||
| 349794dbaa | |||
| 72e91f7492 | |||
| 51477e3f51 | |||
| b2e9b04659 | |||
| 9289def783 | |||
| a015cb2cb8 | |||
| c9b8e32d32 | |||
| 836e58ec3a | |||
| c54265495c | |||
| f0741bfdc1 | |||
| 41dc30b3c2 | |||
| c47c91c1ba | |||
| 8021768883 | |||
| a93187c86d | |||
| 5b5140ec7c | |||
| c6d13a44e9 | |||
| 922a5f0f26 | |||
| c9aa680243 | |||
| ce60f4e2f1 | |||
| dcca2cbec0 | |||
| a7d64a2307 | |||
| ef2a32157e | |||
| 1e72883db0 | |||
| 583368505d | |||
| db9d9022fd | |||
| 646b9c22d3 | |||
| 3c91f99008 | |||
| e8d8e8b160 | |||
| 919aec464e | |||
| eed5d292c6 | |||
| a29af4c914 | |||
| f8649298e0 | |||
| dde339d1b3 | |||
| 0c8bd0a7b4 | |||
| 1cee77cd97 | |||
| 5722cb71bb | |||
| fcb93830e1 | |||
| 1371cf80cb | |||
| be35336a5d | |||
| c197cb797e | |||
| c9a704c44f | |||
| 81370d27a9 | |||
| 4a8fbdce28 | |||
| 50679ce9ed | |||
| 95c5d6a4f1 | |||
| 7d09bd7283 | |||
| e1f28e1b87 | |||
| 954eedd40e | |||
| 5d8133a92f | |||
| 1cf4239149 | |||
| bcb2403059 | |||
| 94982ca253 | |||
| 1216ec252d | |||
| 1ea97d58ea | |||
| 1c902bb64d | |||
| ba2f9644c0 | |||
| fe360cb2bc | |||
| 56aa6e9f7e | |||
| 3d35615136 | |||
| e945e30991 | |||
| 715774bd7f | |||
| bd012a363e | |||
| d9f9f7645e | |||
| 693c74df46 | |||
| 93d5f682fc | |||
| 66c6ee09ee | |||
| a7f1457d72 | |||
| 16cc30393c | |||
| d5449800d7 | |||
| 676a1c28f8 | |||
| edf9e24f8a | |||
| f2493a2b56 | |||
| 6517473603 | |||
| 1629b8b1a9 | |||
| cd2a254d23 | |||
| 2ec8d14439 | |||
| e1956c9807 | |||
| 6516c4564c | |||
| 4ffd367b23 | |||
| fce85c58f9 | |||
| eb22acacee | |||
| 252e7653db | |||
| fc73e313ea | |||
| 51f51f768b | |||
| 87ba1c23e5 | |||
| 8aacdc5235 | |||
| 757094bc23 |
615 changed files with 144639 additions and 19126 deletions
4
.github/workflows/dev_github.yml
vendored
4
.github/workflows/dev_github.yml
vendored
|
|
@ -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
62
.github/workflows/release_github.yml
vendored
|
|
@ -1,62 +0,0 @@
|
|||
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
9
.gitignore
vendored
|
|
@ -10,8 +10,14 @@
|
|||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
migrate_working_dir/
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
|
|
@ -47,3 +53,6 @@ app.*.map.json
|
|||
/android/app/.cxx/
|
||||
android/.kotlin/
|
||||
devtools_options.yaml
|
||||
rust/target
|
||||
rust_dependencies/target
|
||||
fastlane/repo/status/running.json
|
||||
|
|
|
|||
83
CHANGELOG.md
83
CHANGELOG.md
|
|
@ -1,5 +1,88 @@
|
|||
# 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
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -1,10 +1,16 @@
|
|||
# twonly
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
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.
|
||||
|
||||
<div style="margin: 10px 20px 10px 20px">
|
||||
<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">
|
||||
<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" />
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ 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
2
android/.gitignore
vendored
|
|
@ -9,5 +9,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -73,4 +73,5 @@ 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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,38 @@ 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) {
|
||||
|
|
@ -24,7 +54,38 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MediaStoreChannel.configure(flutterEngine, applicationContext)
|
||||
Keyring.initializeNdkContext(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@ 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())
|
||||
}
|
||||
|
|
|
|||
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal file
14
android/app/src/main/kotlin/io/crates/keyring/Keyring.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
69
android/app/src/main/res/drawable/link_animated.xml
Normal file
69
android/app/src/main/res/drawable/link_animated.xml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<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>
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
<?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="@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 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>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
<?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="@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 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>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
|
|||
8
android/key.github.properties.example
Normal file
8
android/key.github.properties.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# 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
|
||||
1
assets/animated_icons/distorted_face.json
Normal file
1
assets/animated_icons/distorted_face.json
Normal file
File diff suppressed because one or more lines are too long
1
assets/animated_icons/hourglass.json
Normal file
1
assets/animated_icons/hourglass.json
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/fonts/NotoColorEmoji.ttf
Normal file
BIN
assets/fonts/NotoColorEmoji.ttf
Normal file
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 732 B |
|
|
@ -0,0 +1,3 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<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>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -1 +1 @@
|
|||
Subproject commit 24d048b4abbe5c266b09965cc6f3ebdf83f97855
|
||||
Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe
|
||||
BIN
docs/header.webp
BIN
docs/header.webp
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
2
fastlane/Appfile
Normal file
2
fastlane/Appfile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
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
|
||||
144
fastlane/Fastfile
Normal file
144
fastlane/Fastfile
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
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
|
||||
3
flutter_rust_bridge.yaml
Normal file
3
flutter_rust_bridge.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
rust_input: crate::bridge
|
||||
rust_root: rust
|
||||
dart_output: lib/core
|
||||
31
integration_test/api_service.test.dart
Normal file
31
integration_test/api_service.test.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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}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")
|
||||
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")
|
||||
}
|
||||
|
||||
extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = "EncryptedPushNotification"
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}keyId\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
|
|
@ -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{1}messageId\0\u{1}additionalContent\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
|
|
@ -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{1}userId\0\u{1}displayName\0\u{1}blocked\0\u{1}lastMessageId\0\u{1}pushKeys\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
|
|
@ -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{1}createdAtUnixTimestamp\0")
|
||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0")
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,8 @@ 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)
|
||||
|
|
@ -274,17 +276,24 @@ 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)
|
||||
|
|
@ -304,31 +313,6 @@ 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):
|
||||
|
|
@ -370,17 +354,19 @@ 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`)
|
||||
|
|
@ -412,10 +398,10 @@ SPEC REPOS:
|
|||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- ScreenProtectorKit
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
- sqlite3
|
||||
- SwiftProtobuf
|
||||
- SwiftyGif
|
||||
|
||||
|
|
@ -470,18 +456,24 @@ 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:
|
||||
|
|
@ -490,8 +482,6 @@ 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:
|
||||
|
|
@ -540,6 +530,7 @@ SPEC CHECKSUMS:
|
|||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
|
|
@ -549,12 +540,15 @@ 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
|
||||
|
|
@ -562,8 +556,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ 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)
|
||||
|
|
@ -32,16 +36,18 @@ 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)
|
||||
|
|
@ -54,7 +60,8 @@ 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(
|
||||
|
|
|
|||
|
|
@ -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="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" red="0.341176" green="0.8" blue="0.6" 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"/>
|
||||
|
|
|
|||
192
lib/app.dart
192
lib/app.dart
|
|
@ -1,118 +1,121 @@
|
|||
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/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';
|
||||
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';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
const App({
|
||||
required this.storageError,
|
||||
required this.recoveryPossible,
|
||||
super.key,
|
||||
});
|
||||
final bool storageError;
|
||||
final bool recoveryPossible;
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
bool wasPaused = false;
|
||||
bool _wasPaused = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
globalIsAppInBackground = false;
|
||||
AppState.isAppInBackground = 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) {
|
||||
globalIsAppInBackground = false;
|
||||
if (_wasPaused) {
|
||||
AppState.isAppInBackground = false;
|
||||
twonlyDB.markUpdated();
|
||||
unawaited(apiService.connect());
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
wasPaused = true;
|
||||
globalIsAppInBackground = true;
|
||||
_wasPaused = true;
|
||||
AppState.isAppInBackground = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
globalCallbackConnectionState = ({required isConnected}) {};
|
||||
globalCallbackUpdatePlan = (planId) {};
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: context.watch<SettingsChangeProvider>(),
|
||||
listenable: context.read<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,
|
||||
scaffoldMessengerKey: globalRootScaffoldMessengerKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
supportedLocales: const [
|
||||
Locale('en', ''),
|
||||
Locale('de', ''),
|
||||
],
|
||||
supportedLocales: supportedLocales,
|
||||
title: 'twonly',
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: context.watch<SettingsChangeProvider>().themeMode,
|
||||
themeMode: context.read<SettingsChangeProvider>().themeMode,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -130,41 +133,46 @@ 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() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
_initialPage = widget.initialPage;
|
||||
Log.info('AppWidgetState: initState started');
|
||||
initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
_isUserCreated = await isUserCreated();
|
||||
|
||||
if (_isUserCreated) {
|
||||
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);
|
||||
}
|
||||
if (_isTwonlyLocked) {
|
||||
// do not change in case twonly was already unlocked at some point
|
||||
_isTwonlyLocked = gUser.screenLockEnabled;
|
||||
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
|
||||
}
|
||||
if (gUser.appVersion < 62) {
|
||||
_showDatabaseMigration = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isUserCreated && !_showDatabaseMigration) {
|
||||
} else {
|
||||
// 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,
|
||||
|
|
@ -181,31 +189,35 @@ 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 (_showDatabaseMigration) {
|
||||
child = const Center(child: Text('Please reinstall twonly.'));
|
||||
} else if (_isUserCreated) {
|
||||
if (userService.isUserCreated) {
|
||||
if (_isTwonlyLocked) {
|
||||
child = UnlockTwonlyView(
|
||||
callbackOnSuccess: () => setState(() {
|
||||
_isTwonlyLocked = false;
|
||||
}),
|
||||
);
|
||||
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
|
||||
child = SetupBackupView(
|
||||
callBack: () {
|
||||
_skipBackup = true;
|
||||
setState(() {});
|
||||
},
|
||||
} 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 {
|
||||
child = HomeView(
|
||||
initialPage: widget.initialPage,
|
||||
initialPage: _initialPage,
|
||||
);
|
||||
}
|
||||
} else if (_showOnboarding) {
|
||||
|
|
@ -224,7 +236,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
const AppOutdated(),
|
||||
const AppOutdatedComp(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
29
lib/core/backup/backup_password.dart
Normal file
29
lib/core/backup/backup_password.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// 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;
|
||||
}
|
||||
97
lib/core/bridge.dart
Normal file
97
lib/core/bridge.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// 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;
|
||||
}
|
||||
61
lib/core/bridge/callbacks.dart
Normal file
61
lib/core/bridge/callbacks.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// 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,
|
||||
);
|
||||
87
lib/core/bridge/wrapper/backup.dart
Normal file
87
lib/core/bridge/wrapper/backup.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// 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;
|
||||
}
|
||||
77
lib/core/bridge/wrapper/key_manager.dart
Normal file
77
lib/core/bridge/wrapper/key_manager.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// 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;
|
||||
}
|
||||
73
lib/core/bridge/wrapper/user_discovery.dart
Normal file
73
lib/core/bridge/wrapper/user_discovery.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// 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;
|
||||
}
|
||||
28
lib/core/context.dart
Normal file
28
lib/core/context.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// 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;
|
||||
}
|
||||
3104
lib/core/frb_generated.dart
Normal file
3104
lib/core/frb_generated.dart
Normal file
File diff suppressed because it is too large
Load diff
669
lib/core/frb_generated.io.dart
Normal file
669
lib/core/frb_generated.io.dart
Normal file
|
|
@ -0,0 +1,669 @@
|
|||
// 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;
|
||||
}
|
||||
669
lib/core/frb_generated.web.dart
Normal file
669
lib/core/frb_generated.web.dart
Normal file
|
|
@ -0,0 +1,669 @@
|
|||
// 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 {}
|
||||
29
lib/core/keys/backup_password_keys.dart
Normal file
29
lib/core/keys/backup_password_keys.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// 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;
|
||||
}
|
||||
20
lib/core/lib.dart
Normal file
20
lib/core/lib.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// 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));
|
||||
}
|
||||
|
|
@ -1,43 +1,37 @@
|
|||
import 'dart:async';
|
||||
import 'package:camera/camera.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';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
late ApiService apiService;
|
||||
class AppEnvironment {
|
||||
static late String cacheDir;
|
||||
static late String supportDir;
|
||||
|
||||
// uses for background notification
|
||||
late TwonlyDB twonlyDB;
|
||||
static bool _isInitialized = false;
|
||||
|
||||
List<CameraDescription> gCameras = <CameraDescription>[];
|
||||
// will be loaded in the main_camera_controller.dart
|
||||
static List<CameraDescription> cameras = [];
|
||||
|
||||
// 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 Future<void> init() async {
|
||||
if (_isInitialized) return;
|
||||
cacheDir = (await getApplicationCacheDirectory()).path;
|
||||
supportDir = (await getApplicationSupportDirectory()).path;
|
||||
Log.init();
|
||||
_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.
|
||||
static void initTesting({String? customCacheDir, String? customSupportDir}) {
|
||||
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
|
||||
supportDir = customSupportDir ?? '/tmp/twonly_support';
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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>();
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
17
lib/locator.dart
Normal file
17
lib/locator.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
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>();
|
||||
197
lib/main.dart
197
lib/main.dart
|
|
@ -1,59 +1,116 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:mutex/mutex.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/src/database/twonly.db.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/callbacks/callbacks.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.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/api/mediafiles/media_background.api.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/backup/create.backup.dart';
|
||||
import 'package:twonly/src/services/backup.service.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/storage.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;
|
||||
}
|
||||
|
||||
void main() async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
final binding = SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
await AppEnvironment.init();
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
unawaited(StartupGuard.markAppStartup());
|
||||
|
||||
initLogger();
|
||||
var storageError = await twonlyMinimumInitialization();
|
||||
await initFCMService();
|
||||
|
||||
var user = await getUser();
|
||||
var userExists = false;
|
||||
|
||||
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();
|
||||
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 (user != null) {
|
||||
gUser = user;
|
||||
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.allowErrorTrackingViaSentry) {
|
||||
globalAllowErrorTrackingViaSentry = true;
|
||||
Log.info('User loaded.');
|
||||
|
||||
final settingsController = SettingsChangeProvider()..loadSettings();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
unawaited(initFileDownloader());
|
||||
|
||||
if (userExists) {
|
||||
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
||||
AppState.allowErrorTrackingViaSentry = true;
|
||||
await SentryFlutter.init(
|
||||
(options) => options
|
||||
..dsn =
|
||||
|
|
@ -63,52 +120,23 @@ void main() async {
|
|||
);
|
||||
}
|
||||
|
||||
unawaited(performTwonlySafeBackup());
|
||||
unawaited(initializeBackgroundTaskManager());
|
||||
} else {
|
||||
Log.info('User is not yet register. Ensure all local data is removed.');
|
||||
await deleteLocalUserData();
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
final settingsController = SettingsChangeProvider();
|
||||
await apiService.listenToNetworkChanges();
|
||||
|
||||
await settingsController.loadSettings();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
stopwatch.stop();
|
||||
|
||||
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());
|
||||
Log.info(
|
||||
'Initialization finished after ${stopwatch.elapsed}. Calling runApp...',
|
||||
);
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
|
@ -118,7 +146,34 @@ void main() async {
|
|||
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
|
||||
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
|
||||
],
|
||||
child: const App(),
|
||||
child: App(
|
||||
storageError: storageError,
|
||||
recoveryPossible: recoveryPossible,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
31
lib/src/callbacks/callbacks.dart
Normal file
31
lib/src/callbacks/callbacks.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
33
lib/src/callbacks/logging.callbacks.dart
Normal file
33
lib/src/callbacks/logging.callbacks.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
326
lib/src/callbacks/user_discovery.callbacks.dart
Normal file
326
lib/src/callbacks/user_discovery.callbacks.dart
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
class KeyValueKeys {
|
||||
static const String lastPeriodicTaskExecution =
|
||||
'last_periodic_task_execution';
|
||||
static const String currentBackupState = 'current_backup_state';
|
||||
static const String backupRecoveryState = 'backup_recovery_state';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ 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';
|
||||
|
|
@ -34,9 +33,16 @@ 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';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
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';
|
||||
static const String googleFcm = 'google_fcm';
|
||||
static const String userData = 'userData';
|
||||
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
|
||||
|
||||
@Deprecated('Use user.json file')
|
||||
static const String userData = 'userData';
|
||||
|
||||
// Not required for backup...
|
||||
static const String receivingPushKeys = 'push_keys_receiving';
|
||||
static const String sendingPushKeys = 'push_keys_sending';
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.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])
|
||||
@DriftAccessor(tables: [Contacts, KeyVerifications])
|
||||
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -103,6 +103,13 @@ 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)
|
||||
|
|
@ -134,6 +141,44 @@ 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -13,4 +15,9 @@ class ContactsDaoManager {
|
|||
ContactsDaoManager(this._db);
|
||||
$$ContactsTableTableManager get contacts =>
|
||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$KeyVerificationsTableTableManager get keyVerifications =>
|
||||
$$KeyVerificationsTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.keyVerifications,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.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';
|
||||
|
|
@ -113,7 +114,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
int contactId,
|
||||
GroupsCompanion group,
|
||||
) async {
|
||||
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId);
|
||||
final groupIdDirectChat = getUUIDforDirectChat(
|
||||
contactId,
|
||||
userService.currentUser.userId,
|
||||
);
|
||||
final insertGroup = group.copyWith(
|
||||
groupId: Value(groupIdDirectChat),
|
||||
isDirectChat: const Value(true),
|
||||
|
|
@ -136,15 +140,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
}
|
||||
|
||||
Future<Group?> _insertGroup(GroupsCompanion group) async {
|
||||
try {
|
||||
await into(groups).insert(group);
|
||||
return await (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
|
||||
} catch (e) {
|
||||
Log.error('Could not insert group: $e');
|
||||
return null;
|
||||
}
|
||||
await into(groups).insertOnConflictUpdate(group);
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getGroupContact(String groupId) async {
|
||||
|
|
@ -209,7 +208,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
}
|
||||
|
||||
Stream<Group?> watchDirectChat(int contactId) {
|
||||
final groupId = getUUIDforDirectChat(contactId, gUser.userId);
|
||||
final groupId = getUUIDforDirectChat(
|
||||
contactId,
|
||||
userService.currentUser.userId,
|
||||
);
|
||||
return (select(
|
||||
groups,
|
||||
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
|
||||
|
|
@ -235,7 +237,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<int> watchFlameCounter(String groupId) {
|
||||
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
|
||||
return (select(groups)..where(
|
||||
(u) =>
|
||||
u.groupId.equals(groupId) &
|
||||
|
|
@ -243,7 +245,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
u.lastMessageSend.isNotNull(),
|
||||
))
|
||||
.watchSingleOrNull()
|
||||
.asyncMap(getFlameCounterFromGroup);
|
||||
.map(getFlameCounterFromGroup);
|
||||
}
|
||||
|
||||
Future<List<Group>> getAllDirectChats() {
|
||||
|
|
@ -271,7 +273,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
groups.groupId.equalsExp(groupMembers.groupId),
|
||||
),
|
||||
],
|
||||
)..where(groups.isDirectChat.isNull()));
|
||||
)..where(groups.isDirectChat.equals(false)));
|
||||
return query.map((row) => row.readTable(groupMembers)).get();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
|
|
@ -291,6 +293,27 @@ 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()]);
|
||||
|
|
@ -311,4 +334,33 @@ 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
261
lib/src/database/daos/key_verification.dao.dart
Normal file
261
lib/src/database/daos/key_verification.dao.dart
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
lib/src/database/daos/key_verification.dao.g.dart
Normal file
52
lib/src/database/daos/key_verification.dao.g.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
|
|
@ -65,6 +65,10 @@ 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,
|
||||
|
|
@ -110,9 +114,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
|
||||
return (select(mediaFiles)..where(
|
||||
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
|
||||
(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()),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
|
@ -154,4 +164,43 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/globals.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/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,37 +140,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
Future<void> purgeMessageTable() async {
|
||||
final allGroups = await select(groups).get();
|
||||
|
||||
for (final group in allGroups) {
|
||||
final deletionTime = clock.now().subtract(
|
||||
Duration(
|
||||
milliseconds: group.deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
);
|
||||
await (delete(messages)..where(
|
||||
(m) =>
|
||||
m.groupId.equals(group.groupId) &
|
||||
(m.mediaStored.equals(true) &
|
||||
m.isDeletedFromSender.equals(true) |
|
||||
m.mediaStored.equals(false)) &
|
||||
// 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();
|
||||
final groupedByTime = <int, List<String>>{};
|
||||
for (final g in allGroups) {
|
||||
groupedByTime
|
||||
.putIfAbsent(g.deleteMessagesAfterMilliseconds, () => [])
|
||||
.add(g.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
for (final entry in groupedByTime.entries) {
|
||||
final deletionTime = clock.now().subtract(
|
||||
Duration(milliseconds: entry.key),
|
||||
);
|
||||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleMessageDeletion(
|
||||
|
|
@ -184,20 +186,35 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
return;
|
||||
}
|
||||
if (msg.mediaId != null && contactId != null) {
|
||||
// contactId -> When a image is send to multiple and one message is delete the image should be still available...
|
||||
await (delete(
|
||||
mediaFiles,
|
||||
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
|
||||
final otherMessagesWithSameMedia =
|
||||
await (select(messages)..where(
|
||||
(t) =>
|
||||
t.mediaId.equals(msg.mediaId!) &
|
||||
t.messageId.equals(messageId).not(),
|
||||
))
|
||||
.get();
|
||||
|
||||
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
|
||||
if (mediaService != null) {
|
||||
mediaService.fullMediaRemoval();
|
||||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
await (delete(
|
||||
messageHistories,
|
||||
)..where((t) => t.messageId.equals(messageId))).go();
|
||||
|
||||
await twonlyDB.receiptsDao.deleteReceiptsByMessageId(messageId);
|
||||
|
||||
await (update(messages)..where(
|
||||
(t) => t.messageId.equals(messageId),
|
||||
))
|
||||
|
|
@ -239,41 +256,70 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Future<void> handleMessagesOpened(
|
||||
int contactId,
|
||||
Value<int> contactId,
|
||||
List<String> messageIds,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
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,
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
for (final messageId in messageIds) {
|
||||
final isOpenedByAll = await haveAllMembers(
|
||||
messageId,
|
||||
MessageActionType.openedAt,
|
||||
);
|
||||
final now = clock.now();
|
||||
final ts = actionTimestamp;
|
||||
await transaction(() async {
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
contactId: contactId,
|
||||
type: const Value(MessageActionType.openedAt),
|
||||
actionAt: Value(ts),
|
||||
),
|
||||
);
|
||||
|
||||
batch.update(
|
||||
twonlyDB.messages,
|
||||
MessagesCompanion(
|
||||
openedAt: Value(now),
|
||||
openedByAll: Value(isOpenedByAll ? now : null),
|
||||
),
|
||||
where: (tbl) => tbl.messageId.equals(messageId),
|
||||
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',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('handleMessagesOpened failed for $messageId: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleMessageAckByServer(
|
||||
|
|
@ -281,57 +327,67 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
DateTime timestamp,
|
||||
) 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(clock.now())),
|
||||
);
|
||||
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)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> haveAllMembers(
|
||||
String messageId,
|
||||
MessageActionType action,
|
||||
) async {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null) return true;
|
||||
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||
message.groupId,
|
||||
);
|
||||
try {
|
||||
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;
|
||||
return members.length == actions.length;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateMessageId(
|
||||
String messageId,
|
||||
MessagesCompanion updatedValues,
|
||||
) async {
|
||||
await (update(
|
||||
final count = 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,
|
||||
) {
|
||||
return (update(
|
||||
) async {
|
||||
final count = await (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 {
|
||||
|
|
@ -344,7 +400,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
}
|
||||
|
||||
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
|
||||
await into(messages).insertOnConflictUpdate(insertMessage);
|
||||
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
message.groupId.value,
|
||||
|
|
@ -365,9 +421,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
}
|
||||
|
||||
final messageId = insertMessage.messageId.value;
|
||||
|
||||
return await (select(
|
||||
messages,
|
||||
)..where((t) => t.rowId.equals(rowId))).getSingle();
|
||||
)..where((t) => t.messageId.equals(messageId))).getSingle();
|
||||
} catch (e) {
|
||||
Log.error('Could not insert message: $e');
|
||||
return null;
|
||||
|
|
@ -399,6 +457,10 @@ 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(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.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/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||
|
||||
part 'reactions.dao.g.dart';
|
||||
|
||||
|
|
@ -29,7 +29,14 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null || msg.groupId != groupId) return;
|
||||
if (msg == null) {
|
||||
Log.error('updateReaction: Message $messageId not found!');
|
||||
return;
|
||||
}
|
||||
if (msg.groupId != groupId) {
|
||||
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (remove) {
|
||||
|
|
|
|||
|
|
@ -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.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
part 'receipts.dao.g.dart';
|
||||
|
|
@ -54,6 +54,13 @@ 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),
|
||||
|
|
@ -91,10 +98,11 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
receiptId: Value(uuid.v4()),
|
||||
);
|
||||
}
|
||||
final id = await into(receipts).insert(insertEntry);
|
||||
await into(receipts).insert(insertEntry);
|
||||
final receiptId = insertEntry.receiptId.value;
|
||||
return await (select(
|
||||
receipts,
|
||||
)..where((t) => t.rowId.equals(id))).getSingle();
|
||||
)..where((t) => t.receiptId.equals(receiptId))).getSingle();
|
||||
} catch (e) {
|
||||
// ignore error, receipts is already in the database...
|
||||
return null;
|
||||
|
|
@ -183,6 +191,23 @@ 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,
|
||||
|
|
|
|||
79
lib/src/database/daos/shortcuts.dao.dart
Normal file
79
lib/src/database/daos/shortcuts.dao.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
25
lib/src/database/daos/shortcuts.dao.g.dart
Normal file
25
lib/src/database/daos/shortcuts.dao.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
251
lib/src/database/daos/user_discovery.dao.dart
Normal file
251
lib/src/database/daos/user_discovery.dao.dart
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
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();
|
||||
}
|
||||
55
lib/src/database/daos/user_discovery.dao.g.dart
Normal file
55
lib/src/database/daos/user_discovery.dao.g.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
||||
164
lib/src/database/drift_logging_interceptor.dart
Normal file
164
lib/src/database/drift_logging_interceptor.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2754
lib/src/database/schemas/twonly_db/drift_schema_v12.json
Normal file
2754
lib/src/database/schemas/twonly_db/drift_schema_v12.json
Normal file
File diff suppressed because it is too large
Load diff
2901
lib/src/database/schemas/twonly_db/drift_schema_v13.json
Normal file
2901
lib/src/database/schemas/twonly_db/drift_schema_v13.json
Normal file
File diff suppressed because it is too large
Load diff
2939
lib/src/database/schemas/twonly_db/drift_schema_v14.json
Normal file
2939
lib/src/database/schemas/twonly_db/drift_schema_v14.json
Normal file
File diff suppressed because it is too large
Load diff
2995
lib/src/database/schemas/twonly_db/drift_schema_v15.json
Normal file
2995
lib/src/database/schemas/twonly_db/drift_schema_v15.json
Normal file
File diff suppressed because it is too large
Load diff
3019
lib/src/database/schemas/twonly_db/drift_schema_v16.json
Normal file
3019
lib/src/database/schemas/twonly_db/drift_schema_v16.json
Normal file
File diff suppressed because it is too large
Load diff
3033
lib/src/database/schemas/twonly_db/drift_schema_v17.json
Normal file
3033
lib/src/database/schemas/twonly_db/drift_schema_v17.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,80 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
||||
class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
||||
|
||||
final IdentityKeyPair identityKeyPair;
|
||||
final int localRegistrationId;
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class ConnectPreKeyStore extends PreKeyStore {
|
||||
class SignalPreKeyStore extends PreKeyStore {
|
||||
@override
|
||||
Future<bool> containsPreKey(int preKeyId) async {
|
||||
final preKeyRecord = await (twonlyDB.select(
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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';
|
||||
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';
|
||||
|
||||
class ConnectSignalProtocolStore implements SignalProtocolStore {
|
||||
ConnectSignalProtocolStore(
|
||||
class SignalSignalProtocolStore implements SignalProtocolStore {
|
||||
SignalSignalProtocolStore(
|
||||
IdentityKeyPair identityKeyPair,
|
||||
int registrationId,
|
||||
) {
|
||||
_identityKeyStore = ConnectIdentityKeyStore(
|
||||
_identityKeyStore = SignalIdentityKeyStore(
|
||||
identityKeyPair,
|
||||
registrationId,
|
||||
);
|
||||
}
|
||||
|
||||
final preKeyStore = ConnectPreKeyStore();
|
||||
final sessionStore = ConnectSessionStore();
|
||||
final signedPreKeyStore = ConnectSignedPreKeyStore();
|
||||
final preKeyStore = SignalPreKeyStore();
|
||||
final sessionStore = SignalSessionStore();
|
||||
final signedPreKeyStore = SignalSignedPreKeyStore();
|
||||
|
||||
late IdentityKeyStore _identityKeyStore;
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectSenderKeyStore extends SenderKeyStore {
|
||||
class SignalSenderKeyStore extends SenderKeyStore {
|
||||
@override
|
||||
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
||||
final identity =
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
class ConnectSessionStore extends SessionStore {
|
||||
class SignalSessionStore extends SessionStore {
|
||||
@override
|
||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||
final sessions =
|
||||
83
lib/src/database/signal/signal_signed_pre_key_store.dart
Normal file
83
lib/src/database/signal/signal_signed_pre_key_store.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
@DataClassName('Contact')
|
||||
class Contacts extends Table {
|
||||
IntColumn get userId => integer()();
|
||||
|
||||
|
|
@ -22,6 +23,46 @@ 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)();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ enum GroupActionType {
|
|||
demoteToMember,
|
||||
updatedGroupName,
|
||||
changeDisplayMaxTime,
|
||||
updatedContactUsername,
|
||||
updatedContactDisplayName,
|
||||
}
|
||||
|
||||
@DataClassName('GroupHistory')
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ 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()();
|
||||
|
||||
|
|
@ -66,7 +69,14 @@ 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};
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||
|
||||
@DataClassName('Message')
|
||||
class Messages extends Table {
|
||||
|
|
|
|||
26
lib/src/database/tables/shortcuts.table.dart
Normal file
26
lib/src/database/tables/shortcuts.table.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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};
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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};
|
||||
}
|
||||
89
lib/src/database/tables/user_discovery.table.dart
Normal file
89
lib/src/database/tables/user_discovery.table.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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,
|
||||
)();
|
||||
}
|
||||
|
|
@ -1,24 +1,31 @@
|
|||
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';
|
||||
|
||||
|
|
@ -40,8 +47,18 @@ part 'twonly.db.g.dart';
|
|||
SignalPreKeyStores,
|
||||
SignalSenderKeyStores,
|
||||
SignalSessionStores,
|
||||
SignalSignedPreKeyStores,
|
||||
MessageActions,
|
||||
GroupHistories,
|
||||
KeyVerifications,
|
||||
VerificationTokens,
|
||||
UserDiscoveryAnnouncedUsers,
|
||||
UserDiscoveryUserRelations,
|
||||
UserDiscoveryOtherPromotions,
|
||||
UserDiscoveryOwnPromotions,
|
||||
UserDiscoveryShares,
|
||||
Shortcuts,
|
||||
ShortcutMembers,
|
||||
],
|
||||
daos: [
|
||||
MessagesDao,
|
||||
|
|
@ -50,6 +67,9 @@ part 'twonly.db.g.dart';
|
|||
GroupsDao,
|
||||
ReactionsDao,
|
||||
MediaFilesDao,
|
||||
UserDiscoveryDao,
|
||||
KeyVerificationDao,
|
||||
ShortcutsDao,
|
||||
],
|
||||
)
|
||||
class TwonlyDB extends _$TwonlyDB {
|
||||
|
|
@ -62,16 +82,29 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 11;
|
||||
int get schemaVersion => 17;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
final connection = driftDatabase(
|
||||
name: 'twonly',
|
||||
native: const DriftNativeOptions(
|
||||
native: 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
|
||||
|
|
@ -93,7 +126,6 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(
|
||||
// ignore: experimental_member_use
|
||||
TableMigration(
|
||||
schema.groupHistories,
|
||||
columnTransformer: {
|
||||
|
|
@ -126,9 +158,7 @@ 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 {
|
||||
|
|
@ -153,6 +183,56 @@ 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);
|
||||
},
|
||||
);
|
||||
|
|
@ -174,38 +254,4 @@ 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 57ec512977e514fca6413622bb4a7e03701f09a0
|
||||
Subproject commit 189bf8f4dbe2bee4f19a15b9640b8826e4f2e235
|
||||
51
lib/src/model/json/backup.model.dart
Normal file
51
lib/src/model/json/backup.model.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
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);
|
||||
}
|
||||
65
lib/src/model/json/backup.model.g.dart
Normal file
65
lib/src/model/json/backup.model.g.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// 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',
|
||||
};
|
||||
89
lib/src/model/json/faq.model.dart
Normal file
89
lib/src/model/json/faq.model.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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
Loading…
Reference in a new issue