Compare commits

..

92 commits

Author SHA1 Message Date
7cb1e31e0c
Merge pull request #419 from twonlyapp/dev
- Improved: Design of some UI components
- Improved: Memories viewer shows state for batch operations and has improved performance
- Fix: Issue with background notifications on Android
- Fix: Changed minimum threshold for the user discovery to 3
- Fix: Multiple UI issues
- Fix: Auto-detect if FCM token does not work and trigger a reset
2026-06-10 22:42:01 +02:00
cef58119c5 remove old apks 2026-06-10 22:41:24 +02:00
3981341cd3 bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-10 00:07:18 +02:00
726cdaf89f use text capitalization
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-09 20:59:19 +02:00
23553b9cb0 fix clippy 2026-06-08 22:59:50 +02:00
37551cacce merge into one single crate 2026-06-08 22:58:01 +02:00
053fbeb66e Fix: Issue with background notifications on Android
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-08 22:05:22 +02:00
b7b2ad701f add app lifecycle state for camera controller
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-08 10:21:29 +02:00
e2f5f41736 add changelog 2026-06-08 10:14:55 +02:00
b192945328
Merge pull request #418 from twonlyapp/dev
- Fix: Changed minimum threshold for the user discovery to 3
- Fix: Multiple UI issues
2026-06-07 12:58:16 +02:00
52f962ae2e fix transparent color
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-07 12:53:44 +02:00
f66e1f17bf fix the response view 2026-06-07 12:44:57 +02:00
b49b9cf452 fix groups where not ordered correctly in the chat list
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-07 12:23:54 +02:00
d1eb5d5be6 fix callback issue and increase minimum threshold 2026-06-07 12:10:10 +02:00
da97fe5f3d add new test
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-06 00:46:19 +02:00
67b3ad2275 fix apple permission issue 2026-06-05 12:56:49 +02:00
97cdee2a37 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-05 12:27:59 +02:00
a9bbbacd3a fix anayzer 2026-06-05 12:27:07 +02:00
e59c4728a0 update flutter for ios 2026-06-05 12:25:59 +02:00
3306a1f9ec upgrading flutter 2026-06-05 11:41:03 +02:00
9224c77eca feedback on click
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-05 11:10:16 +02:00
92b615959b improve buttons 2026-06-05 11:01:20 +02:00
34ecb66e0b fix user discovery not working some times 2026-06-05 10:56:17 +02:00
1d3b8dbd8a ensure correct ordering of media files 2026-06-05 10:49:57 +02:00
3d130cb760 Auto-detect if FCM token does not work and trigger a reset
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-05 10:36:36 +02:00
12dce4f52d replacing more buttons
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-05 02:39:56 +02:00
e2cf5ec74a improved styling 2026-06-05 01:17:42 +02:00
24efc76c02 fix: add logging and hide non-downloaded images
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-01 23:39:12 +02:00
6501590dd7
Merge pull request #417 from twonlyapp/dev
Dev
2026-05-31 03:45:52 +02:00
d03c42659c remove permissions again
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-31 03:35:02 +02:00
849f748968 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-31 02:23:05 +02:00
9e28bb82a2 Improved: UI components adapt to native styling 2026-05-31 02:22:28 +02:00
9beb8ef9d7 update strings
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-31 02:12:04 +02:00
a688954d76 New: Import images from the gallery 2026-05-31 02:11:30 +02:00
358f93979e fixes database issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-30 23:02:58 +02:00
dc0ef25d73 add optional database tracing
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-29 09:25:24 +02:00
7559434f86 switch to fastlane for github and f-droid releases
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 13:59:14 +02:00
62457f1f48 increased logging again
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 12:41:10 +02:00
0602f043d2
Merge pull request #416 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Improves: Smaller UI changes
- Fix: Some messages were not marked as opened.
2026-05-28 02:32:47 +02:00
0c32b41dd0 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 02:32:07 +02:00
c10dc19342 remove date 2026-05-28 02:31:05 +02:00
872592af21 fix: database error and some ui improvements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 00:09:12 +02:00
c7826ad6dd improve logging
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-26 11:57:59 +02:00
25c826bff3
Merge pull request #415 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- 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
2026-05-22 13:52:52 +02:00
789bcda34f update faicons
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-22 13:52:01 +02:00
874cf5fecc
Merge pull request #414 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- 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
2026-05-22 13:23:06 +02:00
34607e05d1 bump version 2026-05-22 13:22:00 +02:00
3499a08155 fixes issue with messsages not received
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-22 12:41:08 +02:00
a50c2ba7d7 remove unused files 2026-05-21 16:14:23 +02:00
fae5ca3d25 Fix: Issues with the camera initialization
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 16:12:19 +02:00
d6432677df fix ui glitch
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 14:30:26 +02:00
00cb615e56 finish verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 14:20:15 +02:00
1ad304ec2e improve response viewer
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 13:33:13 +02:00
cd5409d021 Improved: Flame restore experience
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 13:10:02 +02:00
b7c4832ee2 improving onboarding flow and start with security profiles
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-20 03:24:00 +02:00
f42a49cadf Fix: Issue with focus changing when taking a picture
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-20 01:06:46 +02:00
2d6a2e436f Improved: Onboarding flow for new users. 2026-05-20 00:47:45 +02:00
c0e45cfe1f improve the add friends view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 22:33:24 +02:00
d9da953f77 Adds an "Ask a Friend" button to new contact suggestions.
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 18:49:57 +02:00
b788146beb improved ui elements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 16:23:32 +02:00
6f8f1efe81 shwo mutual groups
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 15:41:37 +02:00
304190387d improve qr code verifications
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 15:27:44 +02:00
65d188c4f2 show the number of verified contacts 2026-05-19 14:46:15 +02:00
d32e319c49 improve typing indicator 2026-05-19 14:46:01 +02:00
2cb51d668a add c2c testing
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 03:04:34 +02:00
927589a505 fix: background message fetching reliability
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 02:12:12 +02:00
fc5c74eaed Issue with receiving messages when user closed app while decrypting 2026-05-19 01:54:42 +02:00
d7e4da0e55 Fix: Images not shown after opening due to cleanup
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 22:31:22 +02:00
bfde01cbc5
Merge pull request #413 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Issue with opening directly in chats
2026-05-17 21:34:16 +02:00
7614da00b1 Issue with opening directly in chats
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 21:33:41 +02:00
dec79f3463
Merge pull request #412 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- Fix: Issue with opening directly in chats
- Fix: Multiple smaller issues
2026-05-17 20:33:02 +02:00
236d94622c only migrate if not yet opened
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 20:30:15 +02:00
5bcb3b3efe Fix: Issue with opening directly in chats 2026-05-17 20:23:37 +02:00
c77c369212 multiple bug issues 2026-05-17 19:25:08 +02:00
5fb51b20d7
Merge pull request #411 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- New: Tutorial on how to use zoom. 
- New: Manage storage view.
- Improved: Media thumbnails for faster loading.
- Fix: Some message where not marked as opened.
2026-05-17 01:35:34 +02:00
df974cd9f7 add translation
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 01:20:29 +02:00
0204a41d43 Fix: Some message where not marked as opened. 2026-05-17 01:13:28 +02:00
11c0ad908e ignore testing urls in CI
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 23:35:16 +02:00
7283852ba5 fix new emoji not shown 2026-05-16 23:32:26 +02:00
805d7a66b3 enable dark mode for registration 2026-05-16 23:15:16 +02:00
ea41158872 redesigning register view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 23:02:12 +02:00
32231d11c2 small redesign of the image view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 20:37:34 +02:00
68c99c271f smaller ui fixes 2026-05-16 19:13:42 +02:00
91eedc76b0 fix media not shown if stored and alread in chat 2026-05-16 18:42:44 +02:00
d0eee1893e fix ios click on push notifications not working
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 18:24:58 +02:00
fe2dd06213 fix strings 2026-05-16 18:24:28 +02:00
102d2579ce update strings 2026-05-16 18:18:51 +02:00
190be5b694 fix flutter analyze issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 17:15:39 +02:00
c2ac706239 Tutorial on how to use zoom. 2026-05-16 17:12:46 +02:00
e9b550023f New: Manage storage view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 16:52:19 +02:00
5556532879 Improved: Media thumbnails for faster loading 2026-05-16 16:21:44 +02:00
f3b64646f5 update graphics 2026-05-16 01:47:11 +02:00
296 changed files with 42495 additions and 7610 deletions

View file

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

View file

@ -1,68 +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: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install requirements
run: sudo apt-get install protobuf-compiler
- 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

7
.gitignore vendored
View file

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

View file

@ -1,5 +1,56 @@
# Changelog # Changelog
## 0.3.0
- Improved: Design of some UI components
- Improved: Memories viewer shows state for batch operations and has improved performance
- Fix: Issue with background notifications on Android
- Fix: Changed minimum threshold for the user discovery to 3
- Fix: Multiple UI issues
- Fix: Auto-detect if FCM token does not work and trigger a reset
## 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 ## 0.2.12
- New: Automatically mark identical media as opened across all chats (Settings > Chats). - New: Automatically mark identical media as opened across all chats (Settings > Chats).

View file

@ -1,6 +1,6 @@
# twonly # twonly
<a href="https://twonly.eu" rel="some text"><img src="docs/header.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a> <a href="https://twonly.eu" rel="some text"><img src="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. 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.

View file

@ -19,6 +19,7 @@ analyzer:
- "lib/core/**" - "lib/core/**"
- "lib/src/localization/**" - "lib/src/localization/**"
- "rust_builder/" - "rust_builder/"
- "build/"
- "dependencies/**" - "dependencies/**"
- "pubspec.yaml" - "pubspec.yaml"
- "**.arb" - "**.arb"

2
android/.gitignore vendored
View file

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

View file

@ -3,7 +3,6 @@ plugins {
// START: FlutterFire Configuration // START: FlutterFire Configuration
id 'com.google.gms.google-services' id 'com.google.gms.google-services'
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
} }

View file

@ -10,11 +10,32 @@ import android.content.Context
import io.crates.keyring.Keyring import io.crates.keyring.Keyring
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import android.os.Bundle import android.os.Bundle
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() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() 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) super.onCreate(savedInstanceState)
} }
@ -35,7 +56,36 @@ class MainActivity : FlutterFragmentActivity() {
Keyring.initializeNdkContext(applicationContext) Keyring.initializeNdkContext(applicationContext)
MediaStoreChannel.configure(flutterEngine, applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext) VideoCompressionChannel.configure(flutterEngine, applicationContext)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"pickImages" -> {
pendingResult = result
pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
"getUriBytes" -> {
val uriString = call.argument<String>("uri")
if (uriString != null) {
try {
val uri = Uri.parse(uriString)
val inputStream: InputStream? = contentResolver.openInputStream(uri)
if (inputStream != null) {
val bytes = inputStream.readBytes()
inputStream.close()
result.success(bytes)
} else {
result.error("UNAVAILABLE", "Could not open InputStream", null)
}
} catch (e: Exception) {
result.error("ERROR", e.message, null)
}
} else {
result.error("INVALID_ARGUMENT", "URI string is null", null)
}
}
else -> result.notImplemented()
}
}
} }
} }

View file

@ -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
}
}
}

View file

@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip

View 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

View file

@ -18,11 +18,11 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.9.1' apply false id "com.android.application" version '8.11.1' apply false
// START: FlutterFire Configuration // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.1.0" apply false id "org.jetbrains.kotlin.android" version "2.2.20" apply false
} }
include ":app" include ":app"

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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 e0c6a9617a20a8d6bc1ad4c6b9c2e229feb5f37a Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

2
fastlane/Appfile Normal file
View 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

149
fastlane/Fastfile Normal file
View file

@ -0,0 +1,149 @@
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 = "Small bug fixes."
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)
# Delete all APK files in the directory
sh("rm -f #{fdroid_repo_dir}/*.apk")
UI.message("All APK files deleted.")
apks.each do |apk_path|
basename = File.basename(apk_path)
new_name = "eu.twonly_v#{version}-#{basename}"
dest_path = File.join(fdroid_repo_dir, new_name)
UI.message("Copying APK to F-Droid repo: #{dest_path}")
FileUtils.cp(apk_path, dest_path)
end
fdroid_dir = "/Users/tobi/Documents/drive/twonly/F-Droid"
update_script = File.join(fdroid_dir, "update.sh")
if File.exist?(update_script)
UI.message("Executing F-Droid update script...")
Dir.chdir(fdroid_dir) do
sh("chmod +x ./update.sh && ./update.sh")
end
else
UI.important("F-Droid update script not found at #{update_script}")
end
end
end

View file

@ -15,6 +15,11 @@ class NotificationService: UNNotificationServiceExtension {
self.contentHandler = contentHandler self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
// Store the current timestamp in Keychain for iOS FCM messaging tracking
let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000)
writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs)
NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)")
if let bestAttemptContent = bestAttemptContent { if let bestAttemptContent = bestAttemptContent {
guard bestAttemptContent.userInfo as? [String: Any] != nil, guard bestAttemptContent.userInfo as? [String: Any] != nil,
@ -188,6 +193,7 @@ func readFromKeychain(key: String) -> String? {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key, kSecAttrAccount as String: key,
kSecAttrService as String: "flutter_secure_storage_service",
kSecReturnData as String: kCFBooleanTrue!, kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne, kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group
@ -205,6 +211,36 @@ func readFromKeychain(key: String) -> String? {
return nil return nil
} }
// Helper function to write to Keychain
func writeToKeychain(key: String, value: String) {
guard let data = value.data(using: .utf8) else {
NSLog("Failed to convert value to data for keychain key: \(key)")
return
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrService as String: "flutter_secure_storage_service",
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared"
]
// Delete existing item first to ensure a clean overwrite
SecItemDelete(query as CFDictionary)
// Add the new item with background-compatible accessibility
var addQuery = query
addQuery[kSecValueData as String] = data
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
let status = SecItemAdd(addQuery as CFDictionary, nil)
if status != errSecSuccess {
NSLog("Failed to write keychain item for key \(key): \(status)")
} else {
NSLog("Successfully wrote keychain item for key: \(key)")
}
}
func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) { func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) {
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language

View file

@ -28,17 +28,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup flutter_ios_podfile_setup
pod 'Firebase', :modular_headers => true
pod 'FirebaseMessaging', :modular_headers => true
pod 'FirebaseCoreInternal', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'SwiftProtobuf'
# pod 'sqlite3', :modular_headers => true
target 'Runner' do target 'Runner' do
pod 'SwiftProtobuf'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths
@ -76,6 +68,28 @@ post_install do |installer|
## dart: PermissionGroup.mediaLibrary ## dart: PermissionGroup.mediaLibrary
'PERMISSION_PHOTOS=1', 'PERMISSION_PHOTOS=1',
## dart: PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse
'PERMISSION_LOCATION=0',
## dart: PermissionGroup.contacts
'PERMISSION_CONTACTS=0',
## dart: PermissionGroup.calendar, PermissionGroup.reminders
'PERMISSION_EVENTS=0',
'PERMISSION_REMINDERS=0',
## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNITION=0',
## dart: PermissionGroup.bluetooth
'PERMISSION_BLUETOOTH=0',
## dart: PermissionGroup.appTrackingTransparency
'PERMISSION_APP_TRACKING_TRANSPARENCY=0',
## dart: PermissionGroup.sensors
'PERMISSION_SENSORS=0',
] ]
end end
@ -83,5 +97,6 @@ post_install do |installer|
end end
target 'NotificationService' do target 'NotificationService' do
pod 'SwiftProtobuf'
# pod 'Firebase/Messaging' # pod 'Firebase/Messaging'
end end

View file

@ -1,136 +1,18 @@
PODS: PODS:
- app_links (7.0.0):
- Flutter
- audio_waveforms (0.0.1): - audio_waveforms (0.0.1):
- Flutter - Flutter
- background_downloader (0.0.1):
- Flutter
- camera_avfoundation (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- cryptography_flutter_plus (0.2.0): - cryptography_flutter_plus (0.2.0):
- Flutter - Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- emoji_picker_flutter (0.0.1):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase (12.9.0):
- Firebase/Core (= 12.9.0)
- Firebase/Core (12.9.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 12.9.0)
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Installations (12.9.0):
- Firebase/CoreOnly
- FirebaseInstallations (~> 12.9.0)
- Firebase/Messaging (12.9.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.9.0)
- firebase_app_installations (0.4.1):
- Firebase/Installations (= 12.9.0)
- firebase_core
- Flutter
- firebase_core (4.6.0):
- Firebase/CoreOnly (= 12.9.0)
- Flutter
- firebase_messaging (16.1.3):
- Firebase/Messaging (= 12.9.0)
- firebase_core
- Flutter
- FirebaseAnalytics (12.9.0):
- FirebaseAnalytics/Default (= 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleAppMeasurement/Default (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.9.0):
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_image_compress_common (1.0.0): - flutter_image_compress_common (1.0.0):
- Flutter - Flutter
- Mantle - Mantle
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
- flutter_keyboard_visibility_temp_fork (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_sharing_intent (1.0.1): - flutter_sharing_intent (1.0.1):
- Flutter - Flutter
- flutter_volume_controller (0.0.1): - flutter_volume_controller (0.0.1):
- Flutter - Flutter
- gal (1.0.0):
- Flutter
- FlutterMacOS
- google_mlkit_barcode_scanning (0.14.2): - google_mlkit_barcode_scanning (0.14.2):
- Flutter - Flutter
- google_mlkit_commons - google_mlkit_commons
@ -142,33 +24,6 @@ PODS:
- Flutter - Flutter
- google_mlkit_commons - google_mlkit_commons
- GoogleMLKit/FaceDetection (~> 9.0.0) - GoogleMLKit/FaceDetection (~> 9.0.0)
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.9.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.9.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleAppMeasurement/IdentitySupport (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.9.0):
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0): - GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
@ -185,54 +40,16 @@ PODS:
- GoogleToolboxForMac/Defines (= 4.2.1) - GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)": - "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1) - GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities (8.1.0):
- GoogleUtilities/AppDelegateSwizzler (= 8.1.0)
- GoogleUtilities/Environment (= 8.1.0)
- GoogleUtilities/Logger (= 8.1.0)
- GoogleUtilities/MethodSwizzler (= 8.1.0)
- GoogleUtilities/Network (= 8.1.0)
- "GoogleUtilities/NSData+zlib (= 8.1.0)"
- GoogleUtilities/Privacy (= 8.1.0)
- GoogleUtilities/Reachability (= 8.1.0)
- GoogleUtilities/SwizzlerTestHelpers (= 8.1.0)
- GoogleUtilities/UserDefaults (= 8.1.0)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0): - GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0) - GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/SwizzlerTestHelpers (8.1.0):
- GoogleUtilities/MethodSwizzler
- GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0) - GTMSessionFetcher/Core (3.5.0)
- image_picker_ios (0.0.1):
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- integration_test (0.0.1):
- Flutter
- libwebp (1.5.0): - libwebp (1.5.0):
- libwebp/demux (= 1.5.0) - libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0) - libwebp/mux (= 1.5.0)
@ -245,9 +62,6 @@ PODS:
- libwebp/sharpyuv (1.5.0) - libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0): - libwebp/webp (1.5.0):
- libwebp/sharpyuv - libwebp/sharpyuv
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- Mantle (2.2.0): - Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0) - Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0) - Mantle/extobjc (2.2.0)
@ -276,15 +90,11 @@ PODS:
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- pro_video_editor (0.0.1): - pro_video_editor (0.0.1):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- restart_app (1.7.3):
- Flutter
- rust_lib_twonly (0.0.1): - rust_lib_twonly (0.0.1):
- Flutter - Flutter
- screen_protector (1.5.1): - screen_protector (1.5.1):
@ -297,89 +107,29 @@ PODS:
- SDWebImageWebPCoder (0.15.0): - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.58.0) - SwiftProtobuf (1.38.0)
- sentry_flutter (9.16.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.58.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- workmanager_apple (0.0.1): - workmanager_apple (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase
- firebase_app_installations (from `.symlinks/plugins/firebase_app_installations/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- FirebaseCore
- FirebaseCoreInternal
- FirebaseMessaging
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`) - google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
- google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`)
- google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`) - google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`)
- 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`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`) - 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`) - rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
- screen_protector (from `.symlinks/plugins/screen_protector/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`)
- SwiftProtobuf - SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport - GoogleDataTransport
- GoogleMLKit - GoogleMLKit
- GoogleToolboxForMac - GoogleToolboxForMac
@ -397,136 +147,54 @@ SPEC REPOS:
- ScreenProtectorKit - ScreenProtectorKit
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
- Sentry
- SwiftProtobuf - SwiftProtobuf
- SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
audio_waveforms: audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios" :path: ".symlinks/plugins/audio_waveforms/ios"
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cryptography_flutter_plus: cryptography_flutter_plus:
:path: ".symlinks/plugins/cryptography_flutter_plus/ios" :path: ".symlinks/plugins/cryptography_flutter_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
emoji_picker_flutter:
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_app_installations:
:path: ".symlinks/plugins/firebase_app_installations/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_image_compress_common: flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios" :path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_keyboard_visibility_temp_fork:
:path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_sharing_intent: flutter_sharing_intent:
:path: ".symlinks/plugins/flutter_sharing_intent/ios" :path: ".symlinks/plugins/flutter_sharing_intent/ios"
flutter_volume_controller: flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios" :path: ".symlinks/plugins/flutter_volume_controller/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
google_mlkit_barcode_scanning: google_mlkit_barcode_scanning:
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios" :path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
google_mlkit_commons: google_mlkit_commons:
:path: ".symlinks/plugins/google_mlkit_commons/ios" :path: ".symlinks/plugins/google_mlkit_commons/ios"
google_mlkit_face_detection: google_mlkit_face_detection:
:path: ".symlinks/plugins/google_mlkit_face_detection/ios" :path: ".symlinks/plugins/google_mlkit_face_detection/ios"
image_picker_ios:
: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"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
pro_video_editor: pro_video_editor:
:path: ".symlinks/plugins/pro_video_editor/ios" :path: ".symlinks/plugins/pro_video_editor/ios"
restart_app:
:path: ".symlinks/plugins/restart_app/ios"
rust_lib_twonly: rust_lib_twonly:
:path: ".symlinks/plugins/rust_lib_twonly/ios" :path: ".symlinks/plugins/rust_lib_twonly/ios"
screen_protector: screen_protector:
:path: ".symlinks/plugins/screen_protector/ios" :path: ".symlinks/plugins/screen_protector/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
workmanager_apple: workmanager_apple:
:path: ".symlinks/plugins/workmanager_apple/ios" :path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_app_installations: 1abd8d071ea2022d7888f7a9713710c37136ff91
firebase_core: 8e6f58412ca227827c366b92e7cee047a2148c60
firebase_messaging: c3aa897e0d40109cfb7927c40dc0dea799863f3b
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
gal: baecd024ebfd13c441269ca7404792a7152fde89
google_mlkit_barcode_scanning: 12d8422d8f7b00726dedf9cac00188a2b98750c2 google_mlkit_barcode_scanning: 12d8422d8f7b00726dedf9cac00188a2b98750c2
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166 google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36 google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444 GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0 MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0
MLKitBarcodeScanning: 39de223e7b1b8a8fbf10816a536dd292d8a39343 MLKitBarcodeScanning: 39de223e7b1b8a8fbf10816a536dd292d8a39343
@ -534,27 +202,17 @@ SPEC CHECKSUMS:
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961 MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520 rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150 screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71 ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7 PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View file

@ -23,6 +23,7 @@
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; }; D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -114,6 +115,7 @@
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; }; F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -165,6 +167,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */, CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */, D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
); );
@ -200,6 +203,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@ -307,6 +311,9 @@
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
@ -378,6 +385,9 @@
/* Begin PBXProject section */ /* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
@ -1309,6 +1319,18 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }

View file

@ -0,0 +1,194 @@
{
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
"version" : "12.14.0"
}
},
{
"identity" : "flutterfire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/flutterfire",
"state" : {
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
"version" : "4.6.0-firebase-core-swift"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
"version" : "3.6.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
"version" : "12.14.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.69.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
"version" : "5.3.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
"version" : "8.58.0"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}

View file

@ -5,6 +5,24 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"

View file

@ -0,0 +1,194 @@
{
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
"version" : "12.14.0"
}
},
{
"identity" : "flutterfire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/flutterfire",
"state" : {
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
"version" : "4.6.0-firebase-core-swift"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
"version" : "3.6.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
"version" : "12.14.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.69.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
"version" : "5.3.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa",
"state" : {
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
"version" : "8.58.0"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}

View file

@ -36,7 +36,9 @@ import workmanager_apple
return super.application(application, didFinishLaunchingWithOptions: launchOptions) 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 let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) { if sharingIntent.hasSameSchemePrefix(url: url) {
@ -58,7 +60,8 @@ import workmanager_apple
NSLog( NSLog(
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@", "Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
response.notification.request.content.userInfo) response.notification.request.content.userInfo)
//... super.userNotificationCenter(
center, didReceive: response, withCompletionHandler: completionHandler)
} }
override func userNotificationCenter( override func userNotificationCenter(

View file

@ -55,6 +55,8 @@
<string>Use your microphone to enable audio when making videos.</string> <string>Use your microphone to enable audio when making videos.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>twonly will save photos or videos to your library.</string> <string>twonly will save photos or videos to your library.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app does not use or store your location information.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

View file

@ -137,12 +137,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _isLoaded = false; bool _isLoaded = false;
bool _isTwonlyLocked = true; bool _isTwonlyLocked = true;
bool _wasLogged = true; bool _wasLogged = true;
late int _initialPage;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initialPage = widget.initialPage;
Log.info('AppWidgetState: initState started'); Log.info('AppWidgetState: initState started');
initAsync(); initAsync();
} }
@ -150,6 +152,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
Future<void> initAsync() async { Future<void> initAsync() async {
Log.info('AppWidgetState: initAsync started'); Log.info('AppWidgetState: initAsync started');
if (userService.isUserCreated) { if (userService.isUserCreated) {
if (_initialPage != 0) {
final count = await twonlyDB.contactsDao.getContactsCount();
if (count == 0) {
_initialPage = 0;
}
}
try { try {
unawaited(FirebaseMessaging.instance.requestPermission()); unawaited(FirebaseMessaging.instance.requestPermission());
} catch (e) { } catch (e) {
@ -210,7 +218,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
); );
} else { } else {
child = HomeView( child = HomeView(
initialPage: widget.initialPage, initialPage: _initialPage,
); );
} }
} else if (_showOnboarding) { } else if (_showOnboarding) {

View file

@ -9,8 +9,10 @@ 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 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` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`
Future<void> initFlutterCallbacks({ Future<void> initFlutterCallbacks({
required int callbackId,
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink, required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData, required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
@ -39,6 +41,7 @@ Future<void> initFlutterCallbacks({
required FutureOr<Uint8List?> Function(PlatformInt64) required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactPromotion, userDiscoveryGetContactPromotion,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks( }) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
callbackId: callbackId,
loggingGetStreamSink: loggingGetStreamSink, loggingGetStreamSink: loggingGetStreamSink,
userDiscoverySignData: userDiscoverySignData, userDiscoverySignData: userDiscoverySignData,
userDiscoveryVerifySignature: userDiscoveryVerifySignature, userDiscoveryVerifySignature: userDiscoveryVerifySignature,

View file

@ -9,36 +9,45 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class FlutterUserDiscovery { class FlutterUserDiscovery {
const FlutterUserDiscovery(); const FlutterUserDiscovery();
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(); RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
callbackId: callbackId,
);
static Future<List<Uint8List>> getNewMessages({ static Future<List<Uint8List>> getNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> receivedVersion, required List<int> receivedVersion,
}) => RustLib.instance.api }) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
callbackId: callbackId,
contactId: contactId, contactId: contactId,
receivedVersion: receivedVersion, receivedVersion: receivedVersion,
); );
static Future<void> handleNewMessages({ static Future<void> handleNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages, required List<Uint8List> messages,
}) => RustLib.instance.api }) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
callbackId: callbackId,
contactId: contactId, contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp, publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
messages: messages, messages: messages,
); );
static Future<void> initializeOrUpdate({ static Future<void> initializeOrUpdate({
required int callbackId,
required int threshold, required int threshold,
required PlatformInt64 userId, required PlatformInt64 userId,
required List<int> publicKey, required List<int> publicKey,
required bool sharePromotion, required bool sharePromotion,
}) => RustLib.instance.api }) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
callbackId: callbackId,
threshold: threshold, threshold: threshold,
userId: userId, userId: userId,
publicKey: publicKey, publicKey: publicKey,
@ -46,19 +55,23 @@ class FlutterUserDiscovery {
); );
static Future<Uint8List?> shouldRequestNewMessages({ static Future<Uint8List?> shouldRequestNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> version, required List<int> version,
}) => RustLib.instance.api }) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
callbackId: callbackId,
contactId: contactId, contactId: contactId,
version: version, version: version,
); );
static Future<void> updateVerificationStateForUser({ static Future<void> updateVerificationStateForUser({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
}) => RustLib.instance.api }) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
callbackId: callbackId,
contactId: contactId, contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp, publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
); );

View file

@ -87,16 +87,20 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
abstract class RustLibApi extends BaseApi { abstract class RustLibApi extends BaseApi {
Future<Uint8List> Future<Uint8List>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(); crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
required int callbackId,
});
Future<List<Uint8List>> Future<List<Uint8List>>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> receivedVersion, required List<int> receivedVersion,
}); });
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages, required List<Uint8List> messages,
@ -104,6 +108,7 @@ abstract class RustLibApi extends BaseApi {
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
required int callbackId,
required int threshold, required int threshold,
required PlatformInt64 userId, required PlatformInt64 userId,
required List<int> publicKey, required List<int> publicKey,
@ -112,17 +117,20 @@ abstract class RustLibApi extends BaseApi {
Future<Uint8List?> Future<Uint8List?>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> version, required List<int> version,
}); });
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
}); });
Future<void> crateBridgeCallbacksInitFlutterCallbacks({ Future<void> crateBridgeCallbacksInitFlutterCallbacks({
required int callbackId,
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink, required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData, required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
@ -242,11 +250,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@override @override
Future<Uint8List> Future<Uint8List>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion() { crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
required int callbackId,
}) {
return handler.executeNormal( return handler.executeNormal(
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
@ -260,7 +271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta,
argValues: [], argValues: [callbackId],
apiImpl: this, apiImpl: this,
), ),
); );
@ -270,12 +281,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_get_current_version", debugName: "flutter_user_discovery_get_current_version",
argNames: [], argNames: ["callbackId"],
); );
@override @override
Future<List<Uint8List>> Future<List<Uint8List>>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> receivedVersion, required List<int> receivedVersion,
}) { }) {
@ -283,6 +295,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_i_64(contactId, serializer); sse_encode_i_64(contactId, serializer);
sse_encode_list_prim_u_8_loose(receivedVersion, serializer); sse_encode_list_prim_u_8_loose(receivedVersion, serializer);
pdeCallFfi( pdeCallFfi(
@ -298,7 +311,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta,
argValues: [contactId, receivedVersion], argValues: [callbackId, contactId, receivedVersion],
apiImpl: this, apiImpl: this,
), ),
); );
@ -308,12 +321,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_get_new_messages", debugName: "flutter_user_discovery_get_new_messages",
argNames: ["contactId", "receivedVersion"], argNames: ["callbackId", "contactId", "receivedVersion"],
); );
@override @override
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages, required List<Uint8List> messages,
@ -322,6 +336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_i_64(contactId, serializer); sse_encode_i_64(contactId, serializer);
sse_encode_opt_box_autoadd_i_64( sse_encode_opt_box_autoadd_i_64(
publicKeyVerifiedTimestamp, publicKeyVerifiedTimestamp,
@ -341,7 +356,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
argValues: [contactId, publicKeyVerifiedTimestamp, messages], argValues: [
callbackId,
contactId,
publicKeyVerifiedTimestamp,
messages,
],
apiImpl: this, apiImpl: this,
), ),
); );
@ -351,12 +371,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_handle_new_messages", debugName: "flutter_user_discovery_handle_new_messages",
argNames: ["contactId", "publicKeyVerifiedTimestamp", "messages"], argNames: [
"callbackId",
"contactId",
"publicKeyVerifiedTimestamp",
"messages",
],
); );
@override @override
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
required int callbackId,
required int threshold, required int threshold,
required PlatformInt64 userId, required PlatformInt64 userId,
required List<int> publicKey, required List<int> publicKey,
@ -366,6 +392,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_u_8(threshold, serializer); sse_encode_u_8(threshold, serializer);
sse_encode_i_64(userId, serializer); sse_encode_i_64(userId, serializer);
sse_encode_list_prim_u_8_loose(publicKey, serializer); sse_encode_list_prim_u_8_loose(publicKey, serializer);
@ -383,7 +410,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta,
argValues: [threshold, userId, publicKey, sharePromotion], argValues: [callbackId, threshold, userId, publicKey, sharePromotion],
apiImpl: this, apiImpl: this,
), ),
); );
@ -393,12 +420,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_initialize_or_update", debugName: "flutter_user_discovery_initialize_or_update",
argNames: ["threshold", "userId", "publicKey", "sharePromotion"], argNames: [
"callbackId",
"threshold",
"userId",
"publicKey",
"sharePromotion",
],
); );
@override @override
Future<Uint8List?> Future<Uint8List?>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
required List<int> version, required List<int> version,
}) { }) {
@ -406,6 +440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_i_64(contactId, serializer); sse_encode_i_64(contactId, serializer);
sse_encode_list_prim_u_8_loose(version, serializer); sse_encode_list_prim_u_8_loose(version, serializer);
pdeCallFfi( pdeCallFfi(
@ -421,7 +456,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta,
argValues: [contactId, version], argValues: [callbackId, contactId, version],
apiImpl: this, apiImpl: this,
), ),
); );
@ -431,12 +466,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_should_request_new_messages", debugName: "flutter_user_discovery_should_request_new_messages",
argNames: ["contactId", "version"], argNames: ["callbackId", "contactId", "version"],
); );
@override @override
Future<void> Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp, PlatformInt64? publicKeyVerifiedTimestamp,
}) { }) {
@ -444,6 +480,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_i_64(contactId, serializer); sse_encode_i_64(contactId, serializer);
sse_encode_opt_box_autoadd_i_64( sse_encode_opt_box_autoadd_i_64(
publicKeyVerifiedTimestamp, publicKeyVerifiedTimestamp,
@ -462,7 +499,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: constMeta:
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta, kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta,
argValues: [contactId, publicKeyVerifiedTimestamp], argValues: [callbackId, contactId, publicKeyVerifiedTimestamp],
apiImpl: this, apiImpl: this,
), ),
); );
@ -472,11 +509,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta => get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta =>
const TaskConstMeta( const TaskConstMeta(
debugName: "flutter_user_discovery_update_verification_state_for_user", debugName: "flutter_user_discovery_update_verification_state_for_user",
argNames: ["contactId", "publicKeyVerifiedTimestamp"], argNames: ["callbackId", "contactId", "publicKeyVerifiedTimestamp"],
); );
@override @override
Future<void> crateBridgeCallbacksInitFlutterCallbacks({ Future<void> crateBridgeCallbacksInitFlutterCallbacks({
required int callbackId,
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink, required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData, required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
@ -513,6 +551,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
NormalTask( NormalTask(
callFfi: (port_) { callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_32(callbackId, serializer);
sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
loggingGetStreamSink, loggingGetStreamSink,
serializer, serializer,
@ -586,6 +625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
), ),
constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta, constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta,
argValues: [ argValues: [
callbackId,
loggingGetStreamSink, loggingGetStreamSink,
userDiscoverySignData, userDiscoverySignData,
userDiscoveryVerifySignature, userDiscoveryVerifySignature,
@ -611,6 +651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
const TaskConstMeta( const TaskConstMeta(
debugName: "init_flutter_callbacks", debugName: "init_flutter_callbacks",
argNames: [ argNames: [
"callbackId",
"loggingGetStreamSink", "loggingGetStreamSink",
"userDiscoverySignData", "userDiscoverySignData",
"userDiscoveryVerifySignature", "userDiscoveryVerifySignature",

View file

@ -1,8 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
class AppEnvironment { class AppEnvironment {
static late String cacheDir; static late String cacheDir;
static late String supportDir; static late String supportDir;
@ -32,6 +35,6 @@ class AppState {
static bool isInBackgroundTask = false; static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false; static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false; static bool gotMessageFromServer = false;
static int latestAppVersionId = 115; static int latestAppVersionId = 116;
static bool hasCameraPermissions = false;
} }

View file

@ -1,10 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
@ -15,34 +11,24 @@ import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart'; import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
show getSignalSignedPreKeyStoreOld;
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.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/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/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/utils/startup_guard.dart'; import 'package:twonly/src/utils/startup_guard.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
final _initMutex = Mutex(); final _initMutex = Mutex();
@ -90,7 +76,7 @@ void main() async {
unawaited(StartupGuard.markAppStartup()); unawaited(StartupGuard.markAppStartup());
var storageError = await twonlyMinimumInitialization(); var storageError = await twonlyMinimumInitialization();
await initFCMService(); await FcmNotificationService.initStartup();
var userExists = false; var userExists = false;
@ -123,6 +109,8 @@ void main() async {
unawaited(initFileDownloader()); unawaited(initFileDownloader());
if (userExists) { if (userExists) {
unawaited(FcmNotificationService.initAfterUserLoaded());
if (userService.currentUser.allowErrorTrackingViaSentry) { if (userService.currentUser.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true; AppState.allowErrorTrackingViaSentry = true;
await SentryFlutter.init( await SentryFlutter.init(
@ -168,144 +156,6 @@ void main() async {
); );
} }
Future<void> runMigrations() async {
if (userService.currentUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await UserService.update((u) => u.appVersion = 90);
}
if (userService.currentUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91();
await UserService.update((u) => u.appVersion = 91);
}
if (userService.currentUser.appVersion < 109) {
final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) {
if (contact.verified) {
await twonlyDB.keyVerificationDao.addKeyVerification(
contact.userId,
VerificationType.migratedFromOldVersion,
);
}
}
await UserService.update((u) {
u
..appVersion = 109
..skipSetupPages = true;
if (u.avatarSvg == null) {
u.currentSetupPage = SetupPages.profile.name;
} else {
u.currentSetupPage = SetupPages.shareYourFriends.name;
}
});
}
if (userService.currentUser.appVersion < 113) {
var migrationSuccess = true;
final signalIdentity = await SecureStorage.instance.read(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
try {
final decoded = jsonDecode(signalIdentity);
final identity = SignalIdentity.fromJson(
decoded as Map<String, dynamic>,
);
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identity.identityKeyPairU8List,
registrationId: identity.registrationId,
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
);
Log.info('Importing signal identiy to the rust key manager');
// Clean up old keys after successful migration
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalSignedPreKey,
);
} catch (e) {
Log.error('Failed to migrate signal identity: $e');
migrationSuccess = false;
}
}
if (migrationSuccess) {
await UserService.update((u) {
u
..appVersion = 113
..canUseLoginTokenForAuth = false
// As usernames changes where not considered in the old version force users
// to reenter there passwords.
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.encryptionKey = []
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.backupId = [];
});
}
}
if (userService.currentUser.appVersion < 114) {
final allMedia = await twonlyDB.mediaFilesDao
.select(twonlyDB.mediaFiles)
.get();
for (final media in allMedia) {
if (media.createdAtMonth == null) {
final monthStr = DateFormat('MMMM yyyy').format(media.createdAt);
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
MediaFilesCompanion(createdAtMonth: Value(monthStr)),
);
}
}
await UserService.update((u) => u.appVersion = 114);
}
if (userService.currentUser.appVersion < 115) {
var migrationSuccess = true;
try {
final rustStore = await RustKeyManager.loadSignedPrekeys();
for (final entry in rustStore.entries) {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(entry.key),
signedPreKey: Value(entry.value),
);
await twonlyDB
.into(twonlyDB.signalSignedPreKeyStores)
.insert(
companion,
mode: InsertMode.insertOrReplace,
);
await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key);
}
} catch (e) {
Log.error('Failed to migrate signed prekeys to Drift: $e');
migrationSuccess = false;
}
if (migrationSuccess) {
await UserService.update((u) => u.appVersion = 115);
}
}
if (kDebugMode) {
assert(
AppState.latestAppVersionId == 115,
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
);
assert(
AppState.latestAppVersionId == userService.currentUser.appVersion,
"Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.",
);
}
}
Future<void> postStartupTasks() async { Future<void> postStartupTasks() async {
Log.info('Post startup started.'); Log.info('Post startup started.');
unawaited(MemoriesService.prewarmCache()); unawaited(MemoriesService.prewarmCache());

View file

@ -1,9 +1,11 @@
import 'package:twonly/core/bridge/callbacks.dart'; import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart'; import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart'; import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
Future<void> initFlutterCallbacksForRust() async { Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks( await initFlutterCallbacks(
callbackId: isolateCallbackId,
loggingGetStreamSink: LoggingCallbacks.getStreamSink, loggingGetStreamSink: LoggingCallbacks.getStreamSink,
userDiscoverySetShares: UserDiscoveryCallbacks.setShares, userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
userDiscoveryGetShareForContact: userDiscoveryGetShareForContact:

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -15,7 +16,8 @@ class LoggingCallbacks {
Log.info(log.split('INFO ')[1]); Log.info(log.split('INFO ')[1]);
} else if (log.contains('DEBUG ')) { } else if (log.contains('DEBUG ')) {
Log.info(log.split('DEBUG ')[1]); Log.info(log.split('DEBUG ')[1]);
} else if (kDebugMode) { } else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) {
// ignore: avoid_print
print(log); print(log);
} }
}, },

View file

@ -35,9 +35,14 @@ class Routes {
'/settings/privacy/block_users'; '/settings/privacy/block_users';
static const String settingsPrivacyUserDiscovery = static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery'; '/settings/privacy/user_discovery';
static const String settingsPrivacyProfileSelection =
'/settings/privacy/profile_selection';
static const String settingsNotification = '/settings/notification'; static const String settingsNotification = '/settings/notification';
static const String settingsStorage = '/settings/storage_data'; 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 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 settingsStorageExport = '/settings/storage_data/export';
static const String settingsHelp = '/settings/help'; static const String settingsHelp = '/settings/help';
static const String settingsHelpFaq = '/settings/help/faq'; static const String settingsHelpFaq = '/settings/help/faq';
@ -57,5 +62,7 @@ class Routes {
'/settings/developer/automated_testing'; '/settings/developer/automated_testing';
static const String settingsDeveloperReduceFlames = static const String settingsDeveloperReduceFlames =
'/settings/developer/reduce_flames'; '/settings/developer/reduce_flames';
static const String settingsDeveloperInformations =
'/settings/developer/informations';
static const String settingsInvite = '/settings/invite'; static const String settingsInvite = '/settings/invite';
} }

View file

@ -12,4 +12,7 @@ class SecureStorageKeys {
// Not required for backup... // Not required for backup...
static const String receivingPushKeys = 'push_keys_receiving'; static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending'; static const String sendingPushKeys = 'push_keys_sending';
static const String lastFcmMessageTimestamp = 'last_fcm_message_timestamp';
static const String lastServerMessageTimestamp =
'last_server_message_timestamp';
} }

View file

@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return select(contacts).get(); 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() { Stream<int?> watchContactsBlocked() {
final count = contacts.userId.count(); final count = contacts.userId.count();
final query = selectOnly(contacts) final query = selectOnly(contacts)

View file

@ -1,6 +1,8 @@
import 'package:clock/clock.dart' show clock;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/locator.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/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
@ -292,6 +294,27 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return query.map((row) => row.readTable(groups)).getSingleOrNull(); 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() { Stream<int> watchSumTotalMediaCounter() {
final query = selectOnly(groups) final query = selectOnly(groups)
..addColumns([groups.totalMediaCounter.sum()]); ..addColumns([groups.totalMediaCounter.sum()]);
@ -305,12 +328,16 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
String groupId, String groupId,
DateTime newLastMessage, DateTime newLastMessage,
) async { ) async {
final now = clock.now();
final clampedLastMessage = newLastMessage.isAfter(now)
? now
: newLastMessage;
await (update(groups)..where( await (update(groups)..where(
(t) => (t) =>
t.groupId.equals(groupId) & t.groupId.equals(groupId) &
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)), (t.lastMessageExchange.isSmallerThanValue(clampedLastMessage)),
)) ))
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); .write(GroupsCompanion(lastMessageExchange: Value(clampedLastMessage)));
} }
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) { Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {

View file

@ -1,6 +1,7 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
@ -27,7 +28,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
KeyVerificationDao(super.db); KeyVerificationDao(super.db);
Future<List<VerificationToken>> getRecentVerificationTokens() { Future<List<VerificationToken>> getRecentVerificationTokens() {
final cutoff = DateTime.now().subtract(const Duration(hours: 24)); // 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( return (select(
verificationTokens, verificationTokens,
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get(); )..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
@ -215,6 +217,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
); );
if (userService.currentUser.isUserDiscoveryEnabled) { if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser( await FlutterUserDiscovery.updateVerificationStateForUser(
callbackId: isolateCallbackId,
contactId: contactId, contactId: contactId,
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch, publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
); );
@ -223,4 +226,40 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
Log.error(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(
callbackId: isolateCallbackId,
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(
callbackId: isolateCallbackId,
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
} }

View file

@ -114,16 +114,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.get(); .get();
} }
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async { Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
return (select(mediaFiles)..where( return (select(mediaFiles)..where(
(t) => t.stored.equals(true) & t.storedFileHash.isNull(), (t) =>
)) t.stored.equals(true) &
.get(); (t.storedFileHash.isNull() |
} t.hasCropAnalyzed.equals(false) |
(t.hasThumbnail.equals(false) &
Future<List<MediaFile>> getAllUnanalyzedStoredMediaFiles() async { t.type.equals(MediaType.audio.name).not()) |
return (select(mediaFiles)..where( t.sizeInBytes.isNull()),
(t) => t.stored.equals(true) & t.hasCropAnalyzed.equals(false),
)) ))
.get(); .get();
} }
@ -142,7 +141,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
Stream<List<MediaFile>> watchAllStoredMediaFiles() { Stream<List<MediaFile>> watchAllStoredMediaFiles() {
final query = final query =
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) (select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
..groupBy([mediaFiles.storedFileHash]); ..groupBy([
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)')
]);
return query.map((row) => row.readTable(mediaFiles)).watch(); return query.map((row) => row.readTable(mediaFiles)).watch();
} }
@ -185,4 +186,23 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
final rows = await query.get(); final rows = await query.get();
return rows.map((row) => row.readTable(db.messages).messageId).toList(); return rows.map((row) => row.readTable(db.messages).messageId).toList();
} }
Future<List<MediaFile>> getMediaByHash(Uint8List hash) async {
final query = select(db.mediaFiles)
..where((t) => t.storedFileHash.equals(hash));
return query.get();
}
Future<Map<MediaType, int>> getStorageStats() async {
final rows = await select(mediaFiles).get();
final stats = <MediaType, int>{};
for (final row in rows) {
final type = row.type;
final size = row.sizeInBytes ?? 0;
stats[type] = (stats[type] ?? 0) + size;
}
return stats;
}
} }

View file

@ -50,7 +50,8 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
mediaFiles, mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId), mediaFiles.mediaId.equalsExp(messages.mediaId),
), ),
])..where( ])
..where(
mediaFiles.downloadState mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name) .equals(DownloadState.reuploadRequested.name)
.not() & .not() &
@ -60,7 +61,8 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
messages.mediaId.isNotNull() & messages.mediaId.isNotNull() &
messages.senderId.isNotNull() & messages.senderId.isNotNull() &
messages.type.equals(MessageType.media.name), messages.type.equals(MessageType.media.name),
); )
..orderBy([OrderingTerm.asc(messages.createdAt)]);
return query.map((row) => row.readTable(messages)).watch(); return query.map((row) => row.readTable(messages)).watch();
} }
@ -93,24 +95,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
milliseconds: group!.deleteMessagesAfterMilliseconds, milliseconds: group!.deleteMessagesAfterMilliseconds,
), ),
); );
return ((select(messages)..where( final query =
(t) => select(messages).join([
t.groupId.equals(groupId) & leftOuterJoin(
// messages in groups will only be removed in case all members have received it... mediaFiles,
// so ensuring that this message is not shown in the messages anymore mediaFiles.mediaId.equalsExp(messages.mediaId),
(t.openedAt.isBiggerThanValue(deletionTime) | ),
t.openedAt.isNull() | ])
t.mediaStored.equals(true)) & ..where(
(t.isDeletedFromSender.equals(true) | messages.groupId.equals(groupId) &
(t.type.equals(MessageType.text.name).not() | (messages.openedAt.isBiggerThanValue(deletionTime) |
t.type.equals(MessageType.media.name).not()) | messages.openedAt.isNull() |
(t.type.equals(MessageType.text.name) & messages.mediaStored.equals(true)) &
t.content.isNotNull()) | (messages.isDeletedFromSender.equals(true) |
(t.type.equals(MessageType.media.name) & (messages.type.equals(MessageType.text.name).not() &
t.mediaId.isNotNull())), messages.type.equals(MessageType.media.name).not()) |
)) (messages.type.equals(MessageType.text.name) &
..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) messages.content.isNotNull()) |
.watch(); (messages.type.equals(MessageType.media.name) &
messages.mediaId.isNotNull() &
(mediaFiles.downloadState.isNull() |
mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name)
.not()))),
)
..orderBy([OrderingTerm.asc(messages.createdAt)]);
return query.map((row) => row.readTable(messages)).watch();
} }
Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) { Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) {
@ -153,18 +164,26 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
final groupIds = entry.value; final groupIds = entry.value;
final deletedCount =
await (delete(messages)..where( await (delete(messages)..where(
(m) => (m) =>
m.groupId.isIn(groupIds) & m.groupId.isIn(groupIds) &
(m.mediaStored.equals(true) & ((m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true) | m.isDeletedFromSender.equals(true)) |
m.mediaStored.equals(false)) & 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.. // 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.openedByAll.isNotNull() &
m.openedByAll.isSmallerThanValue(deletionTime)) |
(m.isDeletedFromSender.equals(true) & (m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))), m.createdAt.isSmallerThanValue(deletionTime))),
)) ))
.go(); .go();
if (deletedCount > 0) {
Log.info(
'Deleted $deletedCount messages for groups $groupIds due to retention policy.',
);
}
} }
} }
@ -249,41 +268,70 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Future<void> handleMessagesOpened( Future<void> handleMessagesOpened(
int contactId, Value<int> contactId,
List<String> messageIds, List<String> messageIds,
DateTime timestamp, DateTime timestamp,
) async { ) async {
await batch((batch) async {
for (final messageId in messageIds) { for (final messageId in messageIds) {
batch.insert( try {
messageActions, var actionTimestamp = timestamp;
MessageActionsCompanion( final msg = await getMessageById(messageId).getSingleOrNull();
messageId: Value(messageId), if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
contactId: Value(contactId), Log.warn(
type: const Value(MessageActionType.openedAt), 'Receiver clock skew detected for message $messageId. '
actionAt: Value(timestamp), 'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
), 'Clamping to creation time.',
mode: InsertMode.insertOrReplace,
); );
actionTimestamp = msg.createdAt;
} }
for (final messageId in messageIds) { final ts = actionTimestamp;
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: contactId,
type: const Value(MessageActionType.openedAt),
actionAt: Value(ts),
),
);
final isOpenedByAll = await haveAllMembers( final isOpenedByAll = await haveAllMembers(
messageId, messageId,
MessageActionType.openedAt, MessageActionType.openedAt,
); );
final now = clock.now(); await (update(
messages,
batch.update( )..where((tbl) => tbl.messageId.equals(messageId))).write(
twonlyDB.messages,
MessagesCompanion( MessagesCompanion(
openedAt: Value(now), openedAt: Value(ts),
openedByAll: Value(isOpenedByAll ? now : null), 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),
), ),
where: (tbl) => tbl.messageId.equals(messageId),
); );
} }
});
Log.info(
'handleMessagesOpened completed for message $messageId',
);
} catch (e) {
Log.error('handleMessagesOpened failed for $messageId: $e');
}
}
} }
Future<void> handleMessageAckByServer( Future<void> handleMessageAckByServer(
@ -291,6 +339,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
await transaction(() async {
await into(messageActions).insertOnConflictUpdate( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
@ -301,14 +350,16 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
messageId, messageId,
MessagesCompanion(ackByServer: Value(clock.now())), MessagesCompanion(ackByServer: Value(timestamp)),
); );
});
} }
Future<bool> haveAllMembers( Future<bool> haveAllMembers(
String messageId, String messageId,
MessageActionType action, MessageActionType action,
) async { ) async {
try {
final message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageById(messageId) .getMessageById(messageId)
.getSingleOrNull(); .getSingleOrNull();
@ -319,29 +370,36 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
final actions = final actions =
await (select(messageActions)..where( await (select(messageActions)..where(
(t) => t.type.equals(action.name) & t.messageId.equals(messageId), (t) =>
t.type.equals(action.name) & t.messageId.equals(messageId),
)) ))
.get(); .get();
return members.length == actions.length; return members.length == actions.length;
} catch (e) {
Log.error(e);
return true;
}
} }
Future<void> updateMessageId( Future<void> updateMessageId(
String messageId, String messageId,
MessagesCompanion updatedValues, MessagesCompanion updatedValues,
) async { ) async {
await (update( final count = await (update(
messages, messages,
)..where((c) => c.messageId.equals(messageId))).write(updatedValues); )..where((c) => c.messageId.equals(messageId))).write(updatedValues);
Log.info('Updated $count message(s) with messageId $messageId');
} }
Future<void> updateMessagesByMediaId( Future<void> updateMessagesByMediaId(
String mediaId, String mediaId,
MessagesCompanion updatedValues, MessagesCompanion updatedValues,
) { ) async {
return (update( final count = await (update(
messages, messages,
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues); )..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
Log.info('Updated $count message(s) with mediaId $mediaId');
} }
Future<Message?> insertMessage(MessagesCompanion message) async { Future<Message?> insertMessage(MessagesCompanion message) async {

View file

@ -29,7 +29,14 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageById(messageId) .getMessageById(messageId)
.getSingleOrNull(); .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 { try {
if (remove) { if (remove) {

View file

@ -228,6 +228,12 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
); );
} }
Future<UserDiscoveryAnnouncedUser?> getAnnouncedUserById(int id) async {
return (select(
userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull();
}
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() => Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
select(userDiscoveryAnnouncedUsers).watch(); select(userDiscoveryAnnouncedUsers).watch();

View 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;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,13 @@ import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async { Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read( final storeSerialized = await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey, key: 'signed_pre_key_store',
); );
final store = HashMap<int, Uint8List>(); final store = HashMap<int, Uint8List>();
if (storeSerialized == null) { if (storeSerialized == null) {

View file

@ -69,6 +69,11 @@ class MediaFiles extends Table {
BlobColumn get storedFileHash => blob().nullable()(); BlobColumn get storedFileHash => blob().nullable()();
BoolColumn get hasThumbnail =>
boolean().withDefault(const Constant(false))();
IntColumn get sizeInBytes => integer().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get createdAtMonth => text().nullable()(); TextColumn get createdAtMonth => text().nullable()();

View file

@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.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') @DataClassName('Message')
class Messages extends Table { class Messages extends Table {

View file

@ -16,6 +16,8 @@ class UserDiscoveryAnnouncedUsers extends Table {
BoolColumn get wasShownToTheUser => BoolColumn get wasShownToTheUser =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
BoolColumn get isHidden => boolean().withDefault(const Constant(false))(); BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
BoolColumn get wasAskedFriends =>
boolean().withDefault(const Constant(false))();
@override @override
Set<Column> get primaryKey => {announcedUserId}; Set<Column> get primaryKey => {announcedUserId};

View file

@ -1,8 +1,8 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart' import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase; show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart'; 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/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.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/key_verification.dao.dart';
@ -12,6 +12,7 @@ import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.dao.dart'; import 'package:twonly/src/database/daos/shortcuts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/drift_logging_interceptor.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -81,21 +82,29 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 15; int get schemaVersion => 17;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( final connection = driftDatabase(
name: 'twonly', name: 'twonly',
native: DriftNativeOptions( native: DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory, databaseDirectory: getApplicationSupportDirectory,
shareAcrossIsolates: true, shareAcrossIsolates: true,
setup: (rawDb) { setup: (rawDb) {
rawDb rawDb
..execute('PRAGMA journal_mode=WAL;') ..execute('PRAGMA journal_mode=DELETE;')
..execute('PRAGMA synchronous=FULL;')
..execute('PRAGMA busy_timeout=5000;'); ..execute('PRAGMA busy_timeout=5000;');
}, },
), ),
); );
try {
if (userService.isUserCreated &&
userService.currentUser.enableDatabaseLogging) {
return connection.interceptWith(DriftLoggingInterceptor());
}
} catch (_) {}
return connection;
} }
@override @override
@ -211,14 +220,30 @@ class TwonlyDB extends _$TwonlyDB {
from14To15: (m, schema) async { from14To15: (m, schema) async {
await m.createTable(schema.signalSignedPreKeyStores); 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); )(m, from, to);
}, },
); );
} }
void markUpdated() { void markUpdated() {
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)}); notifyUpdates({
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)}); TableUpdate.onTable(messages, kind: UpdateKind.update),
TableUpdate.onTable(contacts, kind: UpdateKind.update),
TableUpdate.onTable(groups, kind: UpdateKind.update),
});
} }
Future<void> printTableSizes() async { Future<void> printTableSizes() async {
@ -232,38 +257,4 @@ class TwonlyDB extends _$TwonlyDB {
Log.info('Table: $tableName, Size: $tableSize bytes'); 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();
}
} }

View file

@ -2810,6 +2810,32 @@ class $MediaFilesTable extends MediaFiles
type: DriftSqlType.blob, type: DriftSqlType.blob,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _hasThumbnailMeta = const VerificationMeta(
'hasThumbnail',
);
@override
late final GeneratedColumn<bool> hasThumbnail = GeneratedColumn<bool>(
'has_thumbnail',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("has_thumbnail" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _sizeInBytesMeta = const VerificationMeta(
'sizeInBytes',
);
@override
late final GeneratedColumn<int> sizeInBytes = GeneratedColumn<int>(
'size_in_bytes',
aliasedName,
true,
type: DriftSqlType.int,
requiredDuringInsert: false,
);
static const VerificationMeta _createdAtMeta = const VerificationMeta( static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt', 'createdAt',
); );
@ -2853,6 +2879,8 @@ class $MediaFilesTable extends MediaFiles
encryptionMac, encryptionMac,
encryptionNonce, encryptionNonce,
storedFileHash, storedFileHash,
hasThumbnail,
sizeInBytes,
createdAt, createdAt,
createdAtMonth, createdAtMonth,
]; ];
@ -2987,6 +3015,24 @@ class $MediaFilesTable extends MediaFiles
), ),
); );
} }
if (data.containsKey('has_thumbnail')) {
context.handle(
_hasThumbnailMeta,
hasThumbnail.isAcceptableOrUnknown(
data['has_thumbnail']!,
_hasThumbnailMeta,
),
);
}
if (data.containsKey('size_in_bytes')) {
context.handle(
_sizeInBytesMeta,
sizeInBytes.isAcceptableOrUnknown(
data['size_in_bytes']!,
_sizeInBytesMeta,
),
);
}
if (data.containsKey('created_at')) { if (data.containsKey('created_at')) {
context.handle( context.handle(
_createdAtMeta, _createdAtMeta,
@ -3092,6 +3138,14 @@ class $MediaFilesTable extends MediaFiles
DriftSqlType.blob, DriftSqlType.blob,
data['${effectivePrefix}stored_file_hash'], data['${effectivePrefix}stored_file_hash'],
), ),
hasThumbnail: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}has_thumbnail'],
)!,
sizeInBytes: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}size_in_bytes'],
),
createdAt: attachedDatabase.typeMapping.read( createdAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
@ -3147,6 +3201,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final Uint8List? encryptionMac; final Uint8List? encryptionMac;
final Uint8List? encryptionNonce; final Uint8List? encryptionNonce;
final Uint8List? storedFileHash; final Uint8List? storedFileHash;
final bool hasThumbnail;
final int? sizeInBytes;
final DateTime createdAt; final DateTime createdAt;
final String? createdAtMonth; final String? createdAtMonth;
const MediaFile({ const MediaFile({
@ -3168,6 +3224,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
this.encryptionMac, this.encryptionMac,
this.encryptionNonce, this.encryptionNonce,
this.storedFileHash, this.storedFileHash,
required this.hasThumbnail,
this.sizeInBytes,
required this.createdAt, required this.createdAt,
this.createdAtMonth, this.createdAtMonth,
}); });
@ -3228,6 +3286,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
if (!nullToAbsent || storedFileHash != null) { if (!nullToAbsent || storedFileHash != null) {
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash); map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
} }
map['has_thumbnail'] = Variable<bool>(hasThumbnail);
if (!nullToAbsent || sizeInBytes != null) {
map['size_in_bytes'] = Variable<int>(sizeInBytes);
}
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
if (!nullToAbsent || createdAtMonth != null) { if (!nullToAbsent || createdAtMonth != null) {
map['created_at_month'] = Variable<String>(createdAtMonth); map['created_at_month'] = Variable<String>(createdAtMonth);
@ -3278,6 +3340,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
storedFileHash: storedFileHash == null && nullToAbsent storedFileHash: storedFileHash == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(storedFileHash), : Value(storedFileHash),
hasThumbnail: Value(hasThumbnail),
sizeInBytes: sizeInBytes == null && nullToAbsent
? const Value.absent()
: Value(sizeInBytes),
createdAt: Value(createdAt), createdAt: Value(createdAt),
createdAtMonth: createdAtMonth == null && nullToAbsent createdAtMonth: createdAtMonth == null && nullToAbsent
? const Value.absent() ? const Value.absent()
@ -3323,6 +3389,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']), encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']), encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']), storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
hasThumbnail: serializer.fromJson<bool>(json['hasThumbnail']),
sizeInBytes: serializer.fromJson<int?>(json['sizeInBytes']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']), createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
); );
@ -3357,6 +3425,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac), 'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce), 'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash), 'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
'hasThumbnail': serializer.toJson<bool>(hasThumbnail),
'sizeInBytes': serializer.toJson<int?>(sizeInBytes),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'createdAtMonth': serializer.toJson<String?>(createdAtMonth), 'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
}; };
@ -3381,6 +3451,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
Value<Uint8List?> encryptionMac = const Value.absent(), Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(), Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(), Value<Uint8List?> storedFileHash = const Value.absent(),
bool? hasThumbnail,
Value<int?> sizeInBytes = const Value.absent(),
DateTime? createdAt, DateTime? createdAt,
Value<String?> createdAtMonth = const Value.absent(), Value<String?> createdAtMonth = const Value.absent(),
}) => MediaFile( }) => MediaFile(
@ -3421,6 +3493,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
storedFileHash: storedFileHash.present storedFileHash: storedFileHash.present
? storedFileHash.value ? storedFileHash.value
: this.storedFileHash, : this.storedFileHash,
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
createdAtMonth: createdAtMonth.present createdAtMonth: createdAtMonth.present
? createdAtMonth.value ? createdAtMonth.value
@ -3476,6 +3550,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
storedFileHash: data.storedFileHash.present storedFileHash: data.storedFileHash.present
? data.storedFileHash.value ? data.storedFileHash.value
: this.storedFileHash, : this.storedFileHash,
hasThumbnail: data.hasThumbnail.present
? data.hasThumbnail.value
: this.hasThumbnail,
sizeInBytes: data.sizeInBytes.present
? data.sizeInBytes.value
: this.sizeInBytes,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
createdAtMonth: data.createdAtMonth.present createdAtMonth: data.createdAtMonth.present
? data.createdAtMonth.value ? data.createdAtMonth.value
@ -3504,6 +3584,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('encryptionMac: $encryptionMac, ') ..write('encryptionMac: $encryptionMac, ')
..write('encryptionNonce: $encryptionNonce, ') ..write('encryptionNonce: $encryptionNonce, ')
..write('storedFileHash: $storedFileHash, ') ..write('storedFileHash: $storedFileHash, ')
..write('hasThumbnail: $hasThumbnail, ')
..write('sizeInBytes: $sizeInBytes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('createdAtMonth: $createdAtMonth') ..write('createdAtMonth: $createdAtMonth')
..write(')')) ..write(')'))
@ -3511,7 +3593,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
} }
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hashAll([
mediaId, mediaId,
type, type,
uploadState, uploadState,
@ -3530,9 +3612,11 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$driftBlobEquality.hash(encryptionMac), $driftBlobEquality.hash(encryptionMac),
$driftBlobEquality.hash(encryptionNonce), $driftBlobEquality.hash(encryptionNonce),
$driftBlobEquality.hash(storedFileHash), $driftBlobEquality.hash(storedFileHash),
hasThumbnail,
sizeInBytes,
createdAt, createdAt,
createdAtMonth, createdAtMonth,
); ]);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -3561,6 +3645,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
other.storedFileHash, other.storedFileHash,
this.storedFileHash, this.storedFileHash,
) && ) &&
other.hasThumbnail == this.hasThumbnail &&
other.sizeInBytes == this.sizeInBytes &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt &&
other.createdAtMonth == this.createdAtMonth); other.createdAtMonth == this.createdAtMonth);
} }
@ -3584,6 +3670,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<Uint8List?> encryptionMac; final Value<Uint8List?> encryptionMac;
final Value<Uint8List?> encryptionNonce; final Value<Uint8List?> encryptionNonce;
final Value<Uint8List?> storedFileHash; final Value<Uint8List?> storedFileHash;
final Value<bool> hasThumbnail;
final Value<int?> sizeInBytes;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<String?> createdAtMonth; final Value<String?> createdAtMonth;
final Value<int> rowid; final Value<int> rowid;
@ -3606,6 +3694,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionMac = const Value.absent(), this.encryptionMac = const Value.absent(),
this.encryptionNonce = const Value.absent(), this.encryptionNonce = const Value.absent(),
this.storedFileHash = const Value.absent(), this.storedFileHash = const Value.absent(),
this.hasThumbnail = const Value.absent(),
this.sizeInBytes = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.createdAtMonth = const Value.absent(), this.createdAtMonth = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -3629,6 +3719,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionMac = const Value.absent(), this.encryptionMac = const Value.absent(),
this.encryptionNonce = const Value.absent(), this.encryptionNonce = const Value.absent(),
this.storedFileHash = const Value.absent(), this.storedFileHash = const Value.absent(),
this.hasThumbnail = const Value.absent(),
this.sizeInBytes = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.createdAtMonth = const Value.absent(), this.createdAtMonth = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -3653,6 +3745,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<Uint8List>? encryptionMac, Expression<Uint8List>? encryptionMac,
Expression<Uint8List>? encryptionNonce, Expression<Uint8List>? encryptionNonce,
Expression<Uint8List>? storedFileHash, Expression<Uint8List>? storedFileHash,
Expression<bool>? hasThumbnail,
Expression<int>? sizeInBytes,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<String>? createdAtMonth, Expression<String>? createdAtMonth,
Expression<int>? rowid, Expression<int>? rowid,
@ -3680,6 +3774,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (encryptionMac != null) 'encryption_mac': encryptionMac, if (encryptionMac != null) 'encryption_mac': encryptionMac,
if (encryptionNonce != null) 'encryption_nonce': encryptionNonce, if (encryptionNonce != null) 'encryption_nonce': encryptionNonce,
if (storedFileHash != null) 'stored_file_hash': storedFileHash, if (storedFileHash != null) 'stored_file_hash': storedFileHash,
if (hasThumbnail != null) 'has_thumbnail': hasThumbnail,
if (sizeInBytes != null) 'size_in_bytes': sizeInBytes,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (createdAtMonth != null) 'created_at_month': createdAtMonth, if (createdAtMonth != null) 'created_at_month': createdAtMonth,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
@ -3705,6 +3801,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<Uint8List?>? encryptionMac, Value<Uint8List?>? encryptionMac,
Value<Uint8List?>? encryptionNonce, Value<Uint8List?>? encryptionNonce,
Value<Uint8List?>? storedFileHash, Value<Uint8List?>? storedFileHash,
Value<bool>? hasThumbnail,
Value<int?>? sizeInBytes,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<String?>? createdAtMonth, Value<String?>? createdAtMonth,
Value<int>? rowid, Value<int>? rowid,
@ -3731,6 +3829,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
encryptionMac: encryptionMac ?? this.encryptionMac, encryptionMac: encryptionMac ?? this.encryptionMac,
encryptionNonce: encryptionNonce ?? this.encryptionNonce, encryptionNonce: encryptionNonce ?? this.encryptionNonce,
storedFileHash: storedFileHash ?? this.storedFileHash, storedFileHash: storedFileHash ?? this.storedFileHash,
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
sizeInBytes: sizeInBytes ?? this.sizeInBytes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
createdAtMonth: createdAtMonth ?? this.createdAtMonth, createdAtMonth: createdAtMonth ?? this.createdAtMonth,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
@ -3810,6 +3910,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (storedFileHash.present) { if (storedFileHash.present) {
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash.value); map['stored_file_hash'] = Variable<Uint8List>(storedFileHash.value);
} }
if (hasThumbnail.present) {
map['has_thumbnail'] = Variable<bool>(hasThumbnail.value);
}
if (sizeInBytes.present) {
map['size_in_bytes'] = Variable<int>(sizeInBytes.value);
}
if (createdAt.present) { if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value); map['created_at'] = Variable<DateTime>(createdAt.value);
} }
@ -3843,6 +3949,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('encryptionMac: $encryptionMac, ') ..write('encryptionMac: $encryptionMac, ')
..write('encryptionNonce: $encryptionNonce, ') ..write('encryptionNonce: $encryptionNonce, ')
..write('storedFileHash: $storedFileHash, ') ..write('storedFileHash: $storedFileHash, ')
..write('hasThumbnail: $hasThumbnail, ')
..write('sizeInBytes: $sizeInBytes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('createdAtMonth: $createdAtMonth, ') ..write('createdAtMonth: $createdAtMonth, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
@ -10210,6 +10318,21 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
), ),
defaultValue: const Constant(false), defaultValue: const Constant(false),
); );
static const VerificationMeta _wasAskedFriendsMeta = const VerificationMeta(
'wasAskedFriends',
);
@override
late final GeneratedColumn<bool> wasAskedFriends = GeneratedColumn<bool>(
'was_asked_friends',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("was_asked_friends" IN (0, 1))',
),
defaultValue: const Constant(false),
);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
announcedUserId, announcedUserId,
@ -10218,6 +10341,7 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
username, username,
wasShownToTheUser, wasShownToTheUser,
isHidden, isHidden,
wasAskedFriends,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@ -10280,6 +10404,15 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta), isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta),
); );
} }
if (data.containsKey('was_asked_friends')) {
context.handle(
_wasAskedFriendsMeta,
wasAskedFriends.isAcceptableOrUnknown(
data['was_asked_friends']!,
_wasAskedFriendsMeta,
),
);
}
return context; return context;
} }
@ -10316,6 +10449,10 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
DriftSqlType.bool, DriftSqlType.bool,
data['${effectivePrefix}is_hidden'], data['${effectivePrefix}is_hidden'],
)!, )!,
wasAskedFriends: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}was_asked_friends'],
)!,
); );
} }
@ -10333,6 +10470,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
final String? username; final String? username;
final bool wasShownToTheUser; final bool wasShownToTheUser;
final bool isHidden; final bool isHidden;
final bool wasAskedFriends;
const UserDiscoveryAnnouncedUser({ const UserDiscoveryAnnouncedUser({
required this.announcedUserId, required this.announcedUserId,
required this.announcedPublicKey, required this.announcedPublicKey,
@ -10340,6 +10478,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
this.username, this.username,
required this.wasShownToTheUser, required this.wasShownToTheUser,
required this.isHidden, required this.isHidden,
required this.wasAskedFriends,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@ -10352,6 +10491,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
} }
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser); map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser);
map['is_hidden'] = Variable<bool>(isHidden); map['is_hidden'] = Variable<bool>(isHidden);
map['was_asked_friends'] = Variable<bool>(wasAskedFriends);
return map; return map;
} }
@ -10365,6 +10505,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
: Value(username), : Value(username),
wasShownToTheUser: Value(wasShownToTheUser), wasShownToTheUser: Value(wasShownToTheUser),
isHidden: Value(isHidden), isHidden: Value(isHidden),
wasAskedFriends: Value(wasAskedFriends),
); );
} }
@ -10382,6 +10523,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
username: serializer.fromJson<String?>(json['username']), username: serializer.fromJson<String?>(json['username']),
wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']), wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']),
isHidden: serializer.fromJson<bool>(json['isHidden']), isHidden: serializer.fromJson<bool>(json['isHidden']),
wasAskedFriends: serializer.fromJson<bool>(json['wasAskedFriends']),
); );
} }
@override @override
@ -10394,6 +10536,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
'username': serializer.toJson<String?>(username), 'username': serializer.toJson<String?>(username),
'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser), 'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser),
'isHidden': serializer.toJson<bool>(isHidden), 'isHidden': serializer.toJson<bool>(isHidden),
'wasAskedFriends': serializer.toJson<bool>(wasAskedFriends),
}; };
} }
@ -10404,6 +10547,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
Value<String?> username = const Value.absent(), Value<String?> username = const Value.absent(),
bool? wasShownToTheUser, bool? wasShownToTheUser,
bool? isHidden, bool? isHidden,
bool? wasAskedFriends,
}) => UserDiscoveryAnnouncedUser( }) => UserDiscoveryAnnouncedUser(
announcedUserId: announcedUserId ?? this.announcedUserId, announcedUserId: announcedUserId ?? this.announcedUserId,
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey, announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
@ -10411,6 +10555,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
username: username.present ? username.value : this.username, username: username.present ? username.value : this.username,
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
isHidden: isHidden ?? this.isHidden, isHidden: isHidden ?? this.isHidden,
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
); );
UserDiscoveryAnnouncedUser copyWithCompanion( UserDiscoveryAnnouncedUser copyWithCompanion(
UserDiscoveryAnnouncedUsersCompanion data, UserDiscoveryAnnouncedUsersCompanion data,
@ -10428,6 +10573,9 @@ class UserDiscoveryAnnouncedUser extends DataClass
? data.wasShownToTheUser.value ? data.wasShownToTheUser.value
: this.wasShownToTheUser, : this.wasShownToTheUser,
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
wasAskedFriends: data.wasAskedFriends.present
? data.wasAskedFriends.value
: this.wasAskedFriends,
); );
} }
@ -10439,7 +10587,8 @@ class UserDiscoveryAnnouncedUser extends DataClass
..write('publicId: $publicId, ') ..write('publicId: $publicId, ')
..write('username: $username, ') ..write('username: $username, ')
..write('wasShownToTheUser: $wasShownToTheUser, ') ..write('wasShownToTheUser: $wasShownToTheUser, ')
..write('isHidden: $isHidden') ..write('isHidden: $isHidden, ')
..write('wasAskedFriends: $wasAskedFriends')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -10452,6 +10601,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
username, username,
wasShownToTheUser, wasShownToTheUser,
isHidden, isHidden,
wasAskedFriends,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -10465,7 +10615,8 @@ class UserDiscoveryAnnouncedUser extends DataClass
other.publicId == this.publicId && other.publicId == this.publicId &&
other.username == this.username && other.username == this.username &&
other.wasShownToTheUser == this.wasShownToTheUser && other.wasShownToTheUser == this.wasShownToTheUser &&
other.isHidden == this.isHidden); other.isHidden == this.isHidden &&
other.wasAskedFriends == this.wasAskedFriends);
} }
class UserDiscoveryAnnouncedUsersCompanion class UserDiscoveryAnnouncedUsersCompanion
@ -10476,6 +10627,7 @@ class UserDiscoveryAnnouncedUsersCompanion
final Value<String?> username; final Value<String?> username;
final Value<bool> wasShownToTheUser; final Value<bool> wasShownToTheUser;
final Value<bool> isHidden; final Value<bool> isHidden;
final Value<bool> wasAskedFriends;
const UserDiscoveryAnnouncedUsersCompanion({ const UserDiscoveryAnnouncedUsersCompanion({
this.announcedUserId = const Value.absent(), this.announcedUserId = const Value.absent(),
this.announcedPublicKey = const Value.absent(), this.announcedPublicKey = const Value.absent(),
@ -10483,6 +10635,7 @@ class UserDiscoveryAnnouncedUsersCompanion
this.username = const Value.absent(), this.username = const Value.absent(),
this.wasShownToTheUser = const Value.absent(), this.wasShownToTheUser = const Value.absent(),
this.isHidden = const Value.absent(), this.isHidden = const Value.absent(),
this.wasAskedFriends = const Value.absent(),
}); });
UserDiscoveryAnnouncedUsersCompanion.insert({ UserDiscoveryAnnouncedUsersCompanion.insert({
this.announcedUserId = const Value.absent(), this.announcedUserId = const Value.absent(),
@ -10491,6 +10644,7 @@ class UserDiscoveryAnnouncedUsersCompanion
this.username = const Value.absent(), this.username = const Value.absent(),
this.wasShownToTheUser = const Value.absent(), this.wasShownToTheUser = const Value.absent(),
this.isHidden = const Value.absent(), this.isHidden = const Value.absent(),
this.wasAskedFriends = const Value.absent(),
}) : announcedPublicKey = Value(announcedPublicKey), }) : announcedPublicKey = Value(announcedPublicKey),
publicId = Value(publicId); publicId = Value(publicId);
static Insertable<UserDiscoveryAnnouncedUser> custom({ static Insertable<UserDiscoveryAnnouncedUser> custom({
@ -10500,6 +10654,7 @@ class UserDiscoveryAnnouncedUsersCompanion
Expression<String>? username, Expression<String>? username,
Expression<bool>? wasShownToTheUser, Expression<bool>? wasShownToTheUser,
Expression<bool>? isHidden, Expression<bool>? isHidden,
Expression<bool>? wasAskedFriends,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (announcedUserId != null) 'announced_user_id': announcedUserId, if (announcedUserId != null) 'announced_user_id': announcedUserId,
@ -10509,6 +10664,7 @@ class UserDiscoveryAnnouncedUsersCompanion
if (username != null) 'username': username, if (username != null) 'username': username,
if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser, if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser,
if (isHidden != null) 'is_hidden': isHidden, if (isHidden != null) 'is_hidden': isHidden,
if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends,
}); });
} }
@ -10519,6 +10675,7 @@ class UserDiscoveryAnnouncedUsersCompanion
Value<String?>? username, Value<String?>? username,
Value<bool>? wasShownToTheUser, Value<bool>? wasShownToTheUser,
Value<bool>? isHidden, Value<bool>? isHidden,
Value<bool>? wasAskedFriends,
}) { }) {
return UserDiscoveryAnnouncedUsersCompanion( return UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: announcedUserId ?? this.announcedUserId, announcedUserId: announcedUserId ?? this.announcedUserId,
@ -10527,6 +10684,7 @@ class UserDiscoveryAnnouncedUsersCompanion
username: username ?? this.username, username: username ?? this.username,
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser, wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
isHidden: isHidden ?? this.isHidden, isHidden: isHidden ?? this.isHidden,
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
); );
} }
@ -10553,6 +10711,9 @@ class UserDiscoveryAnnouncedUsersCompanion
if (isHidden.present) { if (isHidden.present) {
map['is_hidden'] = Variable<bool>(isHidden.value); map['is_hidden'] = Variable<bool>(isHidden.value);
} }
if (wasAskedFriends.present) {
map['was_asked_friends'] = Variable<bool>(wasAskedFriends.value);
}
return map; return map;
} }
@ -10564,7 +10725,8 @@ class UserDiscoveryAnnouncedUsersCompanion
..write('publicId: $publicId, ') ..write('publicId: $publicId, ')
..write('username: $username, ') ..write('username: $username, ')
..write('wasShownToTheUser: $wasShownToTheUser, ') ..write('wasShownToTheUser: $wasShownToTheUser, ')
..write('isHidden: $isHidden') ..write('isHidden: $isHidden, ')
..write('wasAskedFriends: $wasAskedFriends')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -15344,6 +15506,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
Value<Uint8List?> encryptionMac, Value<Uint8List?> encryptionMac,
Value<Uint8List?> encryptionNonce, Value<Uint8List?> encryptionNonce,
Value<Uint8List?> storedFileHash, Value<Uint8List?> storedFileHash,
Value<bool> hasThumbnail,
Value<int?> sizeInBytes,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<String?> createdAtMonth, Value<String?> createdAtMonth,
Value<int> rowid, Value<int> rowid,
@ -15368,6 +15532,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
Value<Uint8List?> encryptionMac, Value<Uint8List?> encryptionMac,
Value<Uint8List?> encryptionNonce, Value<Uint8List?> encryptionNonce,
Value<Uint8List?> storedFileHash, Value<Uint8List?> storedFileHash,
Value<bool> hasThumbnail,
Value<int?> sizeInBytes,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<String?> createdAtMonth, Value<String?> createdAtMonth,
Value<int> rowid, Value<int> rowid,
@ -15499,6 +15665,16 @@ class $$MediaFilesTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get hasThumbnail => $composableBuilder(
column: $table.hasThumbnail,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get sizeInBytes => $composableBuilder(
column: $table.sizeInBytes,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get createdAt => $composableBuilder( ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -15634,6 +15810,16 @@ class $$MediaFilesTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get hasThumbnail => $composableBuilder(
column: $table.hasThumbnail,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get sizeInBytes => $composableBuilder(
column: $table.sizeInBytes,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get createdAt => $composableBuilder( ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -15741,6 +15927,16 @@ class $$MediaFilesTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<bool> get hasThumbnail => $composableBuilder(
column: $table.hasThumbnail,
builder: (column) => column,
);
GeneratedColumn<int> get sizeInBytes => $composableBuilder(
column: $table.sizeInBytes,
builder: (column) => column,
);
GeneratedColumn<DateTime> get createdAt => GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column); $composableBuilder(column: $table.createdAt, builder: (column) => column);
@ -15821,6 +16017,8 @@ class $$MediaFilesTableTableManager
Value<Uint8List?> encryptionMac = const Value.absent(), Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(), Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(), Value<Uint8List?> storedFileHash = const Value.absent(),
Value<bool> hasThumbnail = const Value.absent(),
Value<int?> sizeInBytes = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<String?> createdAtMonth = const Value.absent(), Value<String?> createdAtMonth = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -15843,6 +16041,8 @@ class $$MediaFilesTableTableManager
encryptionMac: encryptionMac, encryptionMac: encryptionMac,
encryptionNonce: encryptionNonce, encryptionNonce: encryptionNonce,
storedFileHash: storedFileHash, storedFileHash: storedFileHash,
hasThumbnail: hasThumbnail,
sizeInBytes: sizeInBytes,
createdAt: createdAt, createdAt: createdAt,
createdAtMonth: createdAtMonth, createdAtMonth: createdAtMonth,
rowid: rowid, rowid: rowid,
@ -15867,6 +16067,8 @@ class $$MediaFilesTableTableManager
Value<Uint8List?> encryptionMac = const Value.absent(), Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(), Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(), Value<Uint8List?> storedFileHash = const Value.absent(),
Value<bool> hasThumbnail = const Value.absent(),
Value<int?> sizeInBytes = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<String?> createdAtMonth = const Value.absent(), Value<String?> createdAtMonth = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -15889,6 +16091,8 @@ class $$MediaFilesTableTableManager
encryptionMac: encryptionMac, encryptionMac: encryptionMac,
encryptionNonce: encryptionNonce, encryptionNonce: encryptionNonce,
storedFileHash: storedFileHash, storedFileHash: storedFileHash,
hasThumbnail: hasThumbnail,
sizeInBytes: sizeInBytes,
createdAt: createdAt, createdAt: createdAt,
createdAtMonth: createdAtMonth, createdAtMonth: createdAtMonth,
rowid: rowid, rowid: rowid,
@ -21384,6 +21588,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableCreateCompanionBuilder =
Value<String?> username, Value<String?> username,
Value<bool> wasShownToTheUser, Value<bool> wasShownToTheUser,
Value<bool> isHidden, Value<bool> isHidden,
Value<bool> wasAskedFriends,
}); });
typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder = typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
UserDiscoveryAnnouncedUsersCompanion Function({ UserDiscoveryAnnouncedUsersCompanion Function({
@ -21393,6 +21598,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
Value<String?> username, Value<String?> username,
Value<bool> wasShownToTheUser, Value<bool> wasShownToTheUser,
Value<bool> isHidden, Value<bool> isHidden,
Value<bool> wasAskedFriends,
}); });
final class $$UserDiscoveryAnnouncedUsersTableReferences final class $$UserDiscoveryAnnouncedUsersTableReferences
@ -21481,6 +21687,11 @@ class $$UserDiscoveryAnnouncedUsersTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get wasAskedFriends => $composableBuilder(
column: $table.wasAskedFriends,
builder: (column) => ColumnFilters(column),
);
Expression<bool> userDiscoveryUserRelationsRefs( Expression<bool> userDiscoveryUserRelationsRefs(
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f) Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
f, f,
@ -21547,6 +21758,11 @@ class $$UserDiscoveryAnnouncedUsersTableOrderingComposer
column: $table.isHidden, column: $table.isHidden,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get wasAskedFriends => $composableBuilder(
column: $table.wasAskedFriends,
builder: (column) => ColumnOrderings(column),
);
} }
class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
@ -21582,6 +21798,11 @@ class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
GeneratedColumn<bool> get isHidden => GeneratedColumn<bool> get isHidden =>
$composableBuilder(column: $table.isHidden, builder: (column) => column); $composableBuilder(column: $table.isHidden, builder: (column) => column);
GeneratedColumn<bool> get wasAskedFriends => $composableBuilder(
column: $table.wasAskedFriends,
builder: (column) => column,
);
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>( Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
Expression<T> Function( Expression<T> Function(
$$UserDiscoveryUserRelationsTableAnnotationComposer a, $$UserDiscoveryUserRelationsTableAnnotationComposer a,
@ -21660,6 +21881,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
Value<String?> username = const Value.absent(), Value<String?> username = const Value.absent(),
Value<bool> wasShownToTheUser = const Value.absent(), Value<bool> wasShownToTheUser = const Value.absent(),
Value<bool> isHidden = const Value.absent(), Value<bool> isHidden = const Value.absent(),
Value<bool> wasAskedFriends = const Value.absent(),
}) => UserDiscoveryAnnouncedUsersCompanion( }) => UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: announcedUserId, announcedUserId: announcedUserId,
announcedPublicKey: announcedPublicKey, announcedPublicKey: announcedPublicKey,
@ -21667,6 +21889,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
username: username, username: username,
wasShownToTheUser: wasShownToTheUser, wasShownToTheUser: wasShownToTheUser,
isHidden: isHidden, isHidden: isHidden,
wasAskedFriends: wasAskedFriends,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
@ -21676,6 +21899,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
Value<String?> username = const Value.absent(), Value<String?> username = const Value.absent(),
Value<bool> wasShownToTheUser = const Value.absent(), Value<bool> wasShownToTheUser = const Value.absent(),
Value<bool> isHidden = const Value.absent(), Value<bool> isHidden = const Value.absent(),
Value<bool> wasAskedFriends = const Value.absent(),
}) => UserDiscoveryAnnouncedUsersCompanion.insert( }) => UserDiscoveryAnnouncedUsersCompanion.insert(
announcedUserId: announcedUserId, announcedUserId: announcedUserId,
announcedPublicKey: announcedPublicKey, announcedPublicKey: announcedPublicKey,
@ -21683,6 +21907,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
username: username, username: username,
wasShownToTheUser: wasShownToTheUser, wasShownToTheUser: wasShownToTheUser,
isHidden: isHidden, isHidden: isHidden,
wasAskedFriends: wasAskedFriends,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map( .map(

File diff suppressed because it is too large Load diff

View file

@ -98,16 +98,10 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @registerTitle.
///
/// In en, this message translates to:
/// **'Welcome to twonly!'**
String get registerTitle;
/// No description provided for @registerSlogan. /// No description provided for @registerSlogan.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing'** /// **'Stay in touch privately.'**
String get registerSlogan; String get registerSlogan;
/// No description provided for @onboardingWelcomeTitle. /// No description provided for @onboardingWelcomeTitle.
@ -146,18 +140,6 @@ abstract class AppLocalizations {
/// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'** /// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'**
String get onboardingFocusBody; String get onboardingFocusBody;
/// No description provided for @onboardingSendTwonliesTitle.
///
/// In en, this message translates to:
/// **'Send twonlies'**
String get onboardingSendTwonliesTitle;
/// No description provided for @onboardingSendTwonliesBody.
///
/// In en, this message translates to:
/// **'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!'**
String get onboardingSendTwonliesBody;
/// No description provided for @onboardingNotProductTitle. /// No description provided for @onboardingNotProductTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -170,16 +152,10 @@ abstract class AppLocalizations {
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'** /// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'**
String get onboardingNotProductBody; String get onboardingNotProductBody;
/// No description provided for @onboardingGetStartedTitle.
///
/// In en, this message translates to:
/// **'Let\'s go!'**
String get onboardingGetStartedTitle;
/// No description provided for @registerUsernameSlogan. /// No description provided for @registerUsernameSlogan.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Please select a username so others can find you!'** /// **'Create your account'**
String get registerUsernameSlogan; String get registerUsernameSlogan;
/// No description provided for @registerUsernameDecoration. /// No description provided for @registerUsernameDecoration.
@ -191,7 +167,7 @@ abstract class AppLocalizations {
/// No description provided for @registerUsernameLimits. /// No description provided for @registerUsernameLimits.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Your username must be at least 3 characters long.'** /// **'At least 3 characters.'**
String get registerUsernameLimits; String get registerUsernameLimits;
/// No description provided for @registerProofOfWorkFailed. /// No description provided for @registerProofOfWorkFailed.
@ -326,24 +302,12 @@ abstract class AppLocalizations {
/// **'Username not found'** /// **'Username not found'**
String get searchUsernameNotFound; String get searchUsernameNotFound;
/// No description provided for @searchUsernameNotFoundBody.
///
/// In en, this message translates to:
/// **'There is no user with the username \"{username}\" registered'**
String searchUsernameNotFoundBody(Object username);
/// No description provided for @searchUsernameNewFollowerTitle. /// No description provided for @searchUsernameNewFollowerTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Open requests'** /// **'Open requests'**
String get searchUsernameNewFollowerTitle; String get searchUsernameNewFollowerTitle;
/// No description provided for @chatListViewSearchUserNameBtn.
///
/// In en, this message translates to:
/// **'Add your first twonly contact!'**
String get chatListViewSearchUserNameBtn;
/// No description provided for @chatListDetailInput. /// No description provided for @chatListDetailInput.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -518,12 +482,6 @@ abstract class AppLocalizations {
/// **'Store in Gallery'** /// **'Store in Gallery'**
String get settingsStorageDataStoreInGTitle; String get settingsStorageDataStoreInGTitle;
/// No description provided for @settingsStorageDataStoreInGSubtitle.
///
/// In en, this message translates to:
/// **'Store saved images additional in the systems gallery.'**
String get settingsStorageDataStoreInGSubtitle;
/// No description provided for @settingsStorageDataMediaAutoDownload. /// No description provided for @settingsStorageDataMediaAutoDownload.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -542,6 +500,36 @@ abstract class AppLocalizations {
/// **'When using WI-FI'** /// **'When using WI-FI'**
String get settingsStorageDataAutoDownWifi; String get settingsStorageDataAutoDownWifi;
/// No description provided for @settingsStorageManageTitle.
///
/// In en, this message translates to:
/// **'Manage storage'**
String get settingsStorageManageTitle;
/// No description provided for @settingsStorageUsed.
///
/// In en, this message translates to:
/// **'Storage used'**
String get settingsStorageUsed;
/// No description provided for @settingsStorageImages.
///
/// In en, this message translates to:
/// **'Images'**
String get settingsStorageImages;
/// No description provided for @settingsStorageVideos.
///
/// In en, this message translates to:
/// **'Videos'**
String get settingsStorageVideos;
/// No description provided for @settingsStorageGifs.
///
/// In en, this message translates to:
/// **'GIFs'**
String get settingsStorageGifs;
/// No description provided for @settingsProfileCustomizeAvatar. /// No description provided for @settingsProfileCustomizeAvatar.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -587,13 +575,13 @@ abstract class AppLocalizations {
/// No description provided for @settingsPrivacyBlockUsers. /// No description provided for @settingsPrivacyBlockUsers.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Block users'** /// **'Block contacts'**
String get settingsPrivacyBlockUsers; String get settingsPrivacyBlockUsers;
/// No description provided for @settingsPrivacyBlockUsersDesc. /// No description provided for @settingsPrivacyBlockUsersDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'** /// **'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.'**
String get settingsPrivacyBlockUsersDesc; String get settingsPrivacyBlockUsersDesc;
/// No description provided for @settingsPrivacyBlockUsersCount. /// No description provided for @settingsPrivacyBlockUsersCount.
@ -602,6 +590,48 @@ abstract class AppLocalizations {
/// **'{len} contact(s)'** /// **'{len} contact(s)'**
String settingsPrivacyBlockUsersCount(Object len); String settingsPrivacyBlockUsersCount(Object len);
/// No description provided for @settingsPrivacyProfileSelectionTitle.
///
/// In en, this message translates to:
/// **'Security Profile'**
String get settingsPrivacyProfileSelectionTitle;
/// No description provided for @securityProfileTitle.
///
/// In en, this message translates to:
/// **'Security Profile'**
String get securityProfileTitle;
/// No description provided for @securityProfileSubtitle.
///
/// In en, this message translates to:
/// **'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'**
String get securityProfileSubtitle;
/// No description provided for @securityProfileNormalTitle.
///
/// In en, this message translates to:
/// **'Normal Protection'**
String get securityProfileNormalTitle;
/// No description provided for @securityProfileNormalDesc.
///
/// In en, this message translates to:
/// **'Good balance between a convenient mode without bothering you too much.'**
String get securityProfileNormalDesc;
/// No description provided for @securityProfileStrictTitle.
///
/// In en, this message translates to:
/// **'Strict Protection'**
String get securityProfileStrictTitle;
/// No description provided for @securityProfileStrictDesc.
///
/// In en, this message translates to:
/// **'Maximum anti-phishing protection but may be inconvenient.'**
String get securityProfileStrictDesc;
/// No description provided for @settingsNotification. /// No description provided for @settingsNotification.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -863,13 +893,19 @@ abstract class AppLocalizations {
/// No description provided for @contactVerifyNumberTitle. /// No description provided for @contactVerifyNumberTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Verify contact'** /// **'Verify contacts'**
String get contactVerifyNumberTitle; String get contactVerifyNumberTitle;
/// No description provided for @contactVerifyNumberSubtitle.
///
/// In en, this message translates to:
/// **'Verify the identity of your contacts to make sure you are texting the right person.'**
String get contactVerifyNumberSubtitle;
/// No description provided for @userVerifiedTitle. /// No description provided for @userVerifiedTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'User verified'** /// **'Contact verified'**
String get userVerifiedTitle; String get userVerifiedTitle;
/// No description provided for @contactVerifiedBy. /// No description provided for @contactVerifiedBy.
@ -887,8 +923,8 @@ abstract class AppLocalizations {
/// No description provided for @verificationTypeSecretQrToken. /// No description provided for @verificationTypeSecretQrToken.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'The other person scanned your QR code.'** /// **'{username} has scanned your QR code.'**
String get verificationTypeSecretQrToken; String verificationTypeSecretQrToken(Object username);
/// No description provided for @verificationTypeLink. /// No description provided for @verificationTypeLink.
/// ///
@ -941,13 +977,13 @@ abstract class AppLocalizations {
/// No description provided for @contactBlockBody. /// No description provided for @contactBlockBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'** /// **'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.'**
String get contactBlockBody; String get contactBlockBody;
/// No description provided for @contactRemove. /// No description provided for @contactRemove.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Remove user'** /// **'Remove contact'**
String get contactRemove; String get contactRemove;
/// No description provided for @contactRemoveTitle. /// No description provided for @contactRemoveTitle.
@ -959,7 +995,7 @@ abstract class AppLocalizations {
/// No description provided for @contactRemoveBody. /// No description provided for @contactRemoveBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'** /// **'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.'**
String get contactRemoveBody; String get contactRemoveBody;
/// No description provided for @undo. /// No description provided for @undo.
@ -1133,8 +1169,8 @@ abstract class AppLocalizations {
/// No description provided for @userFoundBody. /// No description provided for @userFoundBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Do you want to create a follow request?'** /// **'Do you want to connect with {username}?'**
String get userFoundBody; String userFoundBody(String username);
/// No description provided for @errorInternalError. /// No description provided for @errorInternalError.
/// ///
@ -1388,6 +1424,12 @@ abstract class AppLocalizations {
/// **'Delete for all'** /// **'Delete for all'**
String get deleteOkBtnForAll; String get deleteOkBtnForAll;
/// No description provided for @memoriesDeleteSnackbarSuccess.
///
/// In en, this message translates to:
/// **'{count, plural, =1 {Deleted 1 item successfully} other {Deleted {count} items successfully}}'**
String memoriesDeleteSnackbarSuccess(num count);
/// No description provided for @deleteOkBtnForMe. /// No description provided for @deleteOkBtnForMe.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1406,6 +1448,12 @@ abstract class AppLocalizations {
/// **'The image will be irrevocably deleted.'** /// **'The image will be irrevocably deleted.'**
String get deleteImageBody; String get deleteImageBody;
/// No description provided for @deleteMemoriesBody.
///
/// In en, this message translates to:
/// **'{count, plural, =1 {The image will be irrevocably deleted.} other {The {count} images will be irrevocably deleted.}}'**
String deleteMemoriesBody(num count);
/// No description provided for @settingsBackup. /// No description provided for @settingsBackup.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1553,15 +1601,9 @@ abstract class AppLocalizations {
/// No description provided for @twonlySafeRecoverTitle. /// No description provided for @twonlySafeRecoverTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Recovery'** /// **'Restore backup'**
String get twonlySafeRecoverTitle; String get twonlySafeRecoverTitle;
/// No description provided for @twonlySafeRecoverDesc.
///
/// In en, this message translates to:
/// **'If you have created a backup with twonly Backup, you can restore it here.'**
String get twonlySafeRecoverDesc;
/// No description provided for @twonlySafeRecoverBtn. /// No description provided for @twonlySafeRecoverBtn.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1643,7 +1685,7 @@ abstract class AppLocalizations {
/// No description provided for @reportUser. /// No description provided for @reportUser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Report user'** /// **'Report contact'**
String get reportUser; String get reportUser;
/// No description provided for @newDeviceRegistered. /// No description provided for @newDeviceRegistered.
@ -2282,18 +2324,6 @@ abstract class AppLocalizations {
/// **'Draft'** /// **'Draft'**
String get draftMessage; String get draftMessage;
/// No description provided for @exportMemories.
///
/// In en, this message translates to:
/// **'Export memories (Beta)'**
String get exportMemories;
/// No description provided for @importMemories.
///
/// In en, this message translates to:
/// **'Import memories (Beta)'**
String get importMemories;
/// No description provided for @voiceMessageSlideToCancel. /// No description provided for @voiceMessageSlideToCancel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2324,6 +2354,12 @@ abstract class AppLocalizations {
/// **'Open your own QR code'** /// **'Open your own QR code'**
String get openYourOwnQRcode; String get openYourOwnQRcode;
/// No description provided for @addContactQrSheetSubtext.
///
/// In en, this message translates to:
/// **'Let a friend scan this QR code to add you'**
String get addContactQrSheetSubtext;
/// No description provided for @finishSetupCardTitle. /// No description provided for @finishSetupCardTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2417,13 +2453,13 @@ abstract class AppLocalizations {
/// No description provided for @userDiscoverySettingsManualApproval. /// No description provided for @userDiscoverySettingsManualApproval.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Manual approval'** /// **'Ask every time before sharing'**
String get userDiscoverySettingsManualApproval; String get userDiscoverySettingsManualApproval;
/// No description provided for @userDiscoverySettingsManualApprovalDesc. /// No description provided for @userDiscoverySettingsManualApprovalDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Before someone is shared, you\'ll be asked first.'** /// **'Before one of your friends is shared, you will be asked every time.'**
String get userDiscoverySettingsManualApprovalDesc; String get userDiscoverySettingsManualApprovalDesc;
/// No description provided for @onboardingUserDiscoveryLetFriendsFindYou. /// No description provided for @onboardingUserDiscoveryLetFriendsFindYou.
@ -2681,13 +2717,13 @@ abstract class AppLocalizations {
/// No description provided for @verificationBadgeGeneralDesc. /// No description provided for @verificationBadgeGeneralDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'** /// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'**
String get verificationBadgeGeneralDesc; String get verificationBadgeGeneralDesc;
/// No description provided for @verificationBadgeGreenDesc. /// No description provided for @verificationBadgeGreenDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A contact you have *personally* verified.'** /// **'A contact you have *personally verified* using the QR code.'**
String get verificationBadgeGreenDesc; String get verificationBadgeGreenDesc;
/// No description provided for @verificationBadgeYellowDesc. /// No description provided for @verificationBadgeYellowDesc.
@ -2702,6 +2738,42 @@ abstract class AppLocalizations {
/// **'A contact whose identity has *not* yet been verified.'** /// **'A contact whose identity has *not* yet been verified.'**
String get verificationBadgeRedDesc; String get verificationBadgeRedDesc;
/// No description provided for @scanNow.
///
/// In en, this message translates to:
/// **'Scan now'**
String get scanNow;
/// No description provided for @openQrCode.
///
/// In en, this message translates to:
/// **'Open QR code'**
String get openQrCode;
/// No description provided for @deleteVerificationTitle.
///
/// In en, this message translates to:
/// **'Delete verification?'**
String get deleteVerificationTitle;
/// No description provided for @deleteVerificationBody.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete this verification?'**
String get deleteVerificationBody;
/// No description provided for @secretQrTokenVerifiedSnackbar.
///
/// In en, this message translates to:
/// **'{username} has scanned your QR code and is now verified.'**
String secretQrTokenVerifiedSnackbar(Object username);
/// No description provided for @mutualGroupsTitle.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'**
String mutualGroupsTitle(num count);
/// No description provided for @chatEntryFlameRestored. /// No description provided for @chatEntryFlameRestored.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2774,12 +2846,6 @@ abstract class AppLocalizations {
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'** /// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
String get settingsTypingIndicationSubtitle; String get settingsTypingIndicationSubtitle;
/// No description provided for @scanQrOrShow.
///
/// In en, this message translates to:
/// **'Scan / Show QR'**
String get scanQrOrShow;
/// No description provided for @contactActionBlock. /// No description provided for @contactActionBlock.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2834,6 +2900,12 @@ abstract class AppLocalizations {
/// **'Mutual Friends'** /// **'Mutual Friends'**
String get userDiscoverySettingsTitle; String get userDiscoverySettingsTitle;
/// No description provided for @userDiscoveryFeatureOffers.
///
/// In en, this message translates to:
/// **'Your benefits at a glance'**
String get userDiscoveryFeatureOffers;
/// No description provided for @userDiscoveryDisabledLearnMore. /// No description provided for @userDiscoveryDisabledLearnMore.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2906,6 +2978,66 @@ abstract class AppLocalizations {
/// **'Request'** /// **'Request'**
String get friendSuggestionsRequest; String get friendSuggestionsRequest;
/// No description provided for @friendSuggestionsAskFriend.
///
/// In en, this message translates to:
/// **'Ask your friends'**
String get friendSuggestionsAskFriend;
/// No description provided for @askFriendsDialogTitle.
///
/// In en, this message translates to:
/// **'Ask about {username}'**
String askFriendsDialogTitle(Object username);
/// No description provided for @askFriendsDialogDescription.
///
/// In en, this message translates to:
/// **'Select the friends you want to ask about this user:'**
String get askFriendsDialogDescription;
/// No description provided for @askFriendsDialogConfirm.
///
/// In en, this message translates to:
/// **'Ask'**
String get askFriendsDialogConfirm;
/// No description provided for @askFriendsDialogCancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get askFriendsDialogCancel;
/// No description provided for @chatAskAFriendReceivedDescription.
///
/// In en, this message translates to:
/// **'Your friend just got this as a suggestion and wants to know if he knows this person.'**
String get chatAskAFriendReceivedDescription;
/// No description provided for @chatAskAFriendAddedDescription.
///
/// In en, this message translates to:
/// **'You have added this user to your contacts.'**
String get chatAskAFriendAddedDescription;
/// No description provided for @chatAskAFriendHide.
///
/// In en, this message translates to:
/// **'Hide'**
String get chatAskAFriendHide;
/// No description provided for @chatAskAFriendRequest.
///
/// In en, this message translates to:
/// **'Request'**
String get chatAskAFriendRequest;
/// No description provided for @chatAskAFriendUnknownUser.
///
/// In en, this message translates to:
/// **'User {userId}'**
String chatAskAFriendUnknownUser(Object userId);
/// No description provided for @contactUserDiscoveryImagesLeft. /// No description provided for @contactUserDiscoveryImagesLeft.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3050,12 +3182,6 @@ abstract class AppLocalizations {
/// **'Back'** /// **'Back'**
String get back; String get back;
/// No description provided for @onboardingExampleLabel.
///
/// In en, this message translates to:
/// **'Example'**
String get onboardingExampleLabel;
/// No description provided for @makerChangedUsername. /// No description provided for @makerChangedUsername.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3188,12 +3314,6 @@ abstract class AppLocalizations {
/// **'Emoji already used or invalid'** /// **'Emoji already used or invalid'**
String get errorEmojiUsedOrInvalid; String get errorEmojiUsedOrInvalid;
/// No description provided for @subscriptionPledgeTitle.
///
/// In en, this message translates to:
/// **'Support independent privacy.'**
String get subscriptionPledgeTitle;
/// No description provided for @subscriptionPledgeSecureTitle. /// No description provided for @subscriptionPledgeSecureTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3218,17 +3338,309 @@ abstract class AppLocalizations {
/// **'twonly will never show advertisements or sell your private data.'** /// **'twonly will never show advertisements or sell your private data.'**
String get subscriptionPledgeNoAdsDesc; String get subscriptionPledgeNoAdsDesc;
/// No description provided for @subscriptionPledgeFundedTitle. /// No description provided for @subscriptionPledgeSubtitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Independent and funded by Users'** /// **'Zero ads. Total privacy.'**
String get subscriptionPledgeFundedTitle; String get subscriptionPledgeSubtitle;
/// No description provided for @subscriptionPledgeFundedDesc. /// No description provided for @dragToZoom.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'** /// **'Drag to Zoom'**
String get subscriptionPledgeFundedDesc; String get dragToZoom;
/// No description provided for @onboardingProfileSelectionTitle.
///
/// In en, this message translates to:
/// **'Choose your setup path'**
String get onboardingProfileSelectionTitle;
/// No description provided for @onboardingProfileSelectionSubtitle.
///
/// In en, this message translates to:
/// **'Choose how you want to configure your security and privacy settings.'**
String get onboardingProfileSelectionSubtitle;
/// No description provided for @onboardingProfileSelectionDefaultTitle.
///
/// In en, this message translates to:
/// **'Default'**
String get onboardingProfileSelectionDefaultTitle;
/// No description provided for @onboardingProfileSelectionDefaultDesc.
///
/// In en, this message translates to:
/// **'Instantly applies recommended settings so you can start using the app.'**
String get onboardingProfileSelectionDefaultDesc;
/// No description provided for @onboardingProfileSelectionDefaultBadge.
///
/// In en, this message translates to:
/// **'Fast Setup'**
String get onboardingProfileSelectionDefaultBadge;
/// No description provided for @onboardingProfileSelectionCustomizeTitle.
///
/// In en, this message translates to:
/// **'Customize'**
String get onboardingProfileSelectionCustomizeTitle;
/// No description provided for @onboardingProfileSelectionCustomizeDesc.
///
/// In en, this message translates to:
/// **'Step-by-step setup so you can decide for yourself.'**
String get onboardingProfileSelectionCustomizeDesc;
/// No description provided for @onboardingProfileSelectionStrictTitle.
///
/// In en, this message translates to:
/// **'Enhanced Protection'**
String get onboardingProfileSelectionStrictTitle;
/// No description provided for @onboardingProfileSelectionStrictDesc.
///
/// In en, this message translates to:
/// **'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'**
String get onboardingProfileSelectionStrictDesc;
/// No description provided for @replyFlameRestored.
///
/// In en, this message translates to:
/// **'Flames restored'**
String get replyFlameRestored;
/// No description provided for @replyAskAFriend.
///
/// In en, this message translates to:
/// **'Ask a friend'**
String get replyAskAFriend;
/// No description provided for @unverifiedWarningDirectTitle.
///
/// In en, this message translates to:
/// **'Identity not verified in person'**
String get unverifiedWarningDirectTitle;
/// No description provided for @unverifiedWarningGroupTitle.
///
/// In en, this message translates to:
/// **'Not all members are verified in person'**
String get unverifiedWarningGroupTitle;
/// No description provided for @unverifiedWarningBody.
///
/// In en, this message translates to:
/// **'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'**
String get unverifiedWarningBody;
/// No description provided for @unverifiedWarningButton.
///
/// In en, this message translates to:
/// **'Verify now'**
String get unverifiedWarningButton;
/// No description provided for @today.
///
/// In en, this message translates to:
/// **'Today'**
String get today;
/// No description provided for @yesterday.
///
/// In en, this message translates to:
/// **'Yesterday'**
String get yesterday;
/// No description provided for @galleryDisableWarningTitle.
///
/// In en, this message translates to:
/// **'Disable gallery saving?'**
String get galleryDisableWarningTitle;
/// No description provided for @galleryDisableWarningBody.
///
/// In en, this message translates to:
/// **'If you disable this, your media files will not be saved to your gallery and could be permanently lost if twonly is removed or has an issue, as media files are not yet backed up.'**
String get galleryDisableWarningBody;
/// No description provided for @galleryDisableWarningConfirm.
///
/// In en, this message translates to:
/// **'Disable'**
String get galleryDisableWarningConfirm;
/// No description provided for @settingsStorageScanGalleryTitle.
///
/// In en, this message translates to:
/// **'Import from Gallery'**
String get settingsStorageScanGalleryTitle;
/// No description provided for @importGalleryDeselectAll.
///
/// In en, this message translates to:
/// **'Deselect all'**
String get importGalleryDeselectAll;
/// No description provided for @importGallerySelectAll.
///
/// In en, this message translates to:
/// **'Select all'**
String get importGallerySelectAll;
/// No description provided for @importGalleryPermissionRequired.
///
/// In en, this message translates to:
/// **'Permission to access your gallery is required to import previous twonly media files.'**
String get importGalleryPermissionRequired;
/// No description provided for @importGalleryPermissionError.
///
/// In en, this message translates to:
/// **'An error occurred while requesting permission: {error}'**
String importGalleryPermissionError(Object error);
/// No description provided for @importGalleryLoadError.
///
/// In en, this message translates to:
/// **'Failed to load assets: {error}'**
String importGalleryLoadError(Object error);
/// No description provided for @importGalleryImportingOf.
///
/// In en, this message translates to:
/// **'Importing {current} of {total}...'**
String importGalleryImportingOf(Object current, Object total);
/// No description provided for @importGalleryStarting.
///
/// In en, this message translates to:
/// **'Starting import...'**
String get importGalleryStarting;
/// No description provided for @importGalleryComplete.
///
/// In en, this message translates to:
/// **'Import complete: {imported} successfully imported, {duplicated} duplicated and {failed} failed.'**
String importGalleryComplete(
Object imported,
Object duplicated,
Object failed,
);
/// No description provided for @importGalleryGrantAccess.
///
/// In en, this message translates to:
/// **'Grant Access'**
String get importGalleryGrantAccess;
/// No description provided for @importGalleryOpenSettings.
///
/// In en, this message translates to:
/// **'Open Settings'**
String get importGalleryOpenSettings;
/// No description provided for @importGalleryPermissionDenied.
///
/// In en, this message translates to:
/// **'Permission to access gallery denied.'**
String get importGalleryPermissionDenied;
/// No description provided for @importGalleryTryAgain.
///
/// In en, this message translates to:
/// **'Try Again'**
String get importGalleryTryAgain;
/// No description provided for @importGalleryAlbumNotFound.
///
/// In en, this message translates to:
/// **'\"twonly\" album not found'**
String get importGalleryAlbumNotFound;
/// No description provided for @importGalleryAlbumNotFoundDesc.
///
/// In en, this message translates to:
/// **'If you don\'t have this album yet, you can also create it to import photos into twonly.'**
String get importGalleryAlbumNotFoundDesc;
/// No description provided for @importGalleryNoImagesFound.
///
/// In en, this message translates to:
/// **'No images found'**
String get importGalleryNoImagesFound;
/// No description provided for @importGalleryNoImagesFoundDesc.
///
/// In en, this message translates to:
/// **'There are no images on your device.'**
String get importGalleryNoImagesFoundDesc;
/// No description provided for @importGalleryFilterTwonly.
///
/// In en, this message translates to:
/// **'Only show the twonly-Album'**
String get importGalleryFilterTwonly;
/// No description provided for @importGalleryRefresh.
///
/// In en, this message translates to:
/// **'Refresh'**
String get importGalleryRefresh;
/// No description provided for @importGallerySelectToImport.
///
/// In en, this message translates to:
/// **'Select items to import'**
String get importGallerySelectToImport;
/// No description provided for @importGalleryImportCount.
///
/// In en, this message translates to:
/// **'{count, plural, =1{Import 1 item} other{Import {count} items}}'**
String importGalleryImportCount(num count);
/// No description provided for @emptyChatListTitle.
///
/// In en, this message translates to:
/// **'Find your first friend'**
String get emptyChatListTitle;
/// No description provided for @emptyChatListDesc.
///
/// In en, this message translates to:
/// **'Let friends scan your QR code, or share them your profile.'**
String get emptyChatListDesc;
/// No description provided for @emptyChatListShareBtn.
///
/// In en, this message translates to:
/// **'Share your profile'**
String get emptyChatListShareBtn;
/// No description provided for @emptyChatListScanBtn.
///
/// In en, this message translates to:
/// **'QR Code'**
String get emptyChatListScanBtn;
/// No description provided for @emptyChatListAddUsernameBtn.
///
/// In en, this message translates to:
/// **'By Username'**
String get emptyChatListAddUsernameBtn;
/// No description provided for @avatarCustomizeRandomize.
///
/// In en, this message translates to:
/// **'Randomize'**
String get avatarCustomizeRandomize;
/// No description provided for @avatarCustomizeReset.
///
/// In en, this message translates to:
/// **'Reset'**
String get avatarCustomizeReset;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -9,11 +9,7 @@ class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override @override
String get registerTitle => 'Willkommen bei twonly!'; String get registerSlogan => 'Privat in Kontakt bleiben.';
@override
String get registerSlogan =>
'twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.';
@override @override
String get onboardingWelcomeTitle => 'Willkommen bei twonly!'; String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
@ -37,13 +33,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get onboardingFocusBody => String get onboardingFocusBody =>
'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.'; 'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.';
@override
String get onboardingSendTwonliesTitle => 'twonlies senden';
@override
String get onboardingSendTwonliesBody =>
'Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!';
@override @override
String get onboardingNotProductTitle => 'Du bist nicht das Produkt!'; String get onboardingNotProductTitle => 'Du bist nicht das Produkt!';
@ -52,18 +41,13 @@ class AppLocalizationsDe extends AppLocalizations {
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.'; 'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
@override @override
String get onboardingGetStartedTitle => 'Auf geht\'s'; String get registerUsernameSlogan => 'Konto erstellen';
@override
String get registerUsernameSlogan =>
'Bitte wähle einen Benutzernamen, damit dich andere finden können!';
@override @override
String get registerUsernameDecoration => 'Benutzername'; String get registerUsernameDecoration => 'Benutzername';
@override @override
String get registerUsernameLimits => String get registerUsernameLimits => 'Mindestens 3 Zeichen.';
'Der Benutzername muss mindestens 3 Zeichen lang sein.';
@override @override
String get registerProofOfWorkFailed => String get registerProofOfWorkFailed =>
@ -132,18 +116,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get searchUsernameNotFound => 'Benutzername nicht gefunden'; String get searchUsernameNotFound => 'Benutzername nicht gefunden';
@override
String searchUsernameNotFoundBody(Object username) {
return 'Es wurde kein Benutzer mit dem Benutzernamen \"$username\" gefunden.';
}
@override @override
String get searchUsernameNewFollowerTitle => 'Offene Anfragen'; String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
@override
String get chatListViewSearchUserNameBtn =>
'Füge deinen ersten twonly-Kontakt hinzu!';
@override @override
String get chatListDetailInput => 'Nachricht eingeben'; String get chatListDetailInput => 'Nachricht eingeben';
@ -235,10 +210,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern'; String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
@override
String get settingsStorageDataStoreInGSubtitle =>
'Speichere Bilder zusätzlich in der Systemgalerie.';
@override @override
String get settingsStorageDataMediaAutoDownload => String get settingsStorageDataMediaAutoDownload =>
'Automatischer Mediendownload'; 'Automatischer Mediendownload';
@ -249,6 +220,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN'; String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN';
@override
String get settingsStorageManageTitle => 'Speicher verwalten';
@override
String get settingsStorageUsed => 'Speicherplatz belegt';
@override
String get settingsStorageImages => 'Bilder';
@override
String get settingsStorageVideos => 'Videos';
@override
String get settingsStorageGifs => 'GIFs';
@override @override
String get settingsProfileCustomizeAvatar => 'Avatar anpassen'; String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
@ -271,17 +257,41 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsPrivacy => 'Datenschutz & Sicherheit'; String get settingsPrivacy => 'Datenschutz & Sicherheit';
@override @override
String get settingsPrivacyBlockUsers => 'Benutzer blockieren'; String get settingsPrivacyBlockUsers => 'Kontakte blockieren';
@override @override
String get settingsPrivacyBlockUsersDesc => String get settingsPrivacyBlockUsersDesc =>
'Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.'; 'Blockierte Kontakte können nicht mit dir kommunizieren. Du kannst einen blockierten Kontakt jederzeit wieder entsperren.';
@override @override
String settingsPrivacyBlockUsersCount(Object len) { String settingsPrivacyBlockUsersCount(Object len) {
return '$len Kontakt(e)'; return '$len Kontakt(e)';
} }
@override
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
@override
String get securityProfileTitle => 'Sicherheitsprofil';
@override
String get securityProfileSubtitle =>
'Wähle das Schutzniveau, das zu deiner täglichen Nutzung passt. Dies kann jederzeit in den Einstellungen geändert werden.';
@override
String get securityProfileNormalTitle => 'Normaler Schutz';
@override
String get securityProfileNormalDesc =>
'Gute Balance zwischen Komfort und Sicherheit, ohne dich zu sehr einzuschränken.';
@override
String get securityProfileStrictTitle => 'Strikter Schutz';
@override
String get securityProfileStrictDesc =>
'Maximaler Schutz vor Phishing, kann aber unkomfortabel sein.';
@override @override
String get settingsNotification => 'Benachrichtigung'; String get settingsNotification => 'Benachrichtigung';
@ -423,10 +433,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.'; 'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
@override @override
String get contactVerifyNumberTitle => 'Benutzer verifizieren'; String get contactVerifyNumberTitle => 'Kontakte verifizieren';
@override @override
String get userVerifiedTitle => 'Benutzer verifiziert'; String get contactVerifyNumberSubtitle =>
'Überprüfe die Identität deiner Kontakte, um sicherzugehen, dass du mit der richtigen Person schreibst.';
@override
String get userVerifiedTitle => 'Kontakt verifiziert';
@override @override
String contactVerifiedBy(Object username) { String contactVerifiedBy(Object username) {
@ -437,8 +451,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.'; String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
@override @override
String get verificationTypeSecretQrToken => String verificationTypeSecretQrToken(Object username) {
'Die andere Person hat deinen QR-Code gescannt.'; return '$username hat deinen QR-Code gescannt.';
}
@override @override
String get verificationTypeLink => 'Per Link verifiziert.'; String get verificationTypeLink => 'Per Link verifiziert.';
@ -470,10 +485,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactBlockBody => String get contactBlockBody =>
'Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.'; 'Ein blockierter Kontakt kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Kontakts aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Kontakte.';
@override @override
String get contactRemove => 'Benutzer löschen'; String get contactRemove => 'Kontakt löschen';
@override @override
String contactRemoveTitle(Object username) { String contactRemoveTitle(Object username) {
@ -482,7 +497,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.'; 'Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.';
@override @override
String get undo => 'Rückgängig'; String get undo => 'Rückgängig';
@ -572,7 +587,9 @@ class AppLocalizationsDe extends AppLocalizations {
} }
@override @override
String get userFoundBody => 'Möchtest du eine Folgeanfrage stellen?'; String userFoundBody(String username) {
return 'Möchtest du dich mit $username vernetzen?';
}
@override @override
String get errorInternalError => String get errorInternalError =>
@ -713,6 +730,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteOkBtnForAll => 'Für alle löschen'; String get deleteOkBtnForAll => 'Für alle löschen';
@override
String memoriesDeleteSnackbarSuccess(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Elemente erfolgreich gelöscht',
one: '1 Element erfolgreich gelöscht',
);
return '$_temp0';
}
@override @override
String get deleteOkBtnForMe => 'Für mich löschen'; String get deleteOkBtnForMe => 'Für mich löschen';
@ -722,6 +750,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get deleteImageBody => 'Das Bild wird unwiderruflich gelöscht.'; String get deleteImageBody => 'Das Bild wird unwiderruflich gelöscht.';
@override
String deleteMemoriesBody(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Die $count Bilder werden unwiderruflich gelöscht.',
one: 'Das Bild wird unwiderruflich gelöscht.',
);
return '$_temp0';
}
@override @override
String get settingsBackup => 'Backup'; String get settingsBackup => 'Backup';
@ -801,11 +840,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupChangePassword => 'Password ändern'; String get backupChangePassword => 'Password ändern';
@override @override
String get twonlySafeRecoverTitle => 'Recovery'; String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
@override
String get twonlySafeRecoverDesc =>
'Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.';
@override @override
String get twonlySafeRecoverBtn => 'Backup wiederherstellen'; String get twonlySafeRecoverBtn => 'Backup wiederherstellen';
@ -852,7 +887,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get reportUserReason => 'Meldegrund'; String get reportUserReason => 'Meldegrund';
@override @override
String get reportUser => 'Benutzer melden'; String get reportUser => 'Kontakt melden';
@override @override
String get newDeviceRegistered => String get newDeviceRegistered =>
@ -1249,12 +1284,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get draftMessage => 'Entwurf'; String get draftMessage => 'Entwurf';
@override
String get exportMemories => 'Memories exportieren (Beta)';
@override
String get importMemories => 'Memories importieren (Beta)';
@override @override
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen'; String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
@ -1270,6 +1299,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen'; String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
@override
String get addContactQrSheetSubtext =>
'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen';
@override @override
String get finishSetupCardTitle => 'Profil vervollständigen'; String get finishSetupCardTitle => 'Profil vervollständigen';
@ -1323,11 +1356,11 @@ class AppLocalizationsDe extends AppLocalizations {
'Erfahre, wer dich anfragt'; 'Erfahre, wer dich anfragt';
@override @override
String get userDiscoverySettingsManualApproval => 'Manuelle Zustimmung'; String get userDiscoverySettingsManualApproval => 'Vor jedem Teilen fragen';
@override @override
String get userDiscoverySettingsManualApprovalDesc => String get userDiscoverySettingsManualApprovalDesc =>
'Bevor jemand geteilt wird, wirst du zuerst gefragt.'; 'Bevor einer deiner Freunde geteilt wird, wirst du jedes Mal gefragt.';
@override @override
String get onboardingUserDiscoveryLetFriendsFindYou => String get onboardingUserDiscoveryLetFriendsFindYou =>
@ -1495,11 +1528,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get verificationBadgeGeneralDesc => String get verificationBadgeGeneralDesc =>
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.'; 'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.';
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'Ein Kontakt, den du *persönlich verifiziert* hast.'; 'Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
@ -1509,6 +1542,35 @@ class AppLocalizationsDe extends AppLocalizations {
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.'; 'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
@override
String get scanNow => 'Jetzt scannen';
@override
String get openQrCode => 'QR-Code öffnen';
@override
String get deleteVerificationTitle => 'Verifizierung löschen?';
@override
String get deleteVerificationBody =>
'Möchtest du diese Verifizierung wirklich löschen?';
@override
String secretQrTokenVerifiedSnackbar(Object username) {
return '$username hat deinen QR-Code gescannt und ist nun verifiziert.';
}
@override
String mutualGroupsTitle(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count gemeinsame Gruppen',
one: '1 gemeinsame Gruppe',
);
return '$_temp0';
}
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {
return '$count Flammen wiederhergestellt'; return '$count Flammen wiederhergestellt';
@ -1554,9 +1616,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsTypingIndicationSubtitle => String get settingsTypingIndicationSubtitle =>
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.'; 'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
@override
String get scanQrOrShow => 'QR scannen / anzeigen';
@override @override
String get contactActionBlock => 'Blockieren'; String get contactActionBlock => 'Blockieren';
@ -1588,6 +1647,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde'; String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
@override
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
@override @override
String get userDiscoveryDisabledLearnMore => 'Mehr erfahren'; String get userDiscoveryDisabledLearnMore => 'Mehr erfahren';
@ -1631,6 +1693,43 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get friendSuggestionsRequest => 'Anfragen'; String get friendSuggestionsRequest => 'Anfragen';
@override
String get friendSuggestionsAskFriend => 'Deine Freunde fragen';
@override
String askFriendsDialogTitle(Object username) {
return 'Nach $username fragen';
}
@override
String get askFriendsDialogDescription =>
'Wähle die Freunde aus, die du zu diesem Nutzer fragen möchtest:';
@override
String get askFriendsDialogConfirm => 'Fragen';
@override
String get askFriendsDialogCancel => 'Abbrechen';
@override
String get chatAskAFriendReceivedDescription =>
'Dein Freund hat diesen Nutzer als Vorschlag erhalten und möchte wissen, ob er diese Person kennt.';
@override
String get chatAskAFriendAddedDescription =>
'Du hast diesen Nutzer zu deinen Kontakten hinzugefügt.';
@override
String get chatAskAFriendHide => 'Ausblenden';
@override
String get chatAskAFriendRequest => 'Anfragen';
@override
String chatAskAFriendUnknownUser(Object userId) {
return 'Nutzer $userId';
}
@override @override
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) { String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.'; return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.';
@ -1715,9 +1814,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get back => 'Zurück'; String get back => 'Zurück';
@override
String get onboardingExampleLabel => 'Beispiel';
@override @override
String makerChangedUsername(Object maker, Object oldName, Object newName) { String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.'; return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
@ -1798,9 +1894,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get errorEmojiUsedOrInvalid => String get errorEmojiUsedOrInvalid =>
'Emoji wird bereits verwendet oder ist ungültig'; 'Emoji wird bereits verwendet oder ist ungültig';
@override
String get subscriptionPledgeTitle => 'Unterstütze unabhängigen Datenschutz.';
@override @override
String get subscriptionPledgeSecureTitle => 'Secure by Design'; String get subscriptionPledgeSecureTitle => 'Secure by Design';
@ -1816,10 +1909,185 @@ class AppLocalizationsDe extends AppLocalizations {
'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.'; 'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.';
@override @override
String get subscriptionPledgeFundedTitle => String get subscriptionPledgeSubtitle => 'Keine Werbung. Volle Privatsphäre.';
'Unabhängig und durch Nutzer finanziert';
@override @override
String get subscriptionPledgeFundedDesc => String get dragToZoom => 'Zum Zoomen ziehen';
'twonly wird rein durch Nutzer-Abonnements finanziert, um unsere Unabhängigkeit und die Zukunft von twonly zu sichern.';
@override
String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg';
@override
String get onboardingProfileSelectionSubtitle =>
'Wähle aus, wie du deine Sicherheits- und Privatsphäre-Einstellungen konfigurieren möchtest.';
@override
String get onboardingProfileSelectionDefaultTitle => 'Standard';
@override
String get onboardingProfileSelectionDefaultDesc =>
'Wendet sofort die empfohlenen Einstellungen an, damit du die App direkt nutzen kannst.';
@override
String get onboardingProfileSelectionDefaultBadge => 'Schnelles Setup';
@override
String get onboardingProfileSelectionCustomizeTitle => 'Anpassen';
@override
String get onboardingProfileSelectionCustomizeDesc =>
'Schritt-für-Schritt-Einrichtung, damit du selbst entscheiden kannst.';
@override
String get onboardingProfileSelectionStrictTitle => 'Erhöhter Schutz';
@override
String get onboardingProfileSelectionStrictDesc =>
'Maximaler Schutz vor Phishing. Empfohlen für *Journalisten & Personen des öffentlichen Lebens*.';
@override
String get replyFlameRestored => 'Flammen wiederhergestellt';
@override
String get replyAskAFriend => 'Einen Freund fragen';
@override
String get unverifiedWarningDirectTitle =>
'Identität nicht persönlich verifiziert';
@override
String get unverifiedWarningGroupTitle =>
'Nicht alle Mitglieder sind persönlich verifiziert';
@override
String get unverifiedWarningBody =>
'*Teile keine geheimen Daten*. Jemand könnte sich *als dein Freund ausgeben*.';
@override
String get unverifiedWarningButton => 'Jetzt verifizieren';
@override
String get today => 'Heute';
@override
String get yesterday => 'Gestern';
@override
String get galleryDisableWarningTitle => 'Galeriespeicherung deaktivieren?';
@override
String get galleryDisableWarningBody =>
'Wenn du dies deaktivierst, werden deine Mediendateien nicht in deiner Galerie gespeichert und könnten dauerhaft verloren gehen, wenn twonly deinstalliert wird oder ein Problem auftritt, da Mediendateien noch nicht in Backups enthalten sind.';
@override
String get galleryDisableWarningConfirm => 'Deaktivieren';
@override
String get settingsStorageScanGalleryTitle => 'Aus Galerie importieren';
@override
String get importGalleryDeselectAll => 'Alle abwählen';
@override
String get importGallerySelectAll => 'Alle auswählen';
@override
String get importGalleryPermissionRequired =>
'Zugriff auf deine Galerie ist erforderlich, um frühere twonly-Mediendateien zu importieren.';
@override
String importGalleryPermissionError(Object error) {
return 'Beim Anfordern der Berechtigung ist ein Fehler aufgetreten: $error';
}
@override
String importGalleryLoadError(Object error) {
return 'Laden der Medien fehlgeschlagen: $error';
}
@override
String importGalleryImportingOf(Object current, Object total) {
return '$current von $total wird importiert...';
}
@override
String get importGalleryStarting => 'Import wird gestartet...';
@override
String importGalleryComplete(
Object imported,
Object duplicated,
Object failed,
) {
return 'Import abgeschlossen: $imported erfolgreich importiert, $duplicated Duplikate und $failed fehlgeschlagen.';
}
@override
String get importGalleryGrantAccess => 'Zugriff erlauben';
@override
String get importGalleryOpenSettings => 'Einstellungen öffnen';
@override
String get importGalleryPermissionDenied => 'Zugriff auf Galerie verweigert.';
@override
String get importGalleryTryAgain => 'Erneut versuchen';
@override
String get importGalleryAlbumNotFound => '\"twonly\"-Album nicht gefunden';
@override
String get importGalleryAlbumNotFoundDesc =>
'Falls du dieses Album noch nicht hast, kannst du es auch erstellen, um Fotos in twonly zu importieren.';
@override
String get importGalleryNoImagesFound => 'Keine Bilder gefunden';
@override
String get importGalleryNoImagesFoundDesc =>
'Es befinden sich keine Bilder auf deinem Gerät.';
@override
String get importGalleryFilterTwonly => 'Nur das twonly-Album anzeigen';
@override
String get importGalleryRefresh => 'Aktualisieren';
@override
String get importGallerySelectToImport =>
'Elemente zum Importieren auswählen';
@override
String importGalleryImportCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Elemente importieren',
one: '1 Element importieren',
);
return '$_temp0';
}
@override
String get emptyChatListTitle => 'Finde deinen ersten Freund';
@override
String get emptyChatListDesc =>
'Lass Freunde deinen QR-Code scannen oder teile dein Profil mit ihnen.';
@override
String get emptyChatListShareBtn => 'Profil teilen';
@override
String get emptyChatListScanBtn => 'QR-Code';
@override
String get emptyChatListAddUsernameBtn => 'Per Benutzername';
@override
String get avatarCustomizeRandomize => 'Zufällig';
@override
String get avatarCustomizeReset => 'Zurücksetzen';
} }

View file

@ -9,11 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override @override
String get registerTitle => 'Welcome to twonly!'; String get registerSlogan => 'Stay in touch privately.';
@override
String get registerSlogan =>
'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing';
@override @override
String get onboardingWelcomeTitle => 'Welcome to twonly!'; String get onboardingWelcomeTitle => 'Welcome to twonly!';
@ -36,13 +32,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingFocusBody => String get onboardingFocusBody =>
'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'; 'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.';
@override
String get onboardingSendTwonliesTitle => 'Send twonlies';
@override
String get onboardingSendTwonliesBody =>
'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!';
@override @override
String get onboardingNotProductTitle => 'You are not the product!'; String get onboardingNotProductTitle => 'You are not the product!';
@ -51,18 +40,13 @@ class AppLocalizationsEn extends AppLocalizations {
'twonly is financed by donations and an optional subscription. Your data will never be sold.'; 'twonly is financed by donations and an optional subscription. Your data will never be sold.';
@override @override
String get onboardingGetStartedTitle => 'Let\'s go!'; String get registerUsernameSlogan => 'Create your account';
@override
String get registerUsernameSlogan =>
'Please select a username so others can find you!';
@override @override
String get registerUsernameDecoration => 'Username'; String get registerUsernameDecoration => 'Username';
@override @override
String get registerUsernameLimits => String get registerUsernameLimits => 'At least 3 characters.';
'Your username must be at least 3 characters long.';
@override @override
String get registerProofOfWorkFailed => String get registerProofOfWorkFailed =>
@ -131,17 +115,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get searchUsernameNotFound => 'Username not found'; String get searchUsernameNotFound => 'Username not found';
@override
String searchUsernameNotFoundBody(Object username) {
return 'There is no user with the username \"$username\" registered';
}
@override @override
String get searchUsernameNewFollowerTitle => 'Open requests'; String get searchUsernameNewFollowerTitle => 'Open requests';
@override
String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!';
@override @override
String get chatListDetailInput => 'Type a message'; String get chatListDetailInput => 'Type a message';
@ -232,10 +208,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsStorageDataStoreInGTitle => 'Store in Gallery'; String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
@override
String get settingsStorageDataStoreInGSubtitle =>
'Store saved images additional in the systems gallery.';
@override @override
String get settingsStorageDataMediaAutoDownload => 'Media auto-download'; String get settingsStorageDataMediaAutoDownload => 'Media auto-download';
@ -245,6 +217,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsStorageDataAutoDownWifi => 'When using WI-FI'; String get settingsStorageDataAutoDownWifi => 'When using WI-FI';
@override
String get settingsStorageManageTitle => 'Manage storage';
@override
String get settingsStorageUsed => 'Storage used';
@override
String get settingsStorageImages => 'Images';
@override
String get settingsStorageVideos => 'Videos';
@override
String get settingsStorageGifs => 'GIFs';
@override @override
String get settingsProfileCustomizeAvatar => 'Customize your avatar'; String get settingsProfileCustomizeAvatar => 'Customize your avatar';
@ -267,17 +254,41 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsPrivacy => 'Privacy & Security'; String get settingsPrivacy => 'Privacy & Security';
@override @override
String get settingsPrivacyBlockUsers => 'Block users'; String get settingsPrivacyBlockUsers => 'Block contacts';
@override @override
String get settingsPrivacyBlockUsersDesc => String get settingsPrivacyBlockUsersDesc =>
'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'; 'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.';
@override @override
String settingsPrivacyBlockUsersCount(Object len) { String settingsPrivacyBlockUsersCount(Object len) {
return '$len contact(s)'; return '$len contact(s)';
} }
@override
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
@override
String get securityProfileTitle => 'Security Profile';
@override
String get securityProfileSubtitle =>
'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.';
@override
String get securityProfileNormalTitle => 'Normal Protection';
@override
String get securityProfileNormalDesc =>
'Good balance between a convenient mode without bothering you too much.';
@override
String get securityProfileStrictTitle => 'Strict Protection';
@override
String get securityProfileStrictDesc =>
'Maximum anti-phishing protection but may be inconvenient.';
@override @override
String get settingsNotification => 'Notification'; String get settingsNotification => 'Notification';
@ -418,10 +429,14 @@ class AppLocalizationsEn extends AppLocalizations {
'Your account will be deleted. There is no change to restore it.'; 'Your account will be deleted. There is no change to restore it.';
@override @override
String get contactVerifyNumberTitle => 'Verify contact'; String get contactVerifyNumberTitle => 'Verify contacts';
@override @override
String get userVerifiedTitle => 'User verified'; String get contactVerifyNumberSubtitle =>
'Verify the identity of your contacts to make sure you are texting the right person.';
@override
String get userVerifiedTitle => 'Contact verified';
@override @override
String contactVerifiedBy(Object username) { String contactVerifiedBy(Object username) {
@ -432,8 +447,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get verificationTypeQrScanned => 'You scanned their QR code.'; String get verificationTypeQrScanned => 'You scanned their QR code.';
@override @override
String get verificationTypeSecretQrToken => String verificationTypeSecretQrToken(Object username) {
'The other person scanned your QR code.'; return '$username has scanned your QR code.';
}
@override @override
String get verificationTypeLink => 'Verified via link.'; String get verificationTypeLink => 'Verified via link.';
@ -465,10 +481,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactBlockBody => String get contactBlockBody =>
'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'; 'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.';
@override @override
String get contactRemove => 'Remove user'; String get contactRemove => 'Remove contact';
@override @override
String contactRemoveTitle(Object username) { String contactRemoveTitle(Object username) {
@ -477,7 +493,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'; 'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.';
@override @override
String get undo => 'Undo'; String get undo => 'Undo';
@ -567,7 +583,9 @@ class AppLocalizationsEn extends AppLocalizations {
} }
@override @override
String get userFoundBody => 'Do you want to create a follow request?'; String userFoundBody(String username) {
return 'Do you want to connect with $username?';
}
@override @override
String get errorInternalError => String get errorInternalError =>
@ -707,6 +725,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteOkBtnForAll => 'Delete for all'; String get deleteOkBtnForAll => 'Delete for all';
@override
String memoriesDeleteSnackbarSuccess(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Deleted $count items successfully',
one: 'Deleted 1 item successfully',
);
return '$_temp0';
}
@override @override
String get deleteOkBtnForMe => 'Delete for me'; String get deleteOkBtnForMe => 'Delete for me';
@ -716,6 +745,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get deleteImageBody => 'The image will be irrevocably deleted.'; String get deleteImageBody => 'The image will be irrevocably deleted.';
@override
String deleteMemoriesBody(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'The $count images will be irrevocably deleted.',
one: 'The image will be irrevocably deleted.',
);
return '$_temp0';
}
@override @override
String get settingsBackup => 'Backup'; String get settingsBackup => 'Backup';
@ -795,11 +835,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get backupChangePassword => 'Change password'; String get backupChangePassword => 'Change password';
@override @override
String get twonlySafeRecoverTitle => 'Recovery'; String get twonlySafeRecoverTitle => 'Restore backup';
@override
String get twonlySafeRecoverDesc =>
'If you have created a backup with twonly Backup, you can restore it here.';
@override @override
String get twonlySafeRecoverBtn => 'Restore backup'; String get twonlySafeRecoverBtn => 'Restore backup';
@ -846,7 +882,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get reportUserReason => 'Reporting reason'; String get reportUserReason => 'Reporting reason';
@override @override
String get reportUser => 'Report user'; String get reportUser => 'Report contact';
@override @override
String get newDeviceRegistered => String get newDeviceRegistered =>
@ -1240,12 +1276,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get draftMessage => 'Draft'; String get draftMessage => 'Draft';
@override
String get exportMemories => 'Export memories (Beta)';
@override
String get importMemories => 'Import memories (Beta)';
@override @override
String get voiceMessageSlideToCancel => 'Slide to cancel'; String get voiceMessageSlideToCancel => 'Slide to cancel';
@ -1261,6 +1291,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get openYourOwnQRcode => 'Open your own QR code'; String get openYourOwnQRcode => 'Open your own QR code';
@override
String get addContactQrSheetSubtext =>
'Let a friend scan this QR code to add you';
@override @override
String get finishSetupCardTitle => 'Complete your profile'; String get finishSetupCardTitle => 'Complete your profile';
@ -1314,11 +1348,12 @@ class AppLocalizationsEn extends AppLocalizations {
'Be informed about who is requesting'; 'Be informed about who is requesting';
@override @override
String get userDiscoverySettingsManualApproval => 'Manual approval'; String get userDiscoverySettingsManualApproval =>
'Ask every time before sharing';
@override @override
String get userDiscoverySettingsManualApprovalDesc => String get userDiscoverySettingsManualApprovalDesc =>
'Before someone is shared, you\'ll be asked first.'; 'Before one of your friends is shared, you will be asked every time.';
@override @override
String get onboardingUserDiscoveryLetFriendsFindYou => String get onboardingUserDiscoveryLetFriendsFindYou =>
@ -1480,11 +1515,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get verificationBadgeGeneralDesc => String get verificationBadgeGeneralDesc =>
'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'; 'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.';
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'A contact you have *personally* verified.'; 'A contact you have *personally verified* using the QR code.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
@ -1494,6 +1529,35 @@ class AppLocalizationsEn extends AppLocalizations {
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'A contact whose identity has *not* yet been verified.'; 'A contact whose identity has *not* yet been verified.';
@override
String get scanNow => 'Scan now';
@override
String get openQrCode => 'Open QR code';
@override
String get deleteVerificationTitle => 'Delete verification?';
@override
String get deleteVerificationBody =>
'Are you sure you want to delete this verification?';
@override
String secretQrTokenVerifiedSnackbar(Object username) {
return '$username has scanned your QR code and is now verified.';
}
@override
String mutualGroupsTitle(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count mutual groups',
one: '1 mutual group',
);
return '$_temp0';
}
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {
return '$count flames restored'; return '$count flames restored';
@ -1539,9 +1603,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsTypingIndicationSubtitle => String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.'; 'When the typing indicator is turned off, you can\'t see when others are typing a message.';
@override
String get scanQrOrShow => 'Scan / Show QR';
@override @override
String get contactActionBlock => 'Block'; String get contactActionBlock => 'Block';
@ -1573,6 +1634,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get userDiscoverySettingsTitle => 'Mutual Friends'; String get userDiscoverySettingsTitle => 'Mutual Friends';
@override
String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
@override @override
String get userDiscoveryDisabledLearnMore => 'Learn more'; String get userDiscoveryDisabledLearnMore => 'Learn more';
@ -1616,6 +1680,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get friendSuggestionsRequest => 'Request'; String get friendSuggestionsRequest => 'Request';
@override
String get friendSuggestionsAskFriend => 'Ask your friends';
@override
String askFriendsDialogTitle(Object username) {
return 'Ask about $username';
}
@override
String get askFriendsDialogDescription =>
'Select the friends you want to ask about this user:';
@override
String get askFriendsDialogConfirm => 'Ask';
@override
String get askFriendsDialogCancel => 'Cancel';
@override
String get chatAskAFriendReceivedDescription =>
'Your friend just got this as a suggestion and wants to know if he knows this person.';
@override
String get chatAskAFriendAddedDescription =>
'You have added this user to your contacts.';
@override
String get chatAskAFriendHide => 'Hide';
@override
String get chatAskAFriendRequest => 'Request';
@override
String chatAskAFriendUnknownUser(Object userId) {
return 'User $userId';
}
@override @override
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) { String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
return '$imagesLeft more images are needed until your friends are shared with $username.'; return '$imagesLeft more images are needed until your friends are shared with $username.';
@ -1700,9 +1801,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get back => 'Back'; String get back => 'Back';
@override
String get onboardingExampleLabel => 'Example';
@override @override
String makerChangedUsername(Object maker, Object oldName, Object newName) { String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker changed their username from $oldName to $newName.'; return '$maker changed their username from $oldName to $newName.';
@ -1782,9 +1880,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid'; String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
@override
String get subscriptionPledgeTitle => 'Support independent privacy.';
@override @override
String get subscriptionPledgeSecureTitle => 'Secure by Design'; String get subscriptionPledgeSecureTitle => 'Secure by Design';
@ -1800,9 +1895,184 @@ class AppLocalizationsEn extends AppLocalizations {
'twonly will never show advertisements or sell your private data.'; 'twonly will never show advertisements or sell your private data.';
@override @override
String get subscriptionPledgeFundedTitle => 'Independent and funded by Users'; String get subscriptionPledgeSubtitle => 'Zero ads. Total privacy.';
@override @override
String get subscriptionPledgeFundedDesc => String get dragToZoom => 'Drag to Zoom';
'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.';
@override
String get onboardingProfileSelectionTitle => 'Choose your setup path';
@override
String get onboardingProfileSelectionSubtitle =>
'Choose how you want to configure your security and privacy settings.';
@override
String get onboardingProfileSelectionDefaultTitle => 'Default';
@override
String get onboardingProfileSelectionDefaultDesc =>
'Instantly applies recommended settings so you can start using the app.';
@override
String get onboardingProfileSelectionDefaultBadge => 'Fast Setup';
@override
String get onboardingProfileSelectionCustomizeTitle => 'Customize';
@override
String get onboardingProfileSelectionCustomizeDesc =>
'Step-by-step setup so you can decide for yourself.';
@override
String get onboardingProfileSelectionStrictTitle => 'Enhanced Protection';
@override
String get onboardingProfileSelectionStrictDesc =>
'Maximum anti-phishing defense. Recommended for *journalists & public figures*.';
@override
String get replyFlameRestored => 'Flames restored';
@override
String get replyAskAFriend => 'Ask a friend';
@override
String get unverifiedWarningDirectTitle => 'Identity not verified in person';
@override
String get unverifiedWarningGroupTitle =>
'Not all members are verified in person';
@override
String get unverifiedWarningBody =>
'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.';
@override
String get unverifiedWarningButton => 'Verify now';
@override
String get today => 'Today';
@override
String get yesterday => 'Yesterday';
@override
String get galleryDisableWarningTitle => 'Disable gallery saving?';
@override
String get galleryDisableWarningBody =>
'If you disable this, your media files will not be saved to your gallery and could be permanently lost if twonly is removed or has an issue, as media files are not yet backed up.';
@override
String get galleryDisableWarningConfirm => 'Disable';
@override
String get settingsStorageScanGalleryTitle => 'Import from Gallery';
@override
String get importGalleryDeselectAll => 'Deselect all';
@override
String get importGallerySelectAll => 'Select all';
@override
String get importGalleryPermissionRequired =>
'Permission to access your gallery is required to import previous twonly media files.';
@override
String importGalleryPermissionError(Object error) {
return 'An error occurred while requesting permission: $error';
}
@override
String importGalleryLoadError(Object error) {
return 'Failed to load assets: $error';
}
@override
String importGalleryImportingOf(Object current, Object total) {
return 'Importing $current of $total...';
}
@override
String get importGalleryStarting => 'Starting import...';
@override
String importGalleryComplete(
Object imported,
Object duplicated,
Object failed,
) {
return 'Import complete: $imported successfully imported, $duplicated duplicated and $failed failed.';
}
@override
String get importGalleryGrantAccess => 'Grant Access';
@override
String get importGalleryOpenSettings => 'Open Settings';
@override
String get importGalleryPermissionDenied =>
'Permission to access gallery denied.';
@override
String get importGalleryTryAgain => 'Try Again';
@override
String get importGalleryAlbumNotFound => '\"twonly\" album not found';
@override
String get importGalleryAlbumNotFoundDesc =>
'If you don\'t have this album yet, you can also create it to import photos into twonly.';
@override
String get importGalleryNoImagesFound => 'No images found';
@override
String get importGalleryNoImagesFoundDesc =>
'There are no images on your device.';
@override
String get importGalleryFilterTwonly => 'Only show the twonly-Album';
@override
String get importGalleryRefresh => 'Refresh';
@override
String get importGallerySelectToImport => 'Select items to import';
@override
String importGalleryImportCount(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Import $count items',
one: 'Import 1 item',
);
return '$_temp0';
}
@override
String get emptyChatListTitle => 'Find your first friend';
@override
String get emptyChatListDesc =>
'Let friends scan your QR code, or share them your profile.';
@override
String get emptyChatListShareBtn => 'Share your profile';
@override
String get emptyChatListScanBtn => 'QR Code';
@override
String get emptyChatListAddUsernameBtn => 'By Username';
@override
String get avatarCustomizeRandomize => 'Randomize';
@override
String get avatarCustomizeReset => 'Reset';
} }

@ -1 +1 @@
Subproject commit f649128fd875a12f23518ff2641190cc129a9339 Subproject commit 673f6d8c3036d64060b1114912bd5bf5515d5420

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:twonly/src/services/profile.service.dart';
part 'userdata.model.g.dart'; part 'userdata.model.g.dart';
@JsonSerializable() @JsonSerializable()
@ -10,6 +11,7 @@ class UserData {
required this.displayName, required this.displayName,
required this.subscriptionPlan, required this.subscriptionPlan,
required this.currentSetupPage, required this.currentSetupPage,
required this.appVersion,
}); });
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json); _$UserDataFromJson(json);
@ -35,6 +37,12 @@ class UserData {
@JsonKey(defaultValue: 0) @JsonKey(defaultValue: 0)
int deviceId = 0; int deviceId = 0;
@JsonKey(defaultValue: SetupProfile.standard)
SetupProfile setupProfile = SetupProfile.standard;
@JsonKey(defaultValue: SecurityProfile.normal)
SecurityProfile securityProfile = SecurityProfile.normal;
// --- SUBSCRIPTION DTA --- // --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: 'Free') @JsonKey(defaultValue: 'Free')
@ -57,6 +65,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool requestedAudioPermission = false; bool requestedAudioPermission = false;
@JsonKey(defaultValue: false)
bool enableDatabaseLogging = false;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool automaticallyMarkEqualMediaFilesAsOpened = false; bool automaticallyMarkEqualMediaFilesAsOpened = false;
@ -76,8 +87,8 @@ class UserData {
Map<String, List<String>>? autoDownloadOptions; Map<String, List<String>>? autoDownloadOptions;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: true)
bool storeMediaFilesInGallery = false; bool storeMediaFilesInGallery = true;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool autoStoreAllSendUnlimitedMediaFiles = false; bool autoStoreAllSendUnlimitedMediaFiles = false;
@ -103,8 +114,8 @@ class UserData {
@JsonKey(defaultValue: 4) @JsonKey(defaultValue: 4)
int requiredSendImages = 4; int requiredSendImages = 4;
@JsonKey(defaultValue: 2) @JsonKey(defaultValue: 3)
int userDiscoveryThreshold = 2; int userDiscoveryThreshold = 3;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool userDiscoveryRequiresManualApproval = false; bool userDiscoveryRequiresManualApproval = false;
@ -165,6 +176,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool skipSetupPages = false; bool skipSetupPages = false;
@JsonKey(defaultValue: false)
bool hasZoomed = false;
Map<String, dynamic> toJson() => _$UserDataToJson(this); Map<String, dynamic> toJson() => _$UserDataToJson(this);
} }

View file

@ -13,13 +13,22 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
currentSetupPage: json['currentSetupPage'] as String?, currentSetupPage: json['currentSetupPage'] as String?,
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
) )
..avatarSvg = json['avatarSvg'] as String? ..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String? ..avatarJson = json['avatarJson'] as String?
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..isDeveloper = json['isDeveloper'] as bool? ?? false ..isDeveloper = json['isDeveloper'] as bool? ?? false
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
..setupProfile =
$enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ??
SetupProfile.standard
..securityProfile =
$enumDecodeNullable(
_$SecurityProfileEnumMap,
json['securityProfile'],
) ??
SecurityProfile.normal
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
..lastImageSend = json['lastImageSend'] == null ..lastImageSend = json['lastImageSend'] == null
? null ? null
@ -33,6 +42,9 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false
..automaticallyMarkEqualMediaFilesAsOpened =
json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false
..videoStabilizationEnabled = ..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? true json['videoStabilizationEnabled'] as bool? ?? true
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
@ -50,7 +62,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
), ),
) )
..storeMediaFilesInGallery = ..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? false json['storeMediaFilesInGallery'] as bool? ?? true
..autoStoreAllSendUnlimitedMediaFiles = ..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..typingIndicators = json['typingIndicators'] as bool? ?? true ..typingIndicators = json['typingIndicators'] as bool? ?? true
@ -66,7 +78,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['isUserDiscoveryEnabled'] as bool? ?? false json['isUserDiscoveryEnabled'] as bool? ?? false
..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4 ..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4
..userDiscoveryThreshold = ..userDiscoveryThreshold =
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 2 (json['userDiscoveryThreshold'] as num?)?.toInt() ?? 3
..userDiscoveryRequiresManualApproval = ..userDiscoveryRequiresManualApproval =
json['userDiscoveryRequiresManualApproval'] as bool? ?? false json['userDiscoveryRequiresManualApproval'] as bool? ?? false
..userDiscoverySharePromotion = ..userDiscoverySharePromotion =
@ -100,7 +112,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null ..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
? null ? null
: DateTime.parse(json['lastUserStudyDataUpload'] as String) : DateTime.parse(json['lastUserStudyDataUpload'] as String)
..skipSetupPages = json['skipSetupPages'] as bool? ?? false; ..skipSetupPages = json['skipSetupPages'] as bool? ?? false
..hasZoomed = json['hasZoomed'] as bool? ?? false;
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId, 'userId': instance.userId,
@ -112,6 +125,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'isDeveloper': instance.isDeveloper, 'isDeveloper': instance.isDeveloper,
'deviceId': instance.deviceId, 'deviceId': instance.deviceId,
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
'lastImageSend': instance.lastImageSend?.toIso8601String(), 'lastImageSend': instance.lastImageSend?.toIso8601String(),
@ -121,6 +136,9 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'enableDatabaseLogging': instance.enableDatabaseLogging,
'automaticallyMarkEqualMediaFilesAsOpened':
instance.automaticallyMarkEqualMediaFilesAsOpened,
'videoStabilizationEnabled': instance.videoStabilizationEnabled, 'videoStabilizationEnabled': instance.videoStabilizationEnabled,
'showFeedbackShortcut': instance.showFeedbackShortcut, 'showFeedbackShortcut': instance.showFeedbackShortcut,
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending, 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
@ -160,6 +178,18 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
?.toIso8601String(), ?.toIso8601String(),
'currentSetupPage': instance.currentSetupPage, 'currentSetupPage': instance.currentSetupPage,
'skipSetupPages': instance.skipSetupPages, 'skipSetupPages': instance.skipSetupPages,
'hasZoomed': instance.hasZoomed,
};
const _$SetupProfileEnumMap = {
SetupProfile.standard: 'standard',
SetupProfile.customized: 'customized',
SetupProfile.maximum: 'maximum',
};
const _$SecurityProfileEnumMap = {
SecurityProfile.normal: 'normal',
SecurityProfile.strict: 'strict',
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {

View file

@ -11,10 +11,12 @@ message AdditionalMessageData {
LINK = 0; LINK = 0;
CONTACTS = 1; CONTACTS = 1;
RESTORED_FLAME_COUNTER = 2; RESTORED_FLAME_COUNTER = 2;
ASK_ABOUT_USER = 3;
} }
Type type = 1; Type type = 1;
optional string link = 2; optional string link = 2;
repeated SharedContact contacts = 3; repeated SharedContact contacts = 3;
optional int64 restored_flame_counter = 4; optional int64 restored_flame_counter = 4;
optional int64 ask_about_user_id = 5;
} }

View file

@ -105,6 +105,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
$core.String? link, $core.String? link,
$core.Iterable<SharedContact>? contacts, $core.Iterable<SharedContact>? contacts,
$fixnum.Int64? restoredFlameCounter, $fixnum.Int64? restoredFlameCounter,
$fixnum.Int64? askAboutUserId,
}) { }) {
final result = create(); final result = create();
if (type != null) result.type = type; if (type != null) result.type = type;
@ -112,6 +113,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
if (contacts != null) result.contacts.addAll(contacts); if (contacts != null) result.contacts.addAll(contacts);
if (restoredFlameCounter != null) if (restoredFlameCounter != null)
result.restoredFlameCounter = restoredFlameCounter; result.restoredFlameCounter = restoredFlameCounter;
if (askAboutUserId != null) result.askAboutUserId = askAboutUserId;
return result; return result;
} }
@ -133,6 +135,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
..pPM<SharedContact>(3, _omitFieldNames ? '' : 'contacts', ..pPM<SharedContact>(3, _omitFieldNames ? '' : 'contacts',
subBuilder: SharedContact.create) subBuilder: SharedContact.create)
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter') ..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
..aInt64(5, _omitFieldNames ? '' : 'askAboutUserId')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -184,6 +187,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
$core.bool hasRestoredFlameCounter() => $_has(3); $core.bool hasRestoredFlameCounter() => $_has(3);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearRestoredFlameCounter() => $_clearField(4); void clearRestoredFlameCounter() => $_clearField(4);
@$pb.TagNumber(5)
$fixnum.Int64 get askAboutUserId => $_getI64(4);
@$pb.TagNumber(5)
set askAboutUserId($fixnum.Int64 value) => $_setInt64(4, value);
@$pb.TagNumber(5)
$core.bool hasAskAboutUserId() => $_has(4);
@$pb.TagNumber(5)
void clearAskAboutUserId() => $_clearField(5);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -22,16 +22,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER = static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
AdditionalMessageData_Type._( AdditionalMessageData_Type._(
2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER'); 2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER');
static const AdditionalMessageData_Type ASK_ABOUT_USER =
AdditionalMessageData_Type._(3, _omitEnumNames ? '' : 'ASK_ABOUT_USER');
static const $core.List<AdditionalMessageData_Type> values = static const $core.List<AdditionalMessageData_Type> values =
<AdditionalMessageData_Type>[ <AdditionalMessageData_Type>[
LINK, LINK,
CONTACTS, CONTACTS,
RESTORED_FLAME_COUNTER, RESTORED_FLAME_COUNTER,
ASK_ABOUT_USER,
]; ];
static final $core.List<AdditionalMessageData_Type?> _byValue = static final $core.List<AdditionalMessageData_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2); $pb.ProtobufEnum.$_initByValueList(values, 3);
static AdditionalMessageData_Type? valueOf($core.int value) => static AdditionalMessageData_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -67,11 +67,21 @@ const AdditionalMessageData$json = {
'10': 'restoredFlameCounter', '10': 'restoredFlameCounter',
'17': true '17': true
}, },
{
'1': 'ask_about_user_id',
'3': 5,
'4': 1,
'5': 3,
'9': 2,
'10': 'askAboutUserId',
'17': true
},
], ],
'4': [AdditionalMessageData_Type$json], '4': [AdditionalMessageData_Type$json],
'8': [ '8': [
{'1': '_link'}, {'1': '_link'},
{'1': '_restored_flame_counter'}, {'1': '_restored_flame_counter'},
{'1': '_ask_about_user_id'},
], ],
}; };
@ -82,6 +92,7 @@ const AdditionalMessageData_Type$json = {
{'1': 'LINK', '2': 0}, {'1': 'LINK', '2': 0},
{'1': 'CONTACTS', '2': 1}, {'1': 'CONTACTS', '2': 1},
{'1': 'RESTORED_FLAME_COUNTER', '2': 2}, {'1': 'RESTORED_FLAME_COUNTER', '2': 2},
{'1': 'ASK_ABOUT_USER', '2': 3},
], ],
}; };
@ -90,6 +101,7 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW' 'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD' 'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX' 'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI' 'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQESLgoRYXNrX2Fib3V0X3VzZXJfaWQY'
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3' 'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQEiTgoEVHlwZRIICgRMSU5LEAASDAoIQ09OVEFDVF'
'RvcmVkX2ZsYW1lX2NvdW50ZXI='); 'MQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAISEgoOQVNLX0FCT1VUX1VTRVIQA0IHCgVf'
'bGlua0IZChdfcmVzdG9yZWRfZmxhbWVfY291bnRlckIUChJfYXNrX2Fib3V0X3VzZXJfaWQ=');

View file

@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage {
$core.List<$core.int>? signedPrekeySignature, $core.List<$core.int>? signedPrekeySignature,
$fixnum.Int64? signedPrekeyId, $fixnum.Int64? signedPrekeyId,
$core.List<$core.int>? secretVerificationToken, $core.List<$core.int>? secretVerificationToken,
$fixnum.Int64? timestamp,
}) { }) {
final result = create(); final result = create();
if (userId != null) result.userId = userId; if (userId != null) result.userId = userId;
@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage {
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId; if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
if (secretVerificationToken != null) if (secretVerificationToken != null)
result.secretVerificationToken = secretVerificationToken; result.secretVerificationToken = secretVerificationToken;
if (timestamp != null) result.timestamp = timestamp;
return result; return result;
} }
@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage {
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId') ..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY) 8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
..aInt64(9, _omitFieldNames ? '' : 'timestamp')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -230,6 +233,15 @@ class PublicProfile extends $pb.GeneratedMessage {
$core.bool hasSecretVerificationToken() => $_has(7); $core.bool hasSecretVerificationToken() => $_has(7);
@$pb.TagNumber(8) @$pb.TagNumber(8)
void clearSecretVerificationToken() => $_clearField(8); void clearSecretVerificationToken() => $_clearField(8);
@$pb.TagNumber(9)
$fixnum.Int64 get timestamp => $_getI64(8);
@$pb.TagNumber(9)
set timestamp($fixnum.Int64 value) => $_setInt64(8, value);
@$pb.TagNumber(9)
$core.bool hasTimestamp() => $_has(8);
@$pb.TagNumber(9)
void clearTimestamp() => $_clearField(9);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -77,9 +77,19 @@ const PublicProfile$json = {
'10': 'secretVerificationToken', '10': 'secretVerificationToken',
'17': true '17': true
}, },
{
'1': 'timestamp',
'3': 9,
'4': 1,
'5': 3,
'9': 1,
'10': 'timestamp',
'17': true
},
], ],
'8': [ '8': [
{'1': '_secret_verification_token'}, {'1': '_secret_verification_token'},
{'1': '_timestamp'},
], ],
}; };
@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY' 'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg' 'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl' '5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu'); 'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX'
'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA==');

View file

@ -17,4 +17,5 @@ message PublicProfile {
bytes signed_prekey_signature = 6; bytes signed_prekey_signature = 6;
int64 signed_prekey_id = 7; int64 signed_prekey_id = 7;
optional bytes secret_verification_token = 8; optional bytes secret_verification_token = 8;
optional int64 timestamp = 9;
} }

View file

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
@ -21,10 +22,11 @@ import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart';
import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/import_from_gallery.view.dart';
import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/manage_storage.view.dart';
import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/visual/views/settings/developer/developer.view.dart'; import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
import 'package:twonly/src/visual/views/settings/developer/informations.view.dart';
import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart'; import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart'; import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart';
import 'package:twonly/src/visual/views/settings/help/changelog.view.dart'; import 'package:twonly/src/visual/views/settings/help/changelog.view.dart';
@ -37,6 +39,7 @@ import 'package:twonly/src/visual/views/settings/help/help.view.dart';
import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart';
import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/profile_selection.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
@ -46,7 +49,10 @@ import 'package:twonly/src/visual/views/settings/subscription/subscription.view.
import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
final routerProvider = GoRouter( final routerProvider = GoRouter(
navigatorKey: rootNavigatorKey,
routes: [ routes: [
GoRoute( GoRoute(
path: Routes.home, path: Routes.home,
@ -200,6 +206,10 @@ final routerProvider = GoRouter(
path: 'user_discovery', path: 'user_discovery',
builder: (context, state) => const UserDiscoverySettingsView(), builder: (context, state) => const UserDiscoverySettingsView(),
), ),
GoRoute(
path: 'profile_selection',
builder: (context, state) => const ProfileSelectionSettingsView(),
),
], ],
), ),
GoRoute( GoRoute(
@ -211,12 +221,12 @@ final routerProvider = GoRouter(
builder: (context, state) => const DataAndStorageView(), builder: (context, state) => const DataAndStorageView(),
routes: [ routes: [
GoRoute( GoRoute(
path: 'import', path: 'manage',
builder: (context, state) => const ImportMediaView(), builder: (context, state) => const ManageStorageView(),
), ),
GoRoute( GoRoute(
path: 'export', path: 'import_gallery',
builder: (context, state) => const ExportMediaView(), builder: (context, state) => const ImportFromGalleryView(),
), ),
], ],
), ),
@ -279,6 +289,10 @@ final routerProvider = GoRouter(
path: 'automated_testing', path: 'automated_testing',
builder: (context, state) => const AutomatedTestingView(), builder: (context, state) => const AutomatedTestingView(),
), ),
GoRoute(
path: 'informations',
builder: (context, state) => const DeveloperInformationsView(),
),
GoRoute( GoRoute(
path: 'reduce_flames', path: 'reduce_flames',
builder: (context, state) => const ReduceFlamesView(), builder: (context, state) => const ReduceFlamesView(),

View file

@ -0,0 +1,25 @@
import 'package:flutter/services.dart';
class AndroidPhotoPickerService {
static const MethodChannel _channel = MethodChannel('eu.twonly/photo_picker');
/// Launches the native Android Photo Picker and returns a list of URIs.
static Future<List<String>> pickImages() async {
try {
final result = await _channel.invokeListMethod<String>('pickImages');
return result ?? [];
} catch (e) {
return [];
}
}
/// Reads the raw bytes from a content URI using the Android ContentResolver.
static Future<Uint8List?> getUriBytes(String uri) async {
try {
final bytes = await _channel.invokeMethod<Uint8List>('getUriBytes', {'uri': uri});
return bytes;
} catch (e) {
return null;
}
}
}

View file

@ -18,7 +18,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
@ -97,19 +96,27 @@ class ApiService {
Uri.parse(apiUrl), Uri.parse(apiUrl),
pingInterval: const Duration(seconds: 30), pingInterval: const Duration(seconds: 30),
); );
try {
await channel.ready.timeout(const Duration(seconds: 10));
} catch (e) {
channel.sink.close().ignore();
rethrow;
}
_channel = channel; _channel = channel;
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
await _channel!.ready;
Log.info('websocket connected to $apiUrl'); Log.info('websocket connected to $apiUrl');
return true; return true;
} catch (_) { } catch (e) {
_channel = null;
return false; return false;
} }
} }
// Function is called after the user is authenticated at the server // Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async { Future<void> onAuthenticated() async {
await initFCMAfterAuthenticated(); await FcmNotificationService.initFCMAfterAuthenticated();
_connectionStateController.add(true); _connectionStateController.add(true);
if (AppState.isInBackgroundTask) { if (AppState.isInBackgroundTask) {
@ -149,6 +156,7 @@ class ApiService {
} }
Future<void> onClosed() async { Future<void> onClosed() async {
if (_channel == null) return;
Log.info('websocket connection closed'); Log.info('websocket connection closed');
_channel = null; _channel = null;
isAuthenticated = false; isAuthenticated = false;
@ -180,15 +188,19 @@ class ApiService {
_reconnectionDelay = 3; _reconnectionDelay = 3;
} }
Future<void> close(Function callback) async { Future<void> close(Function? callback) async {
Log.info('closing websocket connection'); Log.info('closing websocket connection');
if (_channel != null) { if (_channel != null) {
await _channel!.sink.close(); try {
await _channel!.sink.close().timeout(const Duration(seconds: 2));
} catch (e) {
Log.warn('Timeout or error closing websocket: $e');
}
await onClosed(); await onClosed();
callback(); callback?.call();
return; return;
} }
callback(); callback?.call();
} }
Future<void> listenToNetworkChanges() async { Future<void> listenToNetworkChanges() async {
@ -246,7 +258,10 @@ class ApiService {
Future<void> _onData(dynamic msgBuffer) async { Future<void> _onData(dynamic msgBuffer) async {
try { try {
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List); if (msgBuffer is! Uint8List) {
msgBuffer = Uint8List.fromList(msgBuffer as List<int>);
}
final msg = server.ServerToClient.fromBuffer(msgBuffer);
if (msg.v0.hasResponse()) { if (msg.v0.hasResponse()) {
final completer = _pendingRequests.remove(msg.v0.seq); final completer = _pendingRequests.remove(msg.v0.seq);
if (completer != null && !completer.isCompleted) { if (completer != null && !completer.isCompleted) {
@ -426,7 +441,7 @@ class ApiService {
Future<bool> tryAuthenticateWithToken() async { Future<bool> tryAuthenticateWithToken() async {
final apiAuthToken = await SecureStorage.instance.read( final apiAuthToken = await SecureStorage.instance.read(
key: SecureStorageKeys.apiAuthToken, key: 'api_auth_token',
); );
if (apiAuthToken != null) { if (apiAuthToken != null) {
@ -464,7 +479,7 @@ class ApiService {
Log.info('Switch was successfully.'); Log.info('Switch was successfully.');
await UserService.update((u) => u.canUseLoginTokenForAuth = true); await UserService.update((u) => u.canUseLoginTokenForAuth = true);
await SecureStorage.instance.delete( await SecureStorage.instance.delete(
key: SecureStorageKeys.apiAuthToken, key: 'api_auth_token',
); );
} }
} catch (e) { } catch (e) {
@ -586,7 +601,7 @@ class ApiService {
final apiAuthTokenB64 = base64Encode(apiAuthToken); final apiAuthTokenB64 = base64Encode(apiAuthToken);
await SecureStorage.instance.write( await SecureStorage.instance.write(
key: SecureStorageKeys.apiAuthToken, key: 'api_auth_token',
value: apiAuthTokenB64, value: apiAuthTokenB64,
); );

View file

@ -10,9 +10,10 @@ Future<void> handleAdditionalDataMessage(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_AdditionalDataMessage message, EncryptedContent_AdditionalDataMessage message,
String receiptId,
) async { ) async {
Log.info( Log.info(
'Got a additional data message: ${message.senderMessageId} from $groupId', '[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId',
); );
// Prevent message overwrite: reject if a message with this ID already // Prevent message overwrite: reject if a message with this ID already
@ -22,7 +23,7 @@ Future<void> handleAdditionalDataMessage(
.getSingleOrNull(); .getSingleOrNull();
if (existing != null && existing.senderId != fromUserId) { if (existing != null && existing.senderId != fromUserId) {
Log.warn( Log.warn(
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
); );
return; return;
} }
@ -45,6 +46,6 @@ Future<void> handleAdditionalDataMessage(
fromTimestamp(message.timestamp), fromTimestamp(message.timestamp),
); );
if (msg != null) { if (msg != null) {
Log.info('Inserted a new text message with ID: ${msg.messageId}'); Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}');
} }
} }

View file

@ -28,7 +28,6 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
await handleContactAccept(fromUserId); await handleContactAccept(fromUserId);
} }
// contact was already accepted, so just accept the request in the background.
await sendCipherText( await sendCipherText(
contact.userId, contact.userId,
EncryptedContent( EncryptedContent(
@ -36,6 +35,7 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
type: EncryptedContent_ContactRequest_Type.ACCEPT, type: EncryptedContent_ContactRequest_Type.ACCEPT,
), ),
), ),
blocking: false,
); );
return true; return true;
} }
@ -88,16 +88,17 @@ Future<void> handleContactAccept(int fromUserId) async {
Future<bool> handleContactRequest( Future<bool> handleContactRequest(
int fromUserId, int fromUserId,
EncryptedContent_ContactRequest contactRequest, EncryptedContent_ContactRequest contactRequest,
String receiptId,
) async { ) async {
switch (contactRequest.type) { switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST: case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId'); Log.info('[$receiptId] Got a contact request from $fromUserId');
return handleNewContactRequest(fromUserId); return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT: case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId'); Log.info('[$receiptId] Got a contact accept from $fromUserId');
await handleContactAccept(fromUserId); await handleContactAccept(fromUserId);
case EncryptedContent_ContactRequest_Type.REJECT: case EncryptedContent_ContactRequest_Type.REJECT:
Log.info('Got a contact reject from $fromUserId'); Log.info('[$receiptId] Got a contact reject from $fromUserId');
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
fromUserId, fromUserId,
const ContactsCompanion( const ContactsCompanion(
@ -114,14 +115,15 @@ Future<void> handleContactUpdate(
int fromUserId, int fromUserId,
EncryptedContent_ContactUpdate contactUpdate, EncryptedContent_ContactUpdate contactUpdate,
int? senderProfileCounter, int? senderProfileCounter,
String receiptId,
) async { ) async {
switch (contactUpdate.type) { switch (contactUpdate.type) {
case EncryptedContent_ContactUpdate_Type.REQUEST: case EncryptedContent_ContactUpdate_Type.REQUEST:
Log.info('Got a contact update request from $fromUserId'); Log.info('[$receiptId] Got a contact update request from $fromUserId');
await sendContactMyProfileData(fromUserId); await sendContactMyProfileData(fromUserId);
case EncryptedContent_ContactUpdate_Type.UPDATE: case EncryptedContent_ContactUpdate_Type.UPDATE:
Log.info('Got a contact update $fromUserId'); Log.info('[$receiptId] Got a contact update $fromUserId');
Uint8List? avatarSvgCompressed; Uint8List? avatarSvgCompressed;
if (contactUpdate.hasAvatarSvgCompressed()) { if (contactUpdate.hasAvatarSvgCompressed()) {
avatarSvgCompressed = Uint8List.fromList( avatarSvgCompressed = Uint8List.fromList(
@ -188,8 +190,9 @@ Future<void> handleContactUpdate(
Future<void> handleFlameSync( Future<void> handleFlameSync(
String groupId, String groupId,
EncryptedContent_FlameSync flameSync, EncryptedContent_FlameSync flameSync,
String receiptId,
) async { ) async {
Log.info('Got a flameSync for group $groupId'); Log.info('[$receiptId] Got a flameSync for group $groupId');
final group = await twonlyDB.groupsDao.getGroup(groupId); final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null || group.lastFlameCounterChange == null) return; if (group == null || group.lastFlameCounterChange == null) return;
@ -235,6 +238,7 @@ Future<int?> checkForProfileUpdate(
type: EncryptedContent_ContactUpdate_Type.REQUEST, type: EncryptedContent_ContactUpdate_Type.REQUEST,
), ),
), ),
blocking: false,
); );
} }
} }

View file

@ -8,8 +8,9 @@ import 'package:twonly/src/utils/log.dart';
Future<void> handleErrorMessage( Future<void> handleErrorMessage(
int fromUserId, int fromUserId,
EncryptedContent_ErrorMessages error, EncryptedContent_ErrorMessages error,
String receiptId,
) async { ) async {
Log.error('Got error from $fromUserId: $error'); Log.error('[$receiptId] Got error from $fromUserId: $error');
switch (error.type) { switch (error.type) {
case EncryptedContent_ErrorMessages_Type case EncryptedContent_ErrorMessages_Type

View file

@ -15,14 +15,13 @@ Future<void> handleGroupCreate(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_GroupCreate newGroup, EncryptedContent_GroupCreate newGroup,
String receiptId,
) async { ) async {
final user = await twonlyDB.contactsDao final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull();
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) { if (user == null) {
// Only contacts can invite other contacts, so this can (via the UI) not happen. // Only contacts can invite other contacts, so this can (via the UI) not happen.
Log.error( Log.error(
'User is not a contact. Aborting.', '[$receiptId] User is not a contact. Aborting.',
); );
return; return;
} }
@ -66,7 +65,7 @@ Future<void> handleGroupCreate(
if (group == null) { if (group == null) {
Log.error( Log.error(
'Could not create new group. Probably because the group already existed.', '[$receiptId] Could not create new group. Probably because the group already existed.',
); );
return; return;
} }
@ -108,12 +107,13 @@ Future<void> handleGroupUpdate(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_GroupUpdate update, EncryptedContent_GroupUpdate update,
String receiptId,
) async { ) async {
Log.info('Got group update for $groupId from $fromUserId'); Log.info('[$receiptId] Got group update for $groupId from $fromUserId');
final actionType = groupActionTypeFromString(update.groupActionType); final actionType = groupActionTypeFromString(update.groupActionType);
if (actionType == null) { if (actionType == null) {
Log.error('Group action ${update.groupActionType} is unknown ignoring.'); Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.');
return; return;
} }
@ -189,10 +189,11 @@ Future<bool> handleGroupJoin(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_GroupJoin join, EncryptedContent_GroupJoin join,
String receiptId,
) async { ) async {
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
if (!await addNewHiddenContact(fromUserId)) { if (!await addNewHiddenContact(fromUserId)) {
Log.error('Got group join, but could not load contact.'); Log.error('[$receiptId] Got group join, but could not load contact.');
// This can happen in case the group join was received before the group create. // This can happen in case the group join was received before the group create.
// In this case return false, which will cause the receipt to fail and the user // In this case return false, which will cause the receipt to fail and the user
// will resend this message. // will resend this message.
@ -213,6 +214,7 @@ Future<void> handleResendGroupPublicKey(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_GroupJoin join, EncryptedContent_GroupJoin join,
String receiptId,
) async { ) async {
final group = await twonlyDB.groupsDao.getGroup(groupId); final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null || group.myGroupPrivateKey == null) return; if (group == null || group.myGroupPrivateKey == null) return;
@ -225,6 +227,7 @@ Future<void> handleResendGroupPublicKey(
groupPublicKey: keyPair.getPublicKey().serialize(), groupPublicKey: keyPair.getPublicKey().serialize(),
), ),
), ),
blocking: false,
); );
} }
@ -232,6 +235,7 @@ Future<void> handleTypingIndicator(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_TypingIndicator indicator, EncryptedContent_TypingIndicator indicator,
String receiptId,
) async { ) async {
var lastTypeIndicator = const Value<DateTime?>.absent(); var lastTypeIndicator = const Value<DateTime?>.absent();

View file

@ -18,9 +18,10 @@ Future<void> handleMedia(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_Media media, EncryptedContent_Media media,
String receiptId,
) async { ) async {
Log.info( Log.info(
'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', '[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
); );
late MediaType mediaType; late MediaType mediaType;
@ -33,7 +34,7 @@ Future<void> handleMedia(
message.senderId != fromUserId || message.senderId != fromUserId ||
message.mediaId == null) { message.mediaId == null) {
Log.warn( Log.warn(
'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', '[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
); );
return; return;
} }
@ -82,13 +83,13 @@ Future<void> handleMedia(
if (messageTmp != null) { if (messageTmp != null) {
if (messageTmp.senderId != fromUserId) { if (messageTmp.senderId != fromUserId) {
Log.warn( Log.warn(
'$fromUserId tried to modify the message from ${messageTmp.senderId}.', '[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
); );
return; return;
} }
if (messageTmp.mediaId == null) { if (messageTmp.mediaId == null) {
Log.warn( Log.warn(
'This message already exit without a mediaId. Message is dropped.', '[$receiptId] This message already exit without a mediaId. Message is dropped.',
); );
return; return;
} }
@ -97,7 +98,7 @@ Future<void> handleMedia(
); );
if (mediaFile?.downloadState != DownloadState.reuploadRequested) { if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
Log.warn( Log.warn(
'This message and media file already exit and was not requested again. Dropping it.', '[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
); );
return; return;
} }
@ -121,7 +122,9 @@ Future<void> handleMedia(
MediaFile? mediaFile; MediaFile? mediaFile;
Message? message; Message? message;
Log.info('Starting transaction for media message ${media.senderMessageId}'); Log.info(
'[$receiptId] Starting transaction for media message ${media.senderMessageId}',
);
await twonlyDB.transaction(() async { await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion( MediaFilesCompanion(
@ -141,7 +144,7 @@ Future<void> handleMedia(
); );
if (mediaFile == null) { if (mediaFile == null) {
Log.error('Could not insert media file into database'); Log.error('[$receiptId] Could not insert media file into database');
return; return;
} }
@ -165,7 +168,7 @@ Future<void> handleMedia(
); );
}); });
Log.info( Log.info(
'Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}', '[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
); );
if (message != null && mediaFile != null) { if (message != null && mediaFile != null) {
@ -173,7 +176,9 @@ Future<void> handleMedia(
groupId, groupId,
fromTimestamp(media.timestamp), fromTimestamp(media.timestamp),
); );
Log.info('Inserted a new media message with ID: ${message!.messageId}'); Log.info(
'[$receiptId] Inserted a new media message with ID: ${message!.messageId}',
);
await incFlameCounter( await incFlameCounter(
message!.groupId, message!.groupId,
true, true,
@ -184,12 +189,16 @@ Future<void> handleMedia(
} else { } else {
if (mediaFile == null && message == null) { if (mediaFile == null && message == null) {
Log.error( Log.error(
'Could not insert new message as both the message and mediaFile are empty.', '[$receiptId] Could not insert new message as both the message and mediaFile are empty.',
); );
} else if (mediaFile == null) { } else if (mediaFile == null) {
Log.error('Could not insert new message as the mediaFile is empty.'); Log.error(
'[$receiptId] Could not insert new message as the mediaFile is empty.',
);
} else { } else {
Log.error('Could not insert new message as the message is empty.'); Log.error(
'[$receiptId] Could not insert new message as the message is empty.',
);
} }
} }
} }
@ -197,6 +206,7 @@ Future<void> handleMedia(
Future<void> handleMediaUpdate( Future<void> handleMediaUpdate(
int fromUserId, int fromUserId,
EncryptedContent_MediaUpdate mediaUpdate, EncryptedContent_MediaUpdate mediaUpdate,
String receiptId,
) async { ) async {
final message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageById(mediaUpdate.targetMessageId) .getMessageById(mediaUpdate.targetMessageId)
@ -204,14 +214,14 @@ Future<void> handleMediaUpdate(
if (message == null) { if (message == null) {
// this can happen, in case the message was already deleted. // this can happen, in case the message was already deleted.
Log.info( Log.info(
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.', '[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
); );
return; return;
} }
if (message.mediaId == null) { if (message.mediaId == null) {
// this can happen, in case the message was already deleted. // this can happen, in case the message was already deleted.
Log.warn( Log.warn(
'Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.', '[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
); );
return; return;
} }
@ -220,14 +230,14 @@ Future<void> handleMediaUpdate(
); );
if (mediaFile == null) { if (mediaFile == null) {
Log.info( Log.info(
'Got media file update, but media file was not found ${message.mediaId}', '[$receiptId] Got media file update, but media file was not found ${message.mediaId}',
); );
return; return;
} }
switch (mediaUpdate.type) { switch (mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED: case EncryptedContent_MediaUpdate_Type.REOPENED:
Log.info('Got media file reopened ${mediaFile.mediaId}'); Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}');
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
message.messageId, message.messageId,
const MessagesCompanion( const MessagesCompanion(
@ -235,7 +245,7 @@ Future<void> handleMediaUpdate(
), ),
); );
case EncryptedContent_MediaUpdate_Type.STORED: case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('Got media file stored ${mediaFile.mediaId}'); Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}');
final mediaService = MediaFileService(mediaFile); final mediaService = MediaFileService(mediaFile);
await mediaService.storeMediaFile(); await mediaService.storeMediaFile();
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
@ -246,7 +256,9 @@ Future<void> handleMediaUpdate(
); );
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}'); Log.info(
'[$receiptId] Got media file decryption error ${mediaFile.mediaId}',
);
await reuploadMediaFile(fromUserId, mediaFile, message.messageId); await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:drift/drift.dart' show Value;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
@ -6,26 +7,27 @@ import 'package:twonly/src/utils/log.dart';
Future<void> handleMessageUpdate( Future<void> handleMessageUpdate(
int contactId, int contactId,
EncryptedContent_MessageUpdate messageUpdate, EncryptedContent_MessageUpdate messageUpdate,
String receiptId,
) async { ) async {
switch (messageUpdate.type) { switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED: case EncryptedContent_MessageUpdate_Type.OPENED:
Log.info( Log.info(
'Opened message ${messageUpdate.multipleTargetMessageIds}', '[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}',
); );
try { try {
await twonlyDB.messagesDao.handleMessagesOpened( await twonlyDB.messagesDao.handleMessagesOpened(
contactId, Value(contactId),
messageUpdate.multipleTargetMessageIds, messageUpdate.multipleTargetMessageIds,
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
} catch (e) { } catch (e) {
Log.warn(e); Log.warn('[$receiptId] Error handling messages opened: $e');
} }
case EncryptedContent_MessageUpdate_Type.DELETE: case EncryptedContent_MessageUpdate_Type.DELETE:
if (!await isSender(contactId, messageUpdate.senderMessageId)) { if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
return; return;
} }
Log.info('Delete message ${messageUpdate.senderMessageId}'); Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}');
try { try {
await twonlyDB.messagesDao.handleMessageDeletion( await twonlyDB.messagesDao.handleMessageDeletion(
contactId, contactId,
@ -33,13 +35,13 @@ Future<void> handleMessageUpdate(
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
} catch (e) { } catch (e) {
Log.warn(e); Log.warn('[$receiptId] Error handling message deletion: $e');
} }
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
if (!await isSender(contactId, messageUpdate.senderMessageId)) { if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
return; return;
} }
Log.info('Edit message ${messageUpdate.senderMessageId}'); Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}');
try { try {
await twonlyDB.messagesDao.handleTextEdit( await twonlyDB.messagesDao.handleTextEdit(
contactId, contactId,
@ -48,12 +50,12 @@ Future<void> handleMessageUpdate(
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
} catch (e) { } catch (e) {
Log.warn(e); Log.warn('[$receiptId] Error handling text edit: $e');
} }
} }
} }
Future<bool> isSender(int fromUserId, String messageId) async { Future<bool> isSender(int fromUserId, String messageId, String receiptId) async {
final message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageById(messageId) .getMessageById(messageId)
.getSingleOrNull(); .getSingleOrNull();
@ -61,6 +63,6 @@ Future<bool> isSender(int fromUserId, String messageId) async {
if (message.senderId == fromUserId) { if (message.senderId == fromUserId) {
return true; return true;
} }
Log.error('Contact $fromUserId tried to modify the message $messageId'); Log.error('[$receiptId] Contact $fromUserId tried to modify the message $messageId');
return false; return false;
} }

View file

@ -10,10 +10,11 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
Future<void> handlePushKey( Future<void> handlePushKey(
int contactId, int contactId,
EncryptedContent_PushKeys pushKeys, EncryptedContent_PushKeys pushKeys,
String receiptId,
) async { ) async {
switch (pushKeys.type) { switch (pushKeys.type) {
case EncryptedContent_PushKeys_Type.REQUEST: case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('Got a pushkey request from $contactId'); Log.info('[$receiptId] Got a pushkey request from $contactId');
if (lastPushKeyRequest.isBefore( if (lastPushKeyRequest.isBefore(
clock.now().subtract(const Duration(seconds: 60)), clock.now().subtract(const Duration(seconds: 60)),
)) { )) {
@ -22,7 +23,7 @@ Future<void> handlePushKey(
} }
case EncryptedContent_PushKeys_Type.UPDATE: case EncryptedContent_PushKeys_Type.UPDATE:
Log.info('Got a pushkey update from $contactId'); Log.info('[$receiptId] Got a pushkey update from $contactId');
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
} }
} }

View file

@ -8,8 +8,12 @@ Future<void> handleReaction(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_Reaction reaction, EncryptedContent_Reaction reaction,
String receiptId,
) async { ) async {
Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})'); Log.info(
'[$receiptId] Got a reaction from for ${reaction.targetMessageId} (remove=${reaction.remove})',
);
await twonlyDB.reactionsDao.updateReaction( await twonlyDB.reactionsDao.updateReaction(
fromUserId, fromUserId,
reaction.targetMessageId, reaction.targetMessageId,

View file

@ -11,9 +11,10 @@ Future<void> handleTextMessage(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_TextMessage textMessage, EncryptedContent_TextMessage textMessage,
String receiptId,
) async { ) async {
Log.info( Log.info(
'Got a text message: ${textMessage.senderMessageId} from $groupId', '[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId',
); );
// Prevent message overwrite: reject if a message with this ID already // Prevent message overwrite: reject if a message with this ID already
@ -23,7 +24,7 @@ Future<void> handleTextMessage(
.getSingleOrNull(); .getSingleOrNull();
if (existing != null && existing.senderId != fromUserId) { if (existing != null && existing.senderId != fromUserId) {
Log.warn( Log.warn(
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
); );
return; return;
} }
@ -47,6 +48,6 @@ Future<void> handleTextMessage(
fromTimestamp(textMessage.timestamp), fromTimestamp(textMessage.timestamp),
); );
if (message != null) { if (message != null) {
Log.info('Inserted a new text message with ID: ${message.messageId}'); Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}');
} }
} }

View file

@ -15,7 +15,9 @@ void resetUserDiscoveryRequestUpdates() {
Future<void> checkForUserDiscoveryChanges( Future<void> checkForUserDiscoveryChanges(
int fromUserId, int fromUserId,
List<int> receivedVersion, List<int> receivedVersion,
String receiptId,
) async { ) async {
Log.info('[$receiptId] Checking for a new user discovery version.');
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages( final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
fromUserId, fromUserId,
receivedVersion, receivedVersion,
@ -26,7 +28,7 @@ Future<void> checkForUserDiscoveryChanges(
// Only request a new version once per app session // Only request a new version once per app session
return; return;
} }
Log.info('Having old version from contact. Requesting new version.'); Log.info('[$receiptId] Having old version from contact. Requesting new version.');
_requestedUpdates.add(fromUserId); _requestedUpdates.add(fromUserId);
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
@ -35,6 +37,7 @@ Future<void> checkForUserDiscoveryChanges(
currentVersion: currentVersion.toList(), currentVersion: currentVersion.toList(),
), ),
), ),
blocking: false,
); );
} }
} }
@ -42,18 +45,19 @@ Future<void> checkForUserDiscoveryChanges(
Future<void> handleUserDiscoveryRequest( Future<void> handleUserDiscoveryRequest(
int fromUserId, int fromUserId,
EncryptedContent_UserDiscoveryRequest request, EncryptedContent_UserDiscoveryRequest request,
String receiptId,
) async { ) async {
Log.info('Got a user discovery request'); Log.info('[$receiptId] Got a user discovery request');
if (!userService.currentUser.isUserDiscoveryEnabled) { if (!userService.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery request while it is disabled'); Log.warn('[$receiptId] Got a user discovery request while it is disabled');
return; return;
} }
final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (!UserDiscoveryService.isContactAllowed(contact)) { if (!UserDiscoveryService.isContactAllowed(contact)) {
Log.warn( Log.warn(
'Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}', '[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
); );
return; return;
} }
@ -63,7 +67,7 @@ Future<void> handleUserDiscoveryRequest(
request.currentVersion, request.currentVersion,
); );
if (newMessages != null && newMessages.isNotEmpty) { if (newMessages != null && newMessages.isNotEmpty) {
Log.info('Sending ${newMessages.length} user discovery messages'); Log.info('[$receiptId] Sending ${newMessages.length} user discovery messages');
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(
@ -71,21 +75,23 @@ Future<void> handleUserDiscoveryRequest(
messages: newMessages, messages: newMessages,
), ),
), ),
blocking: false,
); );
} else { } else {
Log.info('Got update request, but there are no new updates for the user'); Log.info('[$receiptId] Got update request, but there are no new updates for the user');
} }
} }
Future<void> handleUserDiscoveryUpdate( Future<void> handleUserDiscoveryUpdate(
int fromUserId, int fromUserId,
EncryptedContent_UserDiscoveryUpdate update, EncryptedContent_UserDiscoveryUpdate update,
String receiptId,
) async { ) async {
if (!userService.currentUser.isUserDiscoveryEnabled) { if (!userService.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery update while it is disabled'); Log.warn('[$receiptId] Got a user discovery update while it is disabled');
return; return;
} }
Log.info('Got ${update.messages.length} user discovery messages'); Log.info('[$receiptId] Got ${update.messages.length} user discovery messages');
await UserDiscoveryService.handleNewMessages( await UserDiscoveryService.handleNewMessages(
fromUserId, fromUserId,
update.messages.map(Uint8List.fromList).toList(), update.messages.map(Uint8List.fromList).toList(),

View file

@ -252,7 +252,7 @@ Future<void> downloadFileFast(
} else { } else {
if (response.statusCode == 404 || response.statusCode == 403) { if (response.statusCode == 404 || response.statusCode == 403) {
Log.error( Log.error(
'Got ${response.statusCode} from server. Requesting upload again', 'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
); );
// Message was deleted from the server. Requesting it again from the sender to upload it again... // Message was deleted from the server. Requesting it again from the sender to upload it again...
await requestMediaReupload(media.mediaId); await requestMediaReupload(media.mediaId);

View file

@ -35,7 +35,6 @@ Future<void> _protectMediaUpload(
) async { ) async {
final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new); final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new);
await mutex.protect(action); await mutex.protect(action);
_uploadMutexes.remove(mediaId);
} }
Future<void> reuploadMediaFiles() async { Future<void> reuploadMediaFiles() async {
@ -52,7 +51,7 @@ Future<void> reuploadMediaFiles() async {
final contacts = <int, Contact>{}; final contacts = <int, Contact>{};
for (var receipt in receipts) { for (final receipt in receipts) {
if (receipt.retryCount > 1 && receipt.lastRetry != null) { if (receipt.retryCount > 1 && receipt.lastRetry != null) {
final twentyFourHoursAgo = DateTime.now().subtract( final twentyFourHoursAgo = DateTime.now().subtract(
const Duration(hours: 6), const Duration(hours: 6),
@ -65,20 +64,6 @@ Future<void> reuploadMediaFiles() async {
} }
} }
if (receipt.retryCount >= 2) {
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received, but the app was closed before the message was fully processed. Because the receipt was already stored, subsequent retries were detected as duplicates and rejected.
final oldReceiptId = receipt.receiptId;
final updatedReceipt = await twonlyDB.receiptsDao.rotateReceiptId(
oldReceiptId,
);
if (updatedReceipt == null) continue;
Log.info(
'Changed receiptId $oldReceiptId to ${updatedReceipt.receiptId} as retryCount is ${receipt.retryCount}',
);
receipt = updatedReceipt;
}
var messageId = receipt.messageId; var messageId = receipt.messageId;
if (receipt.messageId == null) { if (receipt.messageId == null) {
Log.info('Message not in receipt. Loading it from the content.'); Log.info('Message not in receipt. Loading it from the content.');
@ -414,6 +399,9 @@ Future<void> insertMediaFileInMessagesTable(
); );
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
if (message != null) { if (message != null) {
Log.info(
'Created message ${message.messageId} for media ${message.mediaId}',
);
// de-archive contact when sending a new message // de-archive contact when sending a new message
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
message.groupId, message.groupId,
@ -445,6 +433,10 @@ Future<void> _startBackgroundMediaUploadInternal(
if (mediaService.mediaFile.uploadState == UploadState.initialized || if (mediaService.mediaFile.uploadState == UploadState.initialized ||
mediaService.mediaFile.uploadState == UploadState.preprocessing) { mediaService.mediaFile.uploadState == UploadState.preprocessing) {
Log.info(
'Hanlding media file ${mediaService.mediaFile.mediaId} in ${mediaService.mediaFile.uploadState}',
);
await mediaService.setUploadState(UploadState.preprocessing); await mediaService.setUploadState(UploadState.preprocessing);
if (!mediaService.tempPath.existsSync()) { if (!mediaService.tempPath.existsSync()) {

View file

@ -61,6 +61,8 @@ Future<void> retransmitAllMessages() async {
}); });
} }
final Map<String, Mutex> _tryToSendLocks = {};
// When the ackByServerAt is set this value is written in the receipted // When the ackByServerAt is set this value is written in the receipted
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
String? receiptId, String? receiptId,
@ -68,15 +70,41 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
bool blocking = true, bool blocking = true,
}) async { }) async {
final rId = receiptId ?? receipt?.receiptId;
if (rId == null) {
Log.error(
'Cannot try to send complete message as both receiptId and receipt are null.',
);
return null;
}
final mutex = _tryToSendLocks.putIfAbsent(rId, Mutex.new);
return mutex.protect(() async {
return _tryToSendCompleteMessageInternal(
receiptId: receiptId,
receipt: receipt,
onlyReturnEncryptedData: onlyReturnEncryptedData,
blocking: blocking,
);
});
}
Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
String? receiptId,
Receipt? receipt,
bool onlyReturnEncryptedData = false,
bool blocking = true,
}) async {
// this should have a lock for every receiptID, split the function into a _internal withou the lock and a normal with the lock
if (apiService.appIsOutdated) return null; if (apiService.appIsOutdated) return null;
if (receiptId == null && receipt == null) return null;
try { try {
if (receiptId == null && receipt == null) return null;
if (receipt == null) { if (receipt == null) {
// ignore: parameter_assignments // ignore: parameter_assignments
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) { if (receipt == null) {
Log.warn('Receipt not found.'); Log.warn('[$receiptId] Receipt not found.');
return null; return null;
} }
} }
@ -120,8 +148,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
message.encryptedContent, message.encryptedContent,
); );
Log.info('Uploading ${receipt.receiptId}.');
Uint8List? pushData; Uint8List? pushData;
if (receipt.retryCount == 0) { if (receipt.retryCount == 0) {
final pushNotification = await getPushNotificationFromEncryptedContent( final pushNotification = await getPushNotificationFromEncryptedContent(
@ -166,9 +192,12 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
} }
if (onlyReturnEncryptedData) { if (onlyReturnEncryptedData) {
Log.info('Returning message with receiptID ${receipt.receiptId}.');
return (message.writeToBuffer(), pushData); return (message.writeToBuffer(), pushData);
} }
Log.info('Uploading message with receiptID ${receipt.receiptId}.');
final resp = await apiService.sendTextMessage( final resp = await apiService.sendTextMessage(
receipt.contactId, receipt.contactId,
message.writeToBuffer(), message.writeToBuffer(),
@ -176,7 +205,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
); );
if (resp.isError) { if (resp.isError) {
Log.warn('Could not transmit message got ${resp.error}.'); Log.warn('Could not transmit ${receipt.receiptId} got ${resp.error}.');
if (resp.error == ErrorCode.UserIdNotFound) { if (resp.error == ErrorCode.UserIdNotFound) {
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
@ -210,7 +239,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
} }
} }
} catch (e) { } catch (e) {
Log.error('Unknown Error when sending message: $e'); Log.error('[$receiptId] unknown error when sending message: $e');
if (receipt != null) { if (receipt != null) {
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
} }
@ -316,6 +345,54 @@ Future<void> insertAndSendContactShareMessage(
); );
} }
Future<void> insertAndSendAskAboutUserMessage(
int contactId,
int askAboutUserId,
) async {
final directChat = await twonlyDB.groupsDao.createOrGetDirectChat(contactId);
if (directChat == null) {
Log.error(
'Failed to get or create direct chat group for contact $contactId',
);
return;
}
final groupId = directChat.groupId;
final additionalMessageData = AdditionalMessageData(
type: AdditionalMessageData_Type.ASK_ABOUT_USER,
askAboutUserId: Int64(askAboutUserId),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(groupId),
type: Value(MessageType.askAboutUser.name),
additionalMessageData: Value(additionalMessageData.writeToBuffer()),
),
);
if (message == null) {
Log.error('Could not insert message into database');
return;
}
final encryptedContent = pb.EncryptedContent(
additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage(
senderMessageId: message.messageId,
additionalMessageData: additionalMessageData.writeToBuffer(),
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
type: MessageType.askAboutUser.name,
),
);
await sendCipherTextToGroup(
groupId,
encryptedContent,
messageId: message.messageId,
);
}
Future<void> sendCipherTextToGroup( Future<void> sendCipherTextToGroup(
String groupId, String groupId,
pb.EncryptedContent encryptedContent, { pb.EncryptedContent encryptedContent, {
@ -409,6 +486,17 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
); );
if (receipt != null) { if (receipt != null) {
try {
final typeKeys = _getEncryptedContentTypes(encryptedContent);
Log.info(
'sendCipherText: type=[$typeKeys] messageId=$messageId receiptId=${receipt.receiptId}',
);
} catch (_) {
Log.info(
'sendCipherText: messageId=$messageId receiptId=${receipt.receiptId}',
);
}
final tmp = tryToSendCompleteMessage( final tmp = tryToSendCompleteMessage(
receipt: receipt, receipt: receipt,
onlyReturnEncryptedData: onlyReturnEncryptedData, onlyReturnEncryptedData: onlyReturnEncryptedData,
@ -492,5 +580,23 @@ Future<void> sendContactMyProfileData(int contactId) async {
username: userService.currentUser.username, username: userService.currentUser.username,
), ),
); );
await sendCipherText(contactId, encryptedContent); await sendCipherText(contactId, encryptedContent, blocking: false);
}
String _getEncryptedContentTypes(pb.EncryptedContent content) {
final ignoredFields = {
'groupId',
'isDirectChat',
'senderProfileCounter',
'senderUserDiscoveryVersion',
};
final types = <String>[];
for (final field in content.info_.byName.values) {
if (content.hasField(field.tagNumber) &&
!ignoredFields.contains(field.name)) {
types.add(field.name);
}
}
return types.join(', ');
} }

View file

@ -5,6 +5,7 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -31,6 +32,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -73,7 +75,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
await apiService.sendResponse(ClientToServer()..v0 = v0); await apiService.sendResponse(ClientToServer()..v0 = v0);
AppState.gotMessageFromServer = true; AppState.gotMessageFromServer = true;
Log.info('Message from server proccessed.'); Log.info('All messages from the server proccessed.');
} }
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
@ -86,11 +88,20 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
final receiptId = message.receiptId; final receiptId = message.receiptId;
final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new); final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new);
if (mutex.isLocked) {
Log.info(
'[$receiptId] Skipping — already being processed by another handler',
);
return;
}
await mutex.protect(() async { await mutex.protect(() async {
try {
await _handleClient2ClientMessage(newMessage, message); await _handleClient2ClientMessage(newMessage, message);
}); } finally {
_messageLocks.remove(receiptId); _messageLocks.remove(receiptId);
} }
});
}
Future<void> _handleClient2ClientMessage( Future<void> _handleClient2ClientMessage(
NewMessage newMessage, NewMessage newMessage,
@ -103,11 +114,11 @@ Future<void> _handleClient2ClientMessage(
return; return;
} }
Log.info('Started processing message with receiptId $receiptId'); Log.info('[$receiptId] Started processing message');
switch (message.type) { switch (message.type) {
case Message_Type.SENDER_DELIVERY_RECEIPT: case Message_Type.SENDER_DELIVERY_RECEIPT:
Log.info('Got delivery receipt for $receiptId!'); Log.info('[$receiptId] Got delivery receipt!');
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
case Message_Type.PLAINTEXT_CONTENT: case Message_Type.PLAINTEXT_CONTENT:
@ -120,13 +131,13 @@ Future<void> _handleClient2ClientMessage(
await handleSessionResync(fromUserId); await handleSessionResync(fromUserId);
} }
Log.info( Log.info(
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', '[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}',
); );
retry = true; retry = true;
} }
if (message.plaintextContent.hasRetryControlError()) { if (message.plaintextContent.hasRetryControlError()) {
Log.info( Log.info(
'Got access control error for $receiptId. Resending message.', '[$receiptId] Got access control error. Resending message.',
); );
retry = true; retry = true;
} }
@ -141,7 +152,13 @@ Future<void> _handleClient2ClientMessage(
ackByServerAt: const Value(null), ackByServerAt: const Value(null),
), ),
); );
await tryToSendCompleteMessage(receiptId: newReceiptId); Log.info(
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
);
await tryToSendCompleteMessage(
receiptId: newReceiptId,
blocking: false,
);
} }
case Message_Type.CIPHERTEXT: case Message_Type.CIPHERTEXT:
@ -197,7 +214,6 @@ Future<void> _handleClient2ClientMessage(
receiptIdDB = const Value.absent(); receiptIdDB = const Value.absent();
} else { } else {
// Message was successful processed // Message was successful processed
//
} }
} }
@ -213,9 +229,9 @@ Future<void> _handleClient2ClientMessage(
), ),
); );
} catch (e) { } catch (e) {
Log.warn(e); Log.warn('[$receiptId] Error inserting receipt: $e');
} }
await tryToSendCompleteMessage(receiptId: receiptId); await tryToSendCompleteMessage(receiptId: receiptId, blocking: false);
} }
case Message_Type.TEST_NOTIFICATION: case Message_Type.TEST_NOTIFICATION:
break; break;
@ -223,9 +239,9 @@ Future<void> _handleClient2ClientMessage(
try { try {
await twonlyDB.receiptsDao.gotReceipt(receiptId); await twonlyDB.receiptsDao.gotReceipt(receiptId);
Log.info('Got a message with receiptId $receiptId'); Log.info('[$receiptId] Finished processing');
} catch (e) { } catch (e) {
Log.error('Error marking message as received $receiptId: $e'); Log.error('[$receiptId] Error marking message as received: $e');
} }
} }
@ -235,26 +251,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
Message_Type messageType, Message_Type messageType,
String receiptId, String receiptId,
) async { ) async {
final (encryptedContent, decryptionErrorType) = await signalDecryptMessage( Log.info('[$receiptId] calling signalDecryptMessage');
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
messageType.value, messageType.value,
); );
if (encryptedContent == null) { if (encryptedContent == null) {
if (decryptionErrorType == null) {
// Duplicate message
return (null, null);
}
return ( return (
null, null,
PlaintextContent() PlaintextContent(
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage(
..type = decryptionErrorType), type: decryptionErrorType ??=
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
),
),
); );
} }
Log.info('Calling handleEncryptedMessage for $receiptId'); Log.info('[$receiptId] Calling handleEncryptedMessage');
final (a, b) = await handleEncryptedMessage( final (a, b) = await handleEncryptedMessage(
fromUserId, fromUserId,
@ -263,11 +279,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
receiptId, receiptId,
); );
Log.info('Finished handleEncryptedMessage for $receiptId'); Log.info('[$receiptId] Finished handleEncryptedMessage');
if (Platform.isAndroid && a == null && b == null) { if (a == null && b == null) {
// Message was handled without any error -> Show push notification to the user. unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
await showPushNotificationFromServerMessages(fromUserId, encryptedContent); if (Platform.isAndroid) {
// Message was handled without any error. Show push notification to the user for Android.
await showPushNotificationFromServerMessages(
fromUserId,
encryptedContent,
);
}
} }
return (a, b); return (a, b);
@ -294,11 +316,16 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await checkForUserDiscoveryChanges( await checkForUserDiscoveryChanges(
fromUserId, fromUserId,
content.senderUserDiscoveryVersion, content.senderUserDiscoveryVersion,
receiptId,
); );
} }
if (content.hasContactRequest()) { if (content.hasContactRequest()) {
if (!await handleContactRequest(fromUserId, content.contactRequest)) { if (!await handleContactRequest(
fromUserId,
content.contactRequest,
receiptId,
)) {
return ( return (
null, null,
PlaintextContent() PlaintextContent()
@ -312,6 +339,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await handleErrorMessage( await handleErrorMessage(
fromUserId, fromUserId,
content.errorMessages, content.errorMessages,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -321,6 +349,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.contactUpdate, content.contactUpdate,
senderProfileCounter, senderProfileCounter,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -329,6 +358,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await handleUserDiscoveryRequest( await handleUserDiscoveryRequest(
fromUserId, fromUserId,
content.userDiscoveryRequest, content.userDiscoveryRequest,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -337,12 +367,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await handleUserDiscoveryUpdate( await handleUserDiscoveryUpdate(
fromUserId, fromUserId,
content.userDiscoveryUpdate, content.userDiscoveryUpdate,
receiptId,
); );
return (null, null); return (null, null);
} }
if (content.hasPushKeys()) { if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys); await handlePushKey(fromUserId, content.pushKeys, receiptId);
return (null, null); return (null, null);
} }
@ -350,6 +381,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await handleMessageUpdate( await handleMessageUpdate(
fromUserId, fromUserId,
content.messageUpdate, content.messageUpdate,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -366,12 +398,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await handleMediaUpdate( await handleMediaUpdate(
fromUserId, fromUserId,
content.mediaUpdate, content.mediaUpdate,
receiptId,
); );
return (null, null); return (null, null);
} }
if (!content.hasGroupId()) { if (!content.hasGroupId()) {
Log.error('Messages should have a groupId $fromUserId.'); Log.error('[$receiptId] Messages should have a groupId $fromUserId.');
return (null, null); return (null, null);
} }
@ -380,6 +413,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.groupCreate, content.groupCreate,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -392,12 +426,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
Log.info( Log.info(
'Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})', '[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
); );
if (contact == null || !contact.accepted || contact.deletedByUser) { if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId); await handleNewContactRequest(fromUserId);
Log.error( Log.error(
'User tries to send message to direct chat while the user does not exists !', '[$receiptId] User tries to send message to direct chat while the user does not exist!',
); );
return ( return (
EncryptedContent( EncryptedContent(
@ -411,7 +445,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
Log.info( Log.info(
'Creating new DirectChat between two users', '[$receiptId] Creating new DirectChat between two users',
); );
await twonlyDB.groupsDao.createNewDirectChat( await twonlyDB.groupsDao.createNewDirectChat(
fromUserId, fromUserId,
@ -422,7 +456,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
} else { } else {
if (content.hasGroupJoin()) { if (content.hasGroupJoin()) {
Log.error( Log.error(
'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.', '[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
); );
// In case the group join was received before the GroupCreate the sender should send it later again. // In case the group join was received before the GroupCreate the sender should send it later again.
return ( return (
@ -432,13 +466,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
Log.error('User $fromUserId tried to access group ${content.groupId}.'); Log.error(
'[$receiptId] User $fromUserId tried to access group ${content.groupId}.',
);
return (null, null); return (null, null);
} }
} }
if (content.hasFlameSync()) { if (content.hasFlameSync()) {
await handleFlameSync(content.groupId, content.flameSync); await handleFlameSync(content.groupId, content.flameSync, receiptId);
return (null, null); return (null, null);
} }
@ -447,6 +483,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.groupUpdate, content.groupUpdate,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -456,6 +493,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.groupJoin, content.groupJoin,
receiptId,
)) { )) {
return ( return (
null, null,
@ -471,6 +509,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.groupJoin, content.groupJoin,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -480,6 +519,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.additionalDataMessage, content.additionalDataMessage,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -489,6 +529,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.textMessage, content.textMessage,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -498,6 +539,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.reaction, content.reaction,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -507,6 +549,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.media, content.media,
receiptId,
); );
return (null, null); return (null, null);
} }
@ -516,6 +559,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.typingIndicator, content.typingIndicator,
receiptId,
); );
} }

View file

@ -4,7 +4,6 @@ import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
@ -129,7 +128,7 @@ Future<Map<String, String>?> getAuthenticationHeader() async {
}; };
} else { } else {
final apiAuthTokenRaw = await SecureStorage.instance.read( final apiAuthTokenRaw = await SecureStorage.instance.read(
key: SecureStorageKeys.apiAuthToken, key: 'api_auth_token',
); );
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {

View file

@ -119,9 +119,15 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
if (!shouldBeExecuted) return; if (!shouldBeExecuted) return;
Log.info('eu.twonly.periodic_task was called.'); Log.info('eu.twonly.periodic_task was called.');
AppState.gotMessageFromServer = false;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
// Issue: Because the background isolate can be reused across multiple periodic tasks,
// the API connection state might be stale or disconnected from a previous run.
// Explicitly close it here to ensure a clean slate before connecting.
await apiService.close(null);
if (!await apiService.connect()) { if (!await apiService.connect()) {
Log.info('Could not connect to the api. Returning early.'); Log.info('Could not connect to the api. Returning early.');
return; return;

View file

@ -65,23 +65,18 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) { ({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
const zero = (counter: 0, isExpiring: false); const zero = (counter: 0, isExpiring: false);
if (group == null) return zero; if (group == null) return zero;
if (group.lastMessageSend == null || if (group.lastMessageSend == null || group.lastMessageReceived == null || group.lastFlameCounterChange == null) {
group.lastMessageReceived == null ||
group.lastFlameCounterChange == null) {
return zero; return zero;
} }
final now = clock.now(); final now = clock.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
final oneDayAgo = startOfToday.subtract(const Duration(days: 1)); final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
if (group.lastMessageSend!.isAfter(twoDaysAgo) && if (group.lastMessageSend!.isAfter(twoDaysAgo) && group.lastMessageReceived!.isAfter(twoDaysAgo) ||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) { group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
// Flame is expiring when today no exchange has happened yet: // Flame is expiring when today no exchange has happened yet:
// both lastMessageSend and lastMessageReceived are before startOfToday. // both lastMessageSend and lastMessageReceived are before startOfToday.
final isExpiring = final isExpiring = group.lastMessageSend!.isBefore(oneDayAgo) || group.lastMessageReceived!.isBefore(oneDayAgo);
group.lastMessageSend!.isBefore(oneDayAgo) ||
group.lastMessageReceived!.isBefore(oneDayAgo);
return (counter: group.flameCounter, isExpiring: isExpiring); return (counter: group.flameCounter, isExpiring: isExpiring);
} else { } else {
return zero; return zero;
@ -122,8 +117,7 @@ Future<void> incFlameCounter(
final now = clock.now(); final now = clock.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (group.lastMessageSend!.isBefore(twoDaysAgo) || if (group.lastMessageSend!.isBefore(twoDaysAgo) || group.lastMessageReceived!.isBefore(twoDaysAgo)) {
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
flameCounter = 0; flameCounter = 0;
} }
} }
@ -135,25 +129,21 @@ Future<void> incFlameCounter(
final now = clock.now(); final now = clock.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
if (group.lastFlameCounterChange == null || if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(startOfToday)) {
group.lastFlameCounterChange!.isBefore(startOfToday)) {
// last flame update was yesterday. check if it can be updated. // last flame update was yesterday. check if it can be updated.
var updateFlame = false; var updateFlame = false;
if (received) { if (received) {
if (group.lastMessageSend != null && if (group.lastMessageSend != null && group.lastMessageSend!.isAfter(startOfToday)) {
group.lastMessageSend!.isAfter(startOfToday)) {
// today a message was already send -> update flame // today a message was already send -> update flame
updateFlame = true; updateFlame = true;
} }
} else if (group.lastMessageReceived != null && } else if (group.lastMessageReceived != null && group.lastMessageReceived!.isAfter(startOfToday)) {
group.lastMessageReceived!.isAfter(startOfToday)) {
// today a message was already received -> update flame // today a message was already received -> update flame
updateFlame = true; updateFlame = true;
} }
if (updateFlame) { if (updateFlame) {
flameCounter += 1; flameCounter += 1;
if (group.lastFlameCounterChange == null || if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(timestamp)) {
group.lastFlameCounterChange!.isBefore(timestamp)) {
// only update if the timestamp is newer // only update if the timestamp is newer
lastFlameCounterChange = Value(timestamp); lastFlameCounterChange = Value(timestamp);
} }
@ -170,13 +160,11 @@ Future<void> incFlameCounter(
} }
if (received) { if (received) {
if (group.lastMessageReceived == null || if (group.lastMessageReceived == null || group.lastMessageReceived!.isBefore(timestamp)) {
group.lastMessageReceived!.isBefore(timestamp)) {
lastMessageReceived = Value(timestamp); lastMessageReceived = Value(timestamp);
} }
} else { } else {
if (group.lastMessageSend == null || if (group.lastMessageSend == null || group.lastMessageSend!.isBefore(timestamp)) {
group.lastMessageSend!.isBefore(timestamp)) {
lastMessageSend = Value(timestamp); lastMessageSend = Value(timestamp);
} }
} }
@ -203,3 +191,18 @@ bool isItPossibleToRestoreFlames(Group group) {
clock.now().subtract(const Duration(days: 7)), clock.now().subtract(const Duration(days: 7)),
); );
} }
Future<void> restoreFlames(String groupId) async {
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) return;
final now = clock.now();
await twonlyDB.groupsDao.updateGroup(
groupId,
GroupsCompanion(
flameCounter: Value(group.maxFlameCounter),
lastFlameCounterChange: Value(now),
lastMessageSend: Value(now),
lastMessageReceived: Value(now),
),
);
}

View file

@ -3,14 +3,17 @@ import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb; as pb;
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class KeyVerificationService { class KeyVerificationService {
static Future<List<int>> getNewSecretVerificationToken() async { static Future<List<int>> getNewSecretVerificationToken() async {
@ -70,6 +73,18 @@ class KeyVerificationService {
VerificationType.secretQrToken, VerificationType.secretQrToken,
); );
Log.info('Contact was verified via secretQrToken'); Log.info('Contact was verified via secretQrToken');
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted && contact != null) {
showSnackbar(
context,
context.lang.secretQrTokenVerifiedSnackbar(
getContactDisplayName(contact),
),
level: SnackbarLevel.success,
);
}
return; return;
} }
} }

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -72,6 +73,13 @@ class MediaFileService {
delete = false; delete = false;
} }
// Never purge temp files while an upload is still in progress.
// The temp file is actively needed for encryption/upload.
if (mediaFile.uploadState != UploadState.uploaded &&
mediaFile.uploadState != UploadState.fileLimitReached) {
delete = false;
}
final messages = messageMap[mediaId] ?? []; final messages = messageMap[mediaId] ?? [];
// in case messages in empty the file will be deleted, as delete is true by default // in case messages in empty the file will be deleted, as delete is true by default
@ -84,6 +92,15 @@ class MediaFileService {
if (message.openedAt == null) { if (message.openedAt == null) {
// Message was not yet opened from all persons, so wait... // Message was not yet opened from all persons, so wait...
delete = false; delete = false;
} else if (message.openedAt!.isAfter(
clock.now().subtract(const Duration(minutes: 3)),
)) {
// When the message was opened in the last two minutes, do not purge.
// Bug: When the user opens an image immediately after starting the app, there is a race condition:
// The message is marked as opened, but then purgeTempFolder is run
// (it is unawaited) and deletes the file. Thi gives a grace period:
// The image must have been opened within the last two minutes, otherwise do not delete it.
delete = false;
} else if (mediaFile.requiresAuthentication || } else if (mediaFile.requiresAuthentication ||
mediaFile.displayLimitInMilliseconds != null) { mediaFile.displayLimitInMilliseconds != null) {
// Message was opened by all persons, and they can not reopen the image. // Message was opened by all persons, and they can not reopen the image.
@ -196,18 +213,40 @@ class MediaFileService {
} }
Future<void> createThumbnail() async { Future<void> createThumbnail() async {
if (!storedPath.existsSync()) { if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
Log.error('Could not create Thumbnail as stored media does not exists.'); if (storedPath.existsSync() && storedPath.lengthSync() == 0) {
try {
storedPath.deleteSync();
} catch (_) {}
}
if (mediaFile.stored &&
mediaFile.createdAt.isBefore(
clock.now().subtract(const Duration(days: 30)),
)) {
// media files does not exists any more so also delete the database entry
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
fullMediaRemoval();
}
return; return;
} }
var success = false;
switch (mediaFile.type) { switch (mediaFile.type) {
case MediaType.gif: case MediaType.gif:
case MediaType.audio: success = await createThumbnailsForGif(storedPath, thumbnailPath);
case MediaType.image: case MediaType.image:
// all images are already compress.. success = await createThumbnailsForImage(storedPath, thumbnailPath);
break;
case MediaType.video: case MediaType.video:
await createThumbnailsForVideo(storedPath, thumbnailPath); success = await createThumbnailsForVideo(storedPath, thumbnailPath);
case MediaType.audio:
break;
}
if (success) {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(hasThumbnail: Value(true)),
);
await updateFromDB();
} }
} }
@ -253,7 +292,11 @@ class MediaFileService {
tempPath.existsSync(); tempPath.existsSync();
bool get imagePreviewAvailable => bool get imagePreviewAvailable =>
thumbnailPath.existsSync() || storedPath.existsSync(); mediaFile.hasThumbnail ||
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
mediaFile.type == MediaType.audio ||
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
storedPath.existsSync() && storedPath.lengthSync() > 0);
Future<void> storeMediaFile() async { Future<void> storeMediaFile() async {
Log.info('Storing media file ${mediaFile.mediaId}'); Log.info('Storing media file ${mediaFile.mediaId}');
@ -275,6 +318,7 @@ class MediaFileService {
} else { } else {
await saveImageToGallery( await saveImageToGallery(
storedPath.readAsBytesSync(), storedPath.readAsBytesSync(),
createdAt: mediaFile.createdAt,
); );
} }
} }
@ -284,10 +328,24 @@ class MediaFileService {
); );
} }
unawaited(createThumbnail()); unawaited(createThumbnail());
await calculateAndSaveSize();
await hashMediaFile(); await hashMediaFile();
// updateFromDb is done in hashStoredMedia() // updateFromDb is done in hashStoredMedia()
} }
Future<void> calculateAndSaveSize() async {
if (storedPath.existsSync()) {
final size = storedPath.lengthSync();
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
sizeInBytes: Value(size),
),
);
await updateFromDB();
}
}
Future<void> hashMediaFile() async { Future<void> hashMediaFile() async {
late final List<int> checksum; late final List<int> checksum;
if (storedPath.existsSync()) { if (storedPath.existsSync()) {
@ -388,7 +446,7 @@ class MediaFileService {
return; return;
} }
if (!storedPath.existsSync()) { if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId, mediaFile.mediaId,
const MediaFilesCompanion(hasCropAnalyzed: Value(true)), const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
@ -397,15 +455,71 @@ class MediaFileService {
} }
try { try {
final bytes = storedPath.readAsBytesSync(); final bytes = await storedPath.readAsBytes();
final image = img.decodeImage(bytes); final result = await compute(_processImageCrop, bytes);
if (image == null) {
if (result.isCropped && result.pngBytes != null) {
try {
final webpBytes = await FlutterImageCompress.compressWithList(
result.pngBytes!,
format: CompressFormat.webp,
quality: 90,
);
if (webpBytes.isNotEmpty) {
await storedPath.writeAsBytes(webpBytes);
} else {
Log.warn('WebP compression returned empty, falling back to PNG');
await storedPath.writeAsBytes(result.pngBytes!);
}
} catch (e) {
Log.error('Error compressing to WebP, falling back to PNG: $e');
await storedPath.writeAsBytes(result.pngBytes!);
}
if (thumbnailPath.existsSync()) {
await thumbnailPath.delete();
}
await createThumbnail();
final checksum = await sha256File(storedPath);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
hasCropAnalyzed: const Value(true),
storedFileHash: Value(Uint8List.fromList(checksum)),
),
);
await updateFromDB();
return;
}
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId, mediaFile.mediaId,
const MediaFilesCompanion(hasCropAnalyzed: Value(true)), const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
); );
return; await updateFromDB();
} catch (e) {
Log.error(
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
);
await updateFromDB();
} }
}
}
class _CropResult {
const _CropResult(this.pngBytes, this.isCropped);
final Uint8List? pngBytes;
final bool isCropped;
}
_CropResult _processImageCrop(Uint8List bytes) {
final image = img.decodeImage(bytes);
if (image == null) return const _CropResult(null, false);
var minY = 0; var minY = 0;
var maxY = image.height - 1; var maxY = image.height - 1;
@ -476,44 +590,9 @@ class MediaFileService {
height: newHeight, height: newHeight,
); );
final pngBytes = img.encodePng(cropped); final pngBytes = img.encodePng(cropped);
final webpBytes = await FlutterImageCompress.compressWithList( return _CropResult(pngBytes, true);
pngBytes,
format: CompressFormat.webp,
quality: 90,
);
storedPath.writeAsBytesSync(webpBytes);
if (thumbnailPath.existsSync()) {
thumbnailPath.deleteSync();
}
await createThumbnail();
final checksum = await sha256File(storedPath);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
hasCropAnalyzed: const Value(true),
storedFileHash: Value(Uint8List.fromList(checksum)),
),
);
await updateFromDB();
return;
} }
} }
await twonlyDB.mediaFilesDao.updateMedia( return const _CropResult(null, false);
mediaFile.mediaId,
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
);
await updateFromDB();
} catch (e) {
Log.error(
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
);
await updateFromDB();
}
}
} }

View file

@ -1,18 +1,38 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:pro_video_editor/pro_video_editor.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> createThumbnailsForVideo( Future<bool> createThumbnailsForVideo(
File sourceFile, File sourceFile,
File destinationFile, File destinationFile,
) async { ) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
Log.warn('Source video file does not exist or is empty.');
try {
if (destinationFile.existsSync()) { if (destinationFile.existsSync()) {
return; destinationFile.deleteSync();
}
} catch (_) {}
return false;
} }
if (destinationFile.existsSync()) {
if (destinationFile.lengthSync() > 0) {
return true;
} else {
try {
destinationFile.deleteSync();
} catch (_) {}
}
}
try {
final images = await ProVideoEditor.instance.getThumbnails( final images = await ProVideoEditor.instance.getThumbnails(
ThumbnailConfigs( ThumbnailConfigs(
video: EditorVideo.file(sourceFile), video: EditorVideo.file(sourceFile),
@ -24,15 +44,174 @@ Future<void> createThumbnailsForVideo(
), ),
); );
if (images.isNotEmpty) { if (images.isNotEmpty && images.first.isNotEmpty) {
stopwatch.stop(); stopwatch.stop();
destinationFile.writeAsBytesSync(images.first); await destinationFile.writeAsBytes(images.first);
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', 'It took ${stopwatch.elapsedMilliseconds}ms to create the video thumbnail.',
); );
} else { return true;
}
}
} catch (e) {
Log.error('Error creating video thumbnail: $e');
}
Log.warn( Log.warn(
'Thumbnail creation failed for the video with exit code.', 'Thumbnail creation failed for the video.',
); );
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
Future<bool> createThumbnailsForImage(
File sourceFile,
File destinationFile,
) async {
final stopwatch = Stopwatch()..start();
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
Log.warn('Source image file does not exist or is empty.');
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
if (destinationFile.existsSync()) {
if (destinationFile.lengthSync() > 0) {
return true;
} else {
try {
destinationFile.deleteSync();
} catch (_) {}
} }
} }
try {
await FlutterImageCompress.compressAndGetFile(
sourceFile.absolute.path,
destinationFile.absolute.path,
minWidth: 300,
minHeight: 300,
quality: 100,
format: CompressFormat.webp,
);
stopwatch.stop();
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to create the image thumbnail.',
);
return true;
} else {
Log.error('Compressed image thumbnail is empty or missing.');
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
} catch (e) {
Log.error('Error creating image thumbnail: $e');
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
}
Future<bool> createThumbnailsForGif(
File sourceFile,
File destinationFile,
) async {
final stopwatch = Stopwatch()..start();
if (!sourceFile.existsSync() || sourceFile.lengthSync() == 0) {
Log.warn('Source GIF file does not exist or is empty.');
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
if (destinationFile.existsSync()) {
if (destinationFile.lengthSync() > 0) {
return true;
} else {
try {
destinationFile.deleteSync();
} catch (_) {}
}
}
try {
// For GIFs, we decode the first frame and save it as WebP
final bytes = await sourceFile.readAsBytes();
final pngBytes = await compute(_processGifThumbnail, bytes);
if (pngBytes == null || pngBytes.isEmpty) {
Log.error('Could not decode GIF for thumbnail.');
return false;
}
final webp = await FlutterImageCompress.compressWithList(
pngBytes,
format: CompressFormat.webp,
quality: 85,
);
if (webp.isEmpty) {
Log.error('GIF thumbnail compression returned empty.');
return false;
}
await destinationFile.writeAsBytes(webp);
stopwatch.stop();
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to create the GIF thumbnail.',
);
return true;
} else {
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
} catch (e) {
Log.error('Error creating GIF thumbnail: $e');
try {
if (destinationFile.existsSync()) {
destinationFile.deleteSync();
}
} catch (_) {}
return false;
}
}
Uint8List? _processGifThumbnail(Uint8List bytes) {
final image = img.decodeGif(bytes);
if (image == null) return null;
final thumbnail = img.copyResize(
image,
width: image.width > image.height ? 400 : null,
height: image.height >= image.width ? 400 : null,
);
return img.encodePng(thumbnail);
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -14,6 +15,7 @@ import 'package:twonly/src/utils/log.dart';
class MemoriesState { class MemoriesState {
const MemoriesState({ const MemoriesState({
required this.filesToMigrate, required this.filesToMigrate,
required this.totalFilesToMigrate,
required this.galleryItems, required this.galleryItems,
required this.months, required this.months,
required this.orderedByMonth, required this.orderedByMonth,
@ -21,16 +23,21 @@ class MemoriesState {
}); });
final int filesToMigrate; final int filesToMigrate;
final int totalFilesToMigrate;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final List<String> months; final List<String> months;
final Map<String, List<int>> orderedByMonth; final Map<String, List<int>> orderedByMonth;
final Map<int, List<MemoryItem>> galleryItemsLastYears; final Map<int, List<MemoryItem>> galleryItemsLastYears;
bool get isLoading => filesToMigrate > 0; bool get isLoading => filesToMigrate > 0;
double get migrationProgress => totalFilesToMigrate > 0
? (totalFilesToMigrate - filesToMigrate) / totalFilesToMigrate
: 0;
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0; bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
MemoriesState copyWith({ MemoriesState copyWith({
int? filesToMigrate, int? filesToMigrate,
int? totalFilesToMigrate,
List<MemoryItem>? galleryItems, List<MemoryItem>? galleryItems,
List<String>? months, List<String>? months,
Map<String, List<int>>? orderedByMonth, Map<String, List<int>>? orderedByMonth,
@ -38,6 +45,7 @@ class MemoriesState {
}) { }) {
return MemoriesState( return MemoriesState(
filesToMigrate: filesToMigrate ?? this.filesToMigrate, filesToMigrate: filesToMigrate ?? this.filesToMigrate,
totalFilesToMigrate: totalFilesToMigrate ?? this.totalFilesToMigrate,
galleryItems: galleryItems ?? this.galleryItems, galleryItems: galleryItems ?? this.galleryItems,
months: months ?? this.months, months: months ?? this.months,
orderedByMonth: orderedByMonth ?? this.orderedByMonth, orderedByMonth: orderedByMonth ?? this.orderedByMonth,
@ -62,6 +70,7 @@ class MemoriesService {
MemoriesState _currentState = const MemoriesState( MemoriesState _currentState = const MemoriesState(
filesToMigrate: 0, filesToMigrate: 0,
totalFilesToMigrate: 0,
galleryItems: [], galleryItems: [],
months: [], months: [],
orderedByMonth: {}, orderedByMonth: {},
@ -88,14 +97,10 @@ class MemoriesService {
final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds( final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
mediaIds, mediaIds,
); );
final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m};
final allContacts = await twonlyDB.contactsDao.getAllContacts(); final allContacts = await twonlyDB.contactsDao.getAllContacts();
final contactMap = {for (final c in allContacts) c.userId: c}; final contactMap = {for (final c in allContacts) c.userId: c};
final mediaIdToSender = <String, Contact?>{};
final now = clock.now();
final tempGalleryItems = <MemoryItem>[];
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
for (final itemJson in itemList) { for (final itemJson in itemList) {
final map = itemJson as Map<String, dynamic>; final map = itemJson as Map<String, dynamic>;
@ -103,64 +108,14 @@ class MemoriesService {
final senderUserId = map['senderUserId'] as int?; final senderUserId = map['senderUserId'] as int?;
if (mediaId == null) continue; if (mediaId == null) continue;
final mediaFile = mediaFileMap[mediaId]; mediaIdToSender[mediaId] = senderUserId != null
if (mediaFile == null) continue;
final mediaService = MediaFileService(mediaFile);
if (!mediaService.imagePreviewAvailable) continue;
final contact = senderUserId != null
? contactMap[senderUserId] ? contactMap[senderUserId]
: null; : null;
final item = MemoryItem(
mediaService: mediaService,
messages: [],
sender: contact,
);
tempGalleryItems.add(item);
if (mediaFile.createdAt.month == now.month &&
mediaFile.createdAt.day == now.day) {
final diff = now.year - mediaFile.createdAt.year;
if (diff > 0) {
tempGalleryItemsLastYears.putIfAbsent(diff, () => []).add(item);
}
}
} }
final tempOrderedByMonth = <String, List<int>>{}; _cachedState = _computeState(
final tempMonths = <String>[]; mediaFiles: mediaFiles,
var lastMonth = ''; mediaIdToSender: mediaIdToSender,
for (var i = 0; i < tempGalleryItems.length; i++) {
final mFile = tempGalleryItems[i].mediaService.mediaFile;
final month =
mFile.createdAtMonth ??
DateFormat('MMMM yyyy').format(mFile.createdAt);
if (lastMonth != month) {
lastMonth = month;
tempMonths.add(month);
}
tempOrderedByMonth.putIfAbsent(month, () => []).add(i);
}
for (final list in tempGalleryItemsLastYears.values) {
list.sort(
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
a.mediaService.mediaFile.createdAt,
),
);
}
final sortedGalleryItemsLastYears =
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
_cachedState = MemoriesState(
filesToMigrate: 0,
galleryItems: tempGalleryItems,
months: tempMonths,
orderedByMonth: tempOrderedByMonth,
galleryItemsLastYears: sortedGalleryItemsLastYears,
); );
} }
} catch (e) { } catch (e) {
@ -168,79 +123,20 @@ class MemoriesService {
} }
} }
Future<void> _initAsync() async { static MemoriesState _computeState({
try { required List<MediaFile> mediaFiles,
// 1. Perform Inventory / Migration of non-hashed stored files required Map<String, Contact?> mediaIdToSender,
final nonHashedFiles = await twonlyDB.mediaFilesDao int filesToMigrate = 0,
.getAllNonHashedStoredMediaFiles(); }) {
final unanalyzedFiles = await twonlyDB.mediaFilesDao
.getAllUnanalyzedStoredMediaFiles();
final totalToMigrate = nonHashedFiles.length + unanalyzedFiles.length;
if (totalToMigrate > 0) {
_updateState(filesToMigrate: totalToMigrate);
for (final mediaFile in nonHashedFiles) {
final mediaService = MediaFileService(mediaFile);
await mediaService.hashMediaFile();
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
}
for (final mediaFile in unanalyzedFiles) {
final mediaService = MediaFileService(mediaFile);
await mediaService.cropTransparentBorders();
_updateState(filesToMigrate: _currentState.filesToMigrate - 1);
}
_updateState(filesToMigrate: 0);
}
// 2. Subscribe to stored media files stream
await _dbSubscription?.cancel();
_dbSubscription = twonlyDB.mediaFilesDao
.watchAllStoredMediaFiles()
.listen(_processMediaFilesStream);
} catch (e) {
Log.error('Error initializing MemoriesService: $e');
}
}
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
try {
final now = clock.now(); final now = clock.now();
final tempGalleryItems = <MemoryItem>[]; final tempGalleryItems = <MemoryItem>[];
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{}; final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
// High-performance batch DB fetch for sender attribution via Messages table mapping
final mediaIds = mediaFiles.map((m) => m.mediaId).toList();
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
mediaIds,
);
final allContacts = await twonlyDB.contactsDao.getAllContacts();
final contactMap = {for (final c in allContacts) c.userId: c};
final mediaIdToSenderContact = <String, Contact>{};
for (final msg in allMessages) {
if (msg.mediaId != null && msg.senderId != null) {
final contact = contactMap[msg.senderId];
if (contact != null) {
mediaIdToSenderContact[msg.mediaId!] = contact;
}
}
}
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
final mediaService = MediaFileService(mediaFile); final mediaService = MediaFileService(mediaFile);
if (!mediaService.imagePreviewAvailable) continue; if (!mediaService.imagePreviewAvailable) continue;
if (mediaService.mediaFile.type == MediaType.video) { final senderContact = mediaIdToSender[mediaFile.mediaId];
if (!mediaService.thumbnailPath.existsSync()) {
unawaited(mediaService.createThumbnail());
}
}
final senderContact = mediaIdToSenderContact[mediaFile.mediaId];
final item = MemoryItem( final item = MemoryItem(
mediaService: mediaService, mediaService: mediaService,
messages: [], messages: [],
@ -269,7 +165,6 @@ class MemoriesService {
final tempMonths = <String>[]; final tempMonths = <String>[];
var lastMonth = ''; var lastMonth = '';
// High performance grouping leveraging pre-computed createdAtMonth column
for (var i = 0; i < tempGalleryItems.length; i++) { for (var i = 0; i < tempGalleryItems.length; i++) {
final mFile = tempGalleryItems[i].mediaService.mediaFile; final mFile = tempGalleryItems[i].mediaService.mediaFile;
final month = final month =
@ -293,19 +188,144 @@ class MemoriesService {
final sortedGalleryItemsLastYears = final sortedGalleryItemsLastYears =
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears); SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
final newState = MemoriesState( return MemoriesState(
filesToMigrate: _currentState.filesToMigrate, filesToMigrate: filesToMigrate,
totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating.
galleryItems: tempGalleryItems, galleryItems: tempGalleryItems,
months: tempMonths, months: tempMonths,
orderedByMonth: tempOrderedByMonth, orderedByMonth: tempOrderedByMonth,
galleryItemsLastYears: sortedGalleryItemsLastYears, galleryItemsLastYears: sortedGalleryItemsLastYears,
); );
}
Future<void> _initAsync() async {
try {
// Start DB subscription first so files with existing thumbnails are shown immediately.
await _dbSubscription?.cancel();
_dbSubscription = twonlyDB.mediaFilesDao
.watchAllStoredMediaFiles()
.listen(_processMediaFilesStream);
final pendingFiles = await twonlyDB.mediaFilesDao
.getAllMediaFilesPendingMigration();
if (pendingFiles.isNotEmpty) {
_currentState = _currentState.copyWith(
filesToMigrate: pendingFiles.length,
totalFilesToMigrate: pendingFiles.length,
);
_notifyState();
// Run the multi-step background migration process asynchronously.
unawaited(_processMigrationQueue(pendingFiles));
}
} catch (e) {
Log.error('Error initializing MemoriesService: $e');
}
}
Future<void> _processMigrationQueue(List<MediaFile> pendingFiles) async {
try {
// Phase 1: Create thumbnails first so files can be shown in the
// gallery immediately, without waiting for heavier operations.
for (final mediaFile in pendingFiles) {
try {
final mediaService = MediaFileService(mediaFile);
if (!mediaService.mediaFile.hasThumbnail) {
if (mediaService.thumbnailPath.existsSync() &&
mediaService.thumbnailPath.lengthSync() > 0) {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(hasThumbnail: Value(true)),
);
} else if (mediaFile.type != MediaType.audio) {
await mediaService.createThumbnail();
}
}
} catch (e) {
Log.error(
'Error creating thumbnail for ${mediaFile.mediaId}: $e',
);
}
_updateMigrationCount(_currentState.filesToMigrate - 1);
}
_updateMigrationCount(0);
// Phase 2: Background hash, crop analysis, size calculation.
// Each DB write here fires the stream subscription above, keeping
// the gallery state fresh without a separate notification step.
await _backgroundProcessPendingFiles(pendingFiles);
} catch (e) {
Log.error('Error in background migration queue: $e');
}
}
Future<void> _backgroundProcessPendingFiles(
List<MediaFile> pendingFiles,
) async {
for (final mediaFile in pendingFiles) {
try {
final mediaService = MediaFileService(mediaFile);
if (mediaService.mediaFile.storedFileHash == null) {
await mediaService.hashMediaFile();
}
if (!mediaService.mediaFile.hasCropAnalyzed) {
await mediaService.cropTransparentBorders();
}
if (mediaService.mediaFile.sizeInBytes == null) {
await mediaService.calculateAndSaveSize();
}
} catch (e) {
Log.error(
'Error in background processing of ${mediaFile.mediaId}: $e',
);
}
}
}
Future<void> _processMediaFilesStream(List<MediaFile> mediaFiles) async {
try {
final mediaIds = mediaFiles.map((m) => m.mediaId).toList();
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
mediaIds,
);
final allContacts = await twonlyDB.contactsDao.getAllContacts();
final contactMap = {for (final c in allContacts) c.userId: c};
final mediaIdToSenderContact = <String, Contact>{};
for (final msg in allMessages) {
if (msg.mediaId != null && msg.senderId != null) {
final contact = contactMap[msg.senderId];
if (contact != null) {
mediaIdToSenderContact[msg.mediaId!] = contact;
}
}
}
final newState = _computeState(
mediaFiles: mediaFiles,
mediaIdToSender: mediaIdToSenderContact,
filesToMigrate: _currentState.filesToMigrate,
).copyWith(totalFilesToMigrate: _currentState.totalFilesToMigrate);
for (final item in newState.galleryItems) {
if (!item.mediaService.mediaFile.hasThumbnail &&
item.mediaService.mediaFile.type != MediaType.audio) {
unawaited(item.mediaService.createThumbnail());
}
}
_cachedState = newState; _cachedState = newState;
_updateStateWithObject(newState); _updateState(newState);
// Persist to KeyValueStore cache asynchronously // Persist to KeyValueStore cache asynchronously
final cacheList = tempGalleryItems final cacheList = newState.galleryItems
.map( .map(
(item) => { (item) => {
'mediaId': item.mediaService.mediaFile.mediaId, 'mediaId': item.mediaService.mediaFile.mediaId,
@ -319,15 +339,17 @@ class MemoriesService {
} }
} }
void _updateStateWithObject(MemoriesState newState) { void _updateState(MemoriesState newState) {
_currentState = newState; _currentState = newState;
if (!_stateController.isClosed) { _notifyState();
_stateController.add(_currentState);
}
} }
void _updateState({int? filesToMigrate}) { void _updateMigrationCount(int filesToMigrate) {
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate); _currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
_notifyState();
}
void _notifyState() {
if (!_stateController.isClosed) { if (!_stateController.isClosed) {
_stateController.add(_currentState); _stateController.add(_currentState);
} }

View file

@ -0,0 +1,171 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
show getSignalSignedPreKeyStoreOld;
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
Future<void> runMigrations() async {
if (userService.currentUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await UserService.update((u) => u.appVersion = 90);
}
if (userService.currentUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91();
await UserService.update((u) => u.appVersion = 91);
}
if (userService.currentUser.appVersion < 109) {
final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) {
if (contact.verified) {
await twonlyDB.keyVerificationDao.addKeyVerification(
contact.userId,
VerificationType.migratedFromOldVersion,
);
}
}
await UserService.update((u) {
u
..appVersion = 109
..skipSetupPages = true;
if (u.avatarSvg == null) {
u.currentSetupPage = SetupPages.profile.name;
} else {
u.currentSetupPage = SetupPages.shareYourFriends.name;
}
});
}
if (userService.currentUser.appVersion < 113) {
var migrationSuccess = true;
final signalIdentity = await SecureStorage.instance.read(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
try {
final decoded = jsonDecode(signalIdentity);
final identity = SignalIdentity.fromJson(
decoded as Map<String, dynamic>,
);
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identity.identityKeyPairU8List,
registrationId: identity.registrationId,
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
);
Log.info('Importing signal identify to the rust key manager');
// Clean up old keys after successful migration
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalSignedPreKey,
);
} catch (e) {
Log.error('Failed to migrate signal identity: $e');
migrationSuccess = false;
}
}
if (migrationSuccess) {
await UserService.update((u) {
u
..appVersion = 113
..canUseLoginTokenForAuth = false
// As usernames changes where not considered in the old version force users
// to reenter there passwords.
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.encryptionKey = []
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.backupId = [];
});
}
}
if (userService.currentUser.appVersion < 114) {
final allMedia = await twonlyDB.mediaFilesDao
.select(twonlyDB.mediaFiles)
.get();
for (final media in allMedia) {
if (media.createdAtMonth == null) {
final monthStr = DateFormat('MMMM yyyy').format(media.createdAt);
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
MediaFilesCompanion(createdAtMonth: Value(monthStr)),
);
}
}
await UserService.update((u) => u.appVersion = 114);
}
if (userService.currentUser.appVersion < 115) {
var migrationSuccess = true;
try {
final rustStore = await RustKeyManager.loadSignedPrekeys();
for (final entry in rustStore.entries) {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(entry.key),
signedPreKey: Value(entry.value),
);
await twonlyDB
.into(twonlyDB.signalSignedPreKeyStores)
.insert(
companion,
mode: InsertMode.insertOrReplace,
);
await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key);
}
} catch (e) {
Log.error('Failed to migrate signed prekeys to Drift: $e');
migrationSuccess = false;
}
if (migrationSuccess) {
await UserService.update((u) => u.appVersion = 115);
}
}
if (userService.currentUser.appVersion < 116) {
if (userService.currentUser.userDiscoveryThreshold == 2) {
if (userService.currentUser.isUserDiscoveryEnabled) {
await UserDiscoveryService.initializeOrUpdate(
threshold: 3,
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
);
} else {
await UserService.update((u) => u..userDiscoveryThreshold = 3);
}
}
await UserService.update((u) => u.appVersion = 116);
}
if (kDebugMode) {
assert(
AppState.latestAppVersionId == 116,
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
);
assert(
AppState.latestAppVersionId == userService.currentUser.appVersion,
"Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.",
);
}
}

View file

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/log.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await FcmNotificationService.handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
}
} else {
// make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000));
}
}

View file

@ -1,17 +1,15 @@
// ignore_for_file: unreachable_from_main
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:firebase_app_installations/firebase_app_installations.dart'; import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.background.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -19,9 +17,62 @@ import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> checkForTokenUpdates() async { class FcmNotificationService {
static Future<void> initStartup() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
}
static Future<void> initAfterUserLoaded() async {
unawaited(_checkForTokenUpdates());
unawaited(_checkFcmHealthAndResetIfNeeded());
}
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await _checkForTokenUpdates();
return;
}
final res = await apiService.updateFCMToken(
fcmToken,
);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
}
}
static Future<void> resetFCMTokens() async {
await FirebaseInstallations.instance.delete();
Log.info('Firebase Installation successfully deleted.');
await FirebaseMessaging.instance.deleteToken();
Log.info('Old FCM deleted.');
await UserService.update((u) => u.fcmToken = null);
await _checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
}
static Future<void> _checkForTokenUpdates() async {
try { try {
if (!userService.isUserCreated) return; if (!userService.isUserCreated) {
Log.info(
'Checking for FCM token updates skipped: user is not yet created.',
);
return;
}
if (Platform.isIOS) { if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken(); var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
@ -51,35 +102,8 @@ Future<void> checkForTokenUpdates() async {
..updateFCMToken = true ..updateFCMToken = true
..fcmToken = fcmToken; ..fcmToken = fcmToken;
}); });
} if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
})
.onError((err) {
Log.error('could not listen on token refresh');
});
} catch (e) {
Log.error('could not load fcm token: $e');
}
}
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await checkForTokenUpdates();
return;
}
final res = await apiService.updateFCMToken(
fcmToken,
);
if (res.isSuccess) { if (res.isSuccess) {
Log.info('Uploaded new FCM token!'); Log.info('Uploaded new FCM token!');
await UserService.update((u) { await UserService.update((u) {
@ -91,48 +115,36 @@ Future<void> initFCMAfterAuthenticated({bool force = false}) async {
} }
} }
Future<void> resetFCMTokens() async { FirebaseMessaging.instance.onTokenRefresh
await FirebaseInstallations.instance.delete(); // ignore: avoid_types_on_closure_parameters
Log.info('Firebase Installation successfully deleted.'); .listen((String fcmToken) async {
await FirebaseMessaging.instance.deleteToken(); await UserService.update((u) {
Log.info('Old FCM deleted.'); u
await UserService.update((u) => u.fcmToken = null); ..updateFCMToken = true
await checkForTokenUpdates(); ..fcmToken = fcmToken;
await initFCMAfterAuthenticated(force: true); });
} if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
Future<void> initFCMService() async { if (res.isSuccess) {
await Firebase.initializeApp( Log.info('Uploaded new FCM token!');
options: DefaultFirebaseOptions.currentPlatform, await UserService.update((u) {
); u.updateFCMToken = false;
});
unawaited(checkForTokenUpdates());
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
}
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
}
} else { } else {
// make sure every thing run... Log.error('Could not update FCM token!');
await Future.delayed(const Duration(milliseconds: 2000)); }
}
})
.onError((err) {
Log.error('could not listen on token refresh');
});
} catch (e) {
Log.error('could not load fcm token: $e');
} }
} }
Future<void> handleRemoteMessage(RemoteMessage message) async { static Future<void> handleRemoteMessage(RemoteMessage message) async {
await _updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS'); Log.error('Got message in Dart while on iOS');
} }
@ -150,10 +162,108 @@ Future<void> handleRemoteMessage(RemoteMessage message) async {
message.notification?.body ?? message.data['body'] as String? ?? ''; message.notification?.body ?? message.data['body'] as String? ?? '';
await customLocalPushNotification(title, body); await customLocalPushNotification(title, body);
} }
}
// On Android the push notification is now shown in the server_message.dart. This ensures
// that the messages was successfully decrypted before showing the push notification static Future<void> _updateLastFcmMessageTimestamp() async {
// else if (message.data['push_data'] != null) { const storage = FlutterSecureStorage();
// await handlePushData(message.data['push_data'] as String); final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
// } try {
await storage.write(
key: SecureStorageKeys.lastFcmMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last FCM message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last FCM message timestamp: $e');
}
}
static Future<void> updateLastServerMessageTimestamp() async {
const storage = FlutterSecureStorage();
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
try {
await storage.write(
key: SecureStorageKeys.lastServerMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last server message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last server message timestamp: $e');
}
}
static Future<void> _checkFcmHealthAndResetIfNeeded() async {
if (!userService.isUserCreated) {
Log.info('FCM health check skipped: user is not yet created.');
return;
}
const storage = FlutterSecureStorage();
try {
final lastFcmStr = await storage.read(
key: SecureStorageKeys.lastFcmMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final lastServerStr = await storage.read(
key: SecureStorageKeys.lastServerMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final now = DateTime.now();
final threeDaysAgo = now.subtract(const Duration(days: 3));
DateTime? lastFcmTime;
if (lastFcmStr != null) {
final ms = int.tryParse(lastFcmStr);
if (ms != null) {
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
}
}
if (lastFcmTime != null) {
Log.info(
'Last message received via FCM messaging system: $lastFcmTime',
);
} else {
Log.info('No record of a message received via FCM messaging system.');
}
DateTime? lastServerTime;
if (lastServerStr != null) {
final ms = int.tryParse(lastServerStr);
if (ms != null) {
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
}
}
final fcmInactive =
lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
final serverActive =
lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
if (fcmInactive && serverActive) {
Log.warn(
'FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...',
);
await resetFCMTokens();
} else {
Log.info('FCM check passed. No reset needed.');
}
} catch (e) {
Log.error('Error during FCM health check: $e');
}
}
} }

View file

@ -30,7 +30,7 @@ Future<void> setupNotificationWithUsers({
// HotFIX: Search for user with id 0 if not there remove all // HotFIX: Search for user with id 0 if not there remove all
// and create new push keys with all users. // and create new push keys with all users.
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0); final pushUser = pushUsers.firstWhereOrNull((x) => x.userId.toInt() == 0);
if (pushUser == null) { if (pushUser == null) {
Log.info('Clearing push keys'); Log.info('Clearing push keys');
await setPushKeys(SecureStorageKeys.receivingPushKeys, []); await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
@ -51,7 +51,7 @@ Future<void> setupNotificationWithUsers({
final contacts = await twonlyDB.contactsDao.getAllContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
final pushUser = pushUsers.firstWhereOrNull( final pushUser = pushUsers.firstWhereOrNull(
(x) => x.userId == contact.userId, (x) => x.userId.toInt() == contact.userId,
); );
if (pushUser != null && pushUser.pushKeys.isNotEmpty) { if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
@ -124,7 +124,9 @@ Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
Future<void> updatePushUser(Contact contact) async { Future<void> updatePushUser(Contact contact) async {
final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId); final pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == contact.userId,
);
if (pushUser == null) { if (pushUser == null) {
pushKeys.add( pushKeys.add(
@ -148,7 +150,9 @@ Future<void> updatePushUser(Contact contact) async {
Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async { Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); var pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == fromUserId,
);
if (pushUser == null) { if (pushUser == null) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -164,7 +168,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
lastMessageId: uuid.v7(), lastMessageId: uuid.v7(),
), ),
); );
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); pushUser = pushKeys.firstWhereOrNull((x) => x.userId.toInt() == fromUserId);
} }
if (pushUser == null) { if (pushUser == null) {
@ -187,7 +191,9 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
Future<void> updateLastMessageId(int fromUserId, String messageId) async { Future<void> updateLastMessageId(int fromUserId, String messageId) async {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId); final pushUser = pushUsers.firstWhereOrNull(
(x) => x.userId.toInt() == fromUserId,
);
if (pushUser == null) { if (pushUser == null) {
unawaited(setupNotificationWithUsers()); unawaited(setupNotificationWithUsers());
return; return;
@ -285,7 +291,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
if (content.hasMediaUpdate()) { if (content.hasMediaUpdate()) {
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageById(content.reaction.targetMessageId) .getMessageById(content.mediaUpdate.targetMessageId)
.getSingleOrNull(); .getSingleOrNull();
// These notifications should only be send to the original sender. // These notifications should only be send to the original sender.
if (msg == null || msg.senderId != toUserId) { if (msg == null || msg.senderId != toUserId) {
@ -304,7 +310,9 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
if (content.hasGroupCreate()) { if (content.hasGroupCreate()) {
kind = PushKind.ADDED_TO_GROUP; kind = PushKind.ADDED_TO_GROUP;
final group = await twonlyDB.groupsDao.getGroup(content.groupId); final group = await twonlyDB.groupsDao.getGroup(content.groupId);
additionalContent = group!.groupName; if (group != null) {
additionalContent = group.groupName;
}
} }
if (kind == null) return null; if (kind == null) return null;
@ -339,7 +347,9 @@ Future<Uint8List?> encryptPushNotification(
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits; var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
var keyId = 0; var keyId = 0;
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId); final pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == toUserId,
);
if (pushUser == null) { if (pushUser == null) {
// user does not have send any push keys // user does not have send any push keys

View file

@ -0,0 +1,8 @@
enum SetupProfile { standard, customized, maximum }
enum SecurityProfile { normal, strict }
extension SecurityProfileExtension on SecurityProfile {
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
}

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