mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-13 07:42:13 +00:00
Compare commits
No commits in common. "main" and "v0.2.12" have entirely different histories.
296 changed files with 7609 additions and 42494 deletions
4
.github/workflows/dev_github.yml
vendored
4
.github/workflows/dev_github.yml
vendored
|
|
@ -31,5 +31,5 @@ jobs:
|
|||
- name: flutter analyze
|
||||
run: flutter analyze
|
||||
|
||||
# - name: flutter test
|
||||
# run: flutter test
|
||||
- name: flutter test
|
||||
run: flutter test
|
||||
|
|
|
|||
68
.github/workflows/release_github.yml
vendored
Normal file
68
.github/workflows/release_github.yml
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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
7
.gitignore
vendored
|
|
@ -10,14 +10,8 @@
|
|||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
migrate_working_dir/
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
|
|
@ -55,4 +49,3 @@ android/.kotlin/
|
|||
devtools_options.yaml
|
||||
rust/target
|
||||
rust_dependencies/target
|
||||
fastlane/repo/status/running.json
|
||||
|
|
|
|||
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,56 +1,5 @@
|
|||
# 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
|
||||
|
||||
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# twonly
|
||||
|
||||
<a href="https://twonly.eu" rel="some text"><img src="metadata/en-US/images/featureGraphic.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
|
||||
<a href="https://twonly.eu" rel="some text"><img src="docs/header.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.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ analyzer:
|
|||
- "lib/core/**"
|
||||
- "lib/src/localization/**"
|
||||
- "rust_builder/"
|
||||
- "build/"
|
||||
- "dependencies/**"
|
||||
- "pubspec.yaml"
|
||||
- "**.arb"
|
||||
|
|
|
|||
2
android/.gitignore
vendored
2
android/.gitignore
vendored
|
|
@ -9,7 +9,5 @@ GeneratedPluginRegistrant.java
|
|||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
key.github.properties
|
||||
key.properties.backup
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
// START: FlutterFire Configuration
|
||||
id 'com.google.gms.google-services'
|
||||
// END: FlutterFire Configuration
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,32 +10,11 @@ import android.content.Context
|
|||
import io.crates.keyring.Keyring
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import android.os.Bundle
|
||||
import android.net.Uri
|
||||
import java.io.InputStream
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
private val CHANNEL = "eu.twonly/photo_picker"
|
||||
private var pendingResult: MethodChannel.Result? = null
|
||||
|
||||
private lateinit var pickMultipleMedia: ActivityResultLauncher<PickVisualMediaRequest>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
|
||||
if (uris.isNotEmpty()) {
|
||||
val uriStrings = uris.map { it.toString() }
|
||||
pendingResult?.success(uriStrings)
|
||||
} else {
|
||||
pendingResult?.success(emptyList<String>())
|
||||
}
|
||||
pendingResult = null
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
|
|
@ -56,36 +35,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
Keyring.initializeNdkContext(applicationContext)
|
||||
|
||||
MediaStoreChannel.configure(flutterEngine, applicationContext)
|
||||
VideoCompressionChannel.configure(flutterEngine, applicationContext)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"pickImages" -> {
|
||||
pendingResult = result
|
||||
pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
}
|
||||
"getUriBytes" -> {
|
||||
val uriString = call.argument<String>("uri")
|
||||
if (uriString != null) {
|
||||
try {
|
||||
val uri = Uri.parse(uriString)
|
||||
val inputStream: InputStream? = contentResolver.openInputStream(uri)
|
||||
if (inputStream != null) {
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
result.success(bytes)
|
||||
} else {
|
||||
result.error("UNAVAILABLE", "Could not open InputStream", null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.error("ERROR", e.message, null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_ARGUMENT", "URI string is null", null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal file
92
android/app/src/main/kotlin/eu/twonly/MediaStoreChannel.kt
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package eu.twonly
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object MediaStoreChannel {
|
||||
private const val CHANNEL = "eu.twonly/mediaStore"
|
||||
|
||||
fun configure(flutterEngine: FlutterEngine, context: Context) {
|
||||
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
try {
|
||||
if (call.method == "safeFileToDownload") {
|
||||
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
|
||||
val sourceFile = arguments["sourceFile"]
|
||||
if (sourceFile == null) {
|
||||
result.success(false)
|
||||
} else {
|
||||
val inputStream = FileInputStream(File(sourceFile))
|
||||
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
|
||||
|
||||
val savedUri = saveZipToDownloads(context, outputName, inputStream)
|
||||
if (savedUri != null) {
|
||||
result.success(savedUri.toString())
|
||||
} else {
|
||||
result.error("SAVE_FAILED", "Could not save ZIP", null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.error("EXCEPTION", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveZipToDownloads(
|
||||
context: Context,
|
||||
fileName: String = "archive.zip",
|
||||
sourceStream: InputStream
|
||||
): android.net.Uri? {
|
||||
val resolver = context.contentResolver
|
||||
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
}
|
||||
|
||||
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
} else {
|
||||
MediaStore.Files.getContentUri("external")
|
||||
}
|
||||
|
||||
val uri = resolver.insert(collection, contentValues) ?: return null
|
||||
|
||||
try {
|
||||
resolver.openOutputStream(uri).use { out: OutputStream? ->
|
||||
requireNotNull(out) { "Unable to open output stream" }
|
||||
sourceStream.use { input ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
out.flush()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
|
||||
resolver.update(uri, done, null, null)
|
||||
}
|
||||
|
||||
return uri
|
||||
} catch (e: Exception) {
|
||||
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=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
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Example signing credentials configuration for GitHub Releases.
|
||||
# Copy this file to 'key.github.properties' and fill in your actual credentials.
|
||||
# Do not commit the actual 'key.github.properties' file to version control.
|
||||
|
||||
storePassword=YOUR_GITHUB_RELEASE_STORE_PASSWORD
|
||||
keyPassword=YOUR_GITHUB_RELEASE_KEY_PASSWORD
|
||||
keyAlias=github-releases-signature
|
||||
storeFile=/absolute/path/to/your/github-release-keystore.jks
|
||||
|
|
@ -18,11 +18,11 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version '8.11.1' apply false
|
||||
id "com.android.application" version '8.9.1' apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM8.107 4.000L9.339 4.000L9.339 12.000L7.832 12.000L7.832 6.246L7.817 6.232L7.339 6.638L6.948 6.899L6.455 7.159L5.861 7.391L5.861 6.014L6.165 5.899L6.499 5.725L6.861 5.493L7.296 5.159L7.643 4.812L7.832 4.565L8.049 4.174L8.107 4.000Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 732 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.899 4.000L8.391 4.000L8.768 4.043L9.029 4.101L9.362 4.217L9.623 4.348L9.942 4.580L10.145 4.783L10.304 4.986L10.406 5.145L10.551 5.464L10.652 5.899L10.667 6.406L10.594 6.884L10.478 7.246L10.290 7.638L10.043 8.029L9.812 8.333L9.507 8.667L8.406 9.696L7.928 10.174L7.754 10.391L7.638 10.580L10.667 10.594L10.667 12.000L5.333 12.000L5.391 11.609L5.522 11.159L5.710 10.725L6.000 10.246L6.232 9.942L6.638 9.478L7.464 8.652L8.072 8.087L8.594 7.551L8.870 7.203L9.029 6.899L9.087 6.739L9.145 6.449L9.145 6.174L9.101 5.928L8.986 5.667L8.768 5.435L8.652 5.362L8.522 5.304L8.246 5.246L7.971 5.246L7.783 5.275L7.652 5.319L7.406 5.464L7.232 5.652L7.101 5.913L7.029 6.203L7.000 6.493L5.493 6.348L5.565 5.870L5.725 5.362L5.957 4.942L6.275 4.594L6.638 4.348L6.942 4.203L7.377 4.072L7.899 4.000Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.730 4.000L8.142 4.000L8.555 4.057L8.854 4.142L9.224 4.313L9.523 4.527L9.794 4.797L10.064 5.196L10.206 5.580L10.249 5.851L10.235 6.263L10.149 6.591L10.007 6.875L9.836 7.103L9.566 7.359L9.125 7.644L9.409 7.730L9.708 7.872L9.950 8.043L10.164 8.256L10.335 8.498L10.477 8.797L10.548 9.039L10.591 9.338L10.577 9.836L10.448 10.377L10.249 10.776L10.093 11.004L9.893 11.231L9.523 11.544L9.125 11.772L8.911 11.858L8.598 11.943L8.171 12.000L7.587 11.986L7.146 11.900L6.733 11.744L6.320 11.488L5.950 11.132L5.680 10.733L5.509 10.320L5.409 9.865L5.409 9.808L6.847 9.637L6.861 9.765L6.947 10.064L7.004 10.192L7.160 10.420L7.416 10.633L7.630 10.733L7.843 10.776L8.171 10.762L8.327 10.719L8.498 10.633L8.797 10.363L8.968 10.064L9.039 9.822L9.068 9.609L9.053 9.167L8.954 8.826L8.769 8.541L8.512 8.327L8.214 8.214L7.872 8.199L7.331 8.299L7.488 7.132L7.929 7.089L8.256 6.975L8.384 6.890L8.569 6.705L8.683 6.505L8.754 6.221L8.754 5.979L8.698 5.737L8.598 5.552L8.427 5.381L8.242 5.281L8.000 5.224L7.772 5.224L7.445 5.324L7.317 5.409L7.132 5.594L7.032 5.751L6.947 5.964L6.890 6.278L5.523 6.036L5.623 5.623L5.794 5.181L5.950 4.911L6.235 4.584L6.591 4.327L6.961 4.157L7.302 4.057L7.730 4.000Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -1 +1 @@
|
|||
Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe
|
||||
Subproject commit e0c6a9617a20a8d6bc1ad4c6b9c2e229feb5f37a
|
||||
BIN
docs/header.png
Normal file
BIN
docs/header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 800 KiB |
|
|
@ -1,2 +0,0 @@
|
|||
json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json")
|
||||
package_name("eu.twonly") # Your application ID
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Submit a new App Bundle to the Google Play Internal Track"
|
||||
lane :internal do
|
||||
# This lane assumes that `flutter build appbundle` has already been run from the flutter root.
|
||||
upload_to_play_store(
|
||||
track: 'internal',
|
||||
aab: 'build/app/outputs/bundle/release/app-release.aab',
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true
|
||||
)
|
||||
end
|
||||
|
||||
desc "Build the application locally and upload it as a new GitHub release"
|
||||
lane :release_github do
|
||||
# Read pubspec.yaml to get the version
|
||||
pubspec_path = File.expand_path("../pubspec.yaml", __dir__)
|
||||
unless File.exist?(pubspec_path)
|
||||
UI.user_error!("Could not find pubspec.yaml at #{pubspec_path}")
|
||||
end
|
||||
|
||||
pubspec_content = File.read(pubspec_path)
|
||||
version_match = pubspec_content.match(/^version:\s*([^+]+)/)
|
||||
unless version_match
|
||||
UI.user_error!("Could not extract version from pubspec.yaml")
|
||||
end
|
||||
|
||||
version = version_match[1].strip
|
||||
tag_name = "v#{version}"
|
||||
UI.message("Extracted version: #{version} (tag: #{tag_name})")
|
||||
|
||||
# Load release notes from CHANGELOG.md
|
||||
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
|
||||
release_notes = "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
|
||||
|
|
@ -15,11 +15,6 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
// Store the current timestamp in Keychain for iOS FCM messaging tracking
|
||||
let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000)
|
||||
writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs)
|
||||
NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)")
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
|
||||
guard bestAttemptContent.userInfo as? [String: Any] != nil,
|
||||
|
|
@ -193,7 +188,6 @@ func readFromKeychain(key: String) -> String? {
|
|||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrService as String: "flutter_secure_storage_service",
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group
|
||||
|
|
@ -211,36 +205,6 @@ func readFromKeychain(key: String) -> String? {
|
|||
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) {
|
||||
let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language
|
||||
|
||||
|
|
|
|||
33
ios/Podfile
33
ios/Podfile
|
|
@ -28,9 +28,17 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
|
|||
|
||||
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
|
||||
pod 'SwiftProtobuf'
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
|
|
@ -68,28 +76,6 @@ post_install do |installer|
|
|||
|
||||
## dart: PermissionGroup.mediaLibrary
|
||||
'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
|
||||
|
|
@ -97,6 +83,5 @@ post_install do |installer|
|
|||
end
|
||||
|
||||
target 'NotificationService' do
|
||||
pod 'SwiftProtobuf'
|
||||
# pod 'Firebase/Messaging'
|
||||
end
|
||||
|
|
|
|||
348
ios/Podfile.lock
348
ios/Podfile.lock
|
|
@ -1,18 +1,136 @@
|
|||
PODS:
|
||||
- app_links (7.0.0):
|
||||
- Flutter
|
||||
- audio_waveforms (0.0.1):
|
||||
- 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):
|
||||
- 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_image_compress_common (1.0.0):
|
||||
- Flutter
|
||||
- Mantle
|
||||
- SDWebImage
|
||||
- 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
|
||||
- flutter_volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- google_mlkit_barcode_scanning (0.14.2):
|
||||
- Flutter
|
||||
- google_mlkit_commons
|
||||
|
|
@ -24,6 +142,33 @@ PODS:
|
|||
- Flutter
|
||||
- google_mlkit_commons
|
||||
- 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):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
|
|
@ -40,16 +185,54 @@ PODS:
|
|||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (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/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- 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/Reachability (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/SwizzlerTestHelpers (8.1.0):
|
||||
- GoogleUtilities/MethodSwizzler
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- 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/demux (= 1.5.0)
|
||||
- libwebp/mux (= 1.5.0)
|
||||
|
|
@ -62,6 +245,9 @@ PODS:
|
|||
- libwebp/sharpyuv (1.5.0)
|
||||
- libwebp/webp (1.5.0):
|
||||
- libwebp/sharpyuv
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Mantle (2.2.0):
|
||||
- Mantle/extobjc (= 2.2.0)
|
||||
- Mantle/extobjc (2.2.0)
|
||||
|
|
@ -90,11 +276,15 @@ PODS:
|
|||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- pro_video_editor (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- restart_app (1.7.3):
|
||||
- Flutter
|
||||
- rust_lib_twonly (0.0.1):
|
||||
- Flutter
|
||||
- screen_protector (1.5.1):
|
||||
|
|
@ -107,29 +297,89 @@ PODS:
|
|||
- SDWebImageWebPCoder (0.15.0):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- SwiftProtobuf (1.38.0)
|
||||
- Sentry/HybridSDK (8.58.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):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/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`)
|
||||
- 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_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_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_commons (from `.symlinks/plugins/google_mlkit_commons/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`)
|
||||
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
|
||||
- restart_app (from `.symlinks/plugins/restart_app/ios`)
|
||||
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
|
||||
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- 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`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Firebase
|
||||
- FirebaseAnalytics
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- GoogleAdsOnDeviceConversion
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
|
|
@ -147,54 +397,136 @@ SPEC REPOS:
|
|||
- ScreenProtectorKit
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
- SwiftProtobuf
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
audio_waveforms:
|
||||
: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:
|
||||
: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:
|
||||
:path: Flutter
|
||||
flutter_image_compress_common:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
gal:
|
||||
:path: ".symlinks/plugins/gal/darwin"
|
||||
google_mlkit_barcode_scanning:
|
||||
:path: ".symlinks/plugins/google_mlkit_barcode_scanning/ios"
|
||||
google_mlkit_commons:
|
||||
:path: ".symlinks/plugins/google_mlkit_commons/ios"
|
||||
google_mlkit_face_detection:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
pro_video_editor:
|
||||
:path: ".symlinks/plugins/pro_video_editor/ios"
|
||||
restart_app:
|
||||
:path: ".symlinks/plugins/restart_app/ios"
|
||||
rust_lib_twonly:
|
||||
:path: ".symlinks/plugins/rust_lib_twonly/ios"
|
||||
screen_protector:
|
||||
:path: ".symlinks/plugins/screen_protector/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/workmanager_apple/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
|
||||
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
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_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
|
||||
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
google_mlkit_barcode_scanning: 12d8422d8f7b00726dedf9cac00188a2b98750c2
|
||||
google_mlkit_commons: a5e4ffae5bc59ea4c7b9025dc72cb6cb79dc1166
|
||||
google_mlkit_face_detection: ee4b72cfae062b4c972204be955d83055a4bfd36
|
||||
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: b1eee21a41c57704fe72483b15c85cb2c0cd7444
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
MLImage: 0de5c6c2bf9e93b80ef752e2797f0836f03b58c0
|
||||
MLKitBarcodeScanning: 39de223e7b1b8a8fbf10816a536dd292d8a39343
|
||||
|
|
@ -202,17 +534,27 @@ SPEC CHECKSUMS:
|
|||
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
|
||||
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
|
||||
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
|
||||
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
|
||||
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||
SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb
|
||||
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
|
||||
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
|
||||
|
||||
PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf
|
||||
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
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 */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -115,7 +114,6 @@
|
|||
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
|
@ -167,7 +165,6 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */,
|
||||
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */,
|
||||
);
|
||||
|
|
@ -203,7 +200,6 @@
|
|||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
|
|
@ -311,9 +307,6 @@
|
|||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
|
|
@ -385,9 +378,6 @@
|
|||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
|
|
@ -1319,18 +1309,6 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -5,24 +5,6 @@
|
|||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
|
|||
|
|
@ -1,194 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -36,9 +36,7 @@ import workmanager_apple
|
|||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
override func application(
|
||||
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||
|
||||
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
|
||||
if sharingIntent.hasSameSchemePrefix(url: url) {
|
||||
|
|
@ -60,8 +58,7 @@ import workmanager_apple
|
|||
NSLog(
|
||||
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
|
||||
response.notification.request.content.userInfo)
|
||||
super.userNotificationCenter(
|
||||
center, didReceive: response, withCompletionHandler: completionHandler)
|
||||
//...
|
||||
}
|
||||
|
||||
override func userNotificationCenter(
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@
|
|||
<string>Use your microphone to enable audio when making videos.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<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>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
|
|
|||
10
lib/app.dart
10
lib/app.dart
|
|
@ -137,14 +137,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
bool _isLoaded = false;
|
||||
bool _isTwonlyLocked = true;
|
||||
bool _wasLogged = true;
|
||||
late int _initialPage;
|
||||
|
||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialPage = widget.initialPage;
|
||||
Log.info('AppWidgetState: initState started');
|
||||
initAsync();
|
||||
}
|
||||
|
|
@ -152,12 +150,6 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
Future<void> initAsync() async {
|
||||
Log.info('AppWidgetState: initAsync started');
|
||||
if (userService.isUserCreated) {
|
||||
if (_initialPage != 0) {
|
||||
final count = await twonlyDB.contactsDao.getContactsCount();
|
||||
if (count == 0) {
|
||||
_initialPage = 0;
|
||||
}
|
||||
}
|
||||
try {
|
||||
unawaited(FirebaseMessaging.instance.requestPermission());
|
||||
} catch (e) {
|
||||
|
|
@ -218,7 +210,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
);
|
||||
} else {
|
||||
child = HomeView(
|
||||
initialPage: _initialPage,
|
||||
initialPage: widget.initialPage,
|
||||
);
|
||||
}
|
||||
} else if (_showOnboarding) {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|||
|
||||
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
|
||||
// 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({
|
||||
required int callbackId,
|
||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||
|
|
@ -41,7 +39,6 @@ Future<void> initFlutterCallbacks({
|
|||
required FutureOr<Uint8List?> Function(PlatformInt64)
|
||||
userDiscoveryGetContactPromotion,
|
||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||
callbackId: callbackId,
|
||||
loggingGetStreamSink: loggingGetStreamSink,
|
||||
userDiscoverySignData: userDiscoverySignData,
|
||||
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
|
||||
|
|
|
|||
|
|
@ -9,45 +9,36 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|||
class FlutterUserDiscovery {
|
||||
const FlutterUserDiscovery();
|
||||
|
||||
static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
|
||||
RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
|
||||
callbackId: callbackId,
|
||||
);
|
||||
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
|
||||
|
||||
static Future<List<Uint8List>> getNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> receivedVersion,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
|
||||
callbackId: callbackId,
|
||||
contactId: contactId,
|
||||
receivedVersion: receivedVersion,
|
||||
);
|
||||
|
||||
static Future<void> handleNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
required List<Uint8List> messages,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
||||
callbackId: callbackId,
|
||||
contactId: contactId,
|
||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||
messages: messages,
|
||||
);
|
||||
|
||||
static Future<void> initializeOrUpdate({
|
||||
required int callbackId,
|
||||
required int threshold,
|
||||
required PlatformInt64 userId,
|
||||
required List<int> publicKey,
|
||||
required bool sharePromotion,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
|
||||
callbackId: callbackId,
|
||||
threshold: threshold,
|
||||
userId: userId,
|
||||
publicKey: publicKey,
|
||||
|
|
@ -55,23 +46,19 @@ class FlutterUserDiscovery {
|
|||
);
|
||||
|
||||
static Future<Uint8List?> shouldRequestNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> version,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
|
||||
callbackId: callbackId,
|
||||
contactId: contactId,
|
||||
version: version,
|
||||
);
|
||||
|
||||
static Future<void> updateVerificationStateForUser({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
}) => RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
|
||||
callbackId: callbackId,
|
||||
contactId: contactId,
|
||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -87,20 +87,16 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
|
||||
abstract class RustLibApi extends BaseApi {
|
||||
Future<Uint8List>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
|
||||
required int callbackId,
|
||||
});
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
|
||||
|
||||
Future<List<Uint8List>>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> receivedVersion,
|
||||
});
|
||||
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
required List<Uint8List> messages,
|
||||
|
|
@ -108,7 +104,6 @@ abstract class RustLibApi extends BaseApi {
|
|||
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
||||
required int callbackId,
|
||||
required int threshold,
|
||||
required PlatformInt64 userId,
|
||||
required List<int> publicKey,
|
||||
|
|
@ -117,20 +112,17 @@ abstract class RustLibApi extends BaseApi {
|
|||
|
||||
Future<Uint8List?>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> version,
|
||||
});
|
||||
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
});
|
||||
|
||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||
required int callbackId,
|
||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||
|
|
@ -250,14 +242,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
|
||||
@override
|
||||
Future<Uint8List>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({
|
||||
required int callbackId,
|
||||
}) {
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
|
|
@ -271,7 +260,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta,
|
||||
argValues: [callbackId],
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -281,13 +270,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersionConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_get_current_version",
|
||||
argNames: ["callbackId"],
|
||||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<Uint8List>>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> receivedVersion,
|
||||
}) {
|
||||
|
|
@ -295,7 +283,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
sse_encode_list_prim_u_8_loose(receivedVersion, serializer);
|
||||
pdeCallFfi(
|
||||
|
|
@ -311,7 +298,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta,
|
||||
argValues: [callbackId, contactId, receivedVersion],
|
||||
argValues: [contactId, receivedVersion],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -321,13 +308,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_get_new_messages",
|
||||
argNames: ["callbackId", "contactId", "receivedVersion"],
|
||||
argNames: ["contactId", "receivedVersion"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
required List<Uint8List> messages,
|
||||
|
|
@ -336,7 +322,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
sse_encode_opt_box_autoadd_i_64(
|
||||
publicKeyVerifiedTimestamp,
|
||||
|
|
@ -356,12 +341,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
|
||||
argValues: [
|
||||
callbackId,
|
||||
contactId,
|
||||
publicKeyVerifiedTimestamp,
|
||||
messages,
|
||||
],
|
||||
argValues: [contactId, publicKeyVerifiedTimestamp, messages],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -371,18 +351,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_handle_new_messages",
|
||||
argNames: [
|
||||
"callbackId",
|
||||
"contactId",
|
||||
"publicKeyVerifiedTimestamp",
|
||||
"messages",
|
||||
],
|
||||
argNames: ["contactId", "publicKeyVerifiedTimestamp", "messages"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({
|
||||
required int callbackId,
|
||||
required int threshold,
|
||||
required PlatformInt64 userId,
|
||||
required List<int> publicKey,
|
||||
|
|
@ -392,7 +366,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_u_8(threshold, serializer);
|
||||
sse_encode_i_64(userId, serializer);
|
||||
sse_encode_list_prim_u_8_loose(publicKey, serializer);
|
||||
|
|
@ -410,7 +383,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta,
|
||||
argValues: [callbackId, threshold, userId, publicKey, sharePromotion],
|
||||
argValues: [threshold, userId, publicKey, sharePromotion],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -420,19 +393,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_initialize_or_update",
|
||||
argNames: [
|
||||
"callbackId",
|
||||
"threshold",
|
||||
"userId",
|
||||
"publicKey",
|
||||
"sharePromotion",
|
||||
],
|
||||
argNames: ["threshold", "userId", "publicKey", "sharePromotion"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Uint8List?>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
required List<int> version,
|
||||
}) {
|
||||
|
|
@ -440,7 +406,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
sse_encode_list_prim_u_8_loose(version, serializer);
|
||||
pdeCallFfi(
|
||||
|
|
@ -456,7 +421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta,
|
||||
argValues: [callbackId, contactId, version],
|
||||
argValues: [contactId, version],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -466,13 +431,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_should_request_new_messages",
|
||||
argNames: ["callbackId", "contactId", "version"],
|
||||
argNames: ["contactId", "version"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void>
|
||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||
}) {
|
||||
|
|
@ -480,7 +444,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_i_64(contactId, serializer);
|
||||
sse_encode_opt_box_autoadd_i_64(
|
||||
publicKeyVerifiedTimestamp,
|
||||
|
|
@ -499,7 +462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta,
|
||||
argValues: [callbackId, contactId, publicKeyVerifiedTimestamp],
|
||||
argValues: [contactId, publicKeyVerifiedTimestamp],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
|
|
@ -509,12 +472,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "flutter_user_discovery_update_verification_state_for_user",
|
||||
argNames: ["callbackId", "contactId", "publicKeyVerifiedTimestamp"],
|
||||
argNames: ["contactId", "publicKeyVerifiedTimestamp"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||
required int callbackId,
|
||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
||||
|
|
@ -551,7 +513,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(callbackId, serializer);
|
||||
sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
|
||||
loggingGetStreamSink,
|
||||
serializer,
|
||||
|
|
@ -625,7 +586,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
),
|
||||
constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta,
|
||||
argValues: [
|
||||
callbackId,
|
||||
loggingGetStreamSink,
|
||||
userDiscoverySignData,
|
||||
userDiscoveryVerifySignature,
|
||||
|
|
@ -651,7 +611,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
const TaskConstMeta(
|
||||
debugName: "init_flutter_callbacks",
|
||||
argNames: [
|
||||
"callbackId",
|
||||
"loggingGetStreamSink",
|
||||
"userDiscoverySignData",
|
||||
"userDiscoveryVerifySignature",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
final int isolateCallbackId = Random().nextInt(0x7FFFFFFF);
|
||||
|
||||
class AppEnvironment {
|
||||
static late String cacheDir;
|
||||
static late String supportDir;
|
||||
|
|
@ -35,6 +32,6 @@ class AppState {
|
|||
static bool isInBackgroundTask = false;
|
||||
static bool allowErrorTrackingViaSentry = false;
|
||||
static bool gotMessageFromServer = false;
|
||||
static int latestAppVersionId = 116;
|
||||
static bool hasCameraPermissions = false;
|
||||
static int latestAppVersionId = 115;
|
||||
|
||||
}
|
||||
|
|
|
|||
158
lib/main.dart
158
lib/main.dart
|
|
@ -1,6 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
|
@ -11,24 +15,34 @@ import 'package:twonly/core/frb_generated.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/callbacks/callbacks.dart';
|
||||
import 'package:twonly/src/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/image_editor.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.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/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/backup.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/memories/memories.service.dart';
|
||||
import 'package:twonly/src/services/migrations.service.dart';
|
||||
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||
import 'package:twonly/src/utils/avatars.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
import 'package:twonly/src/utils/startup_guard.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
|
||||
final _initMutex = Mutex();
|
||||
|
||||
|
|
@ -76,7 +90,7 @@ void main() async {
|
|||
unawaited(StartupGuard.markAppStartup());
|
||||
|
||||
var storageError = await twonlyMinimumInitialization();
|
||||
await FcmNotificationService.initStartup();
|
||||
await initFCMService();
|
||||
|
||||
var userExists = false;
|
||||
|
||||
|
|
@ -109,8 +123,6 @@ void main() async {
|
|||
unawaited(initFileDownloader());
|
||||
|
||||
if (userExists) {
|
||||
unawaited(FcmNotificationService.initAfterUserLoaded());
|
||||
|
||||
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
||||
AppState.allowErrorTrackingViaSentry = true;
|
||||
await SentryFlutter.init(
|
||||
|
|
@ -156,6 +168,144 @@ 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 {
|
||||
Log.info('Post startup started.');
|
||||
unawaited(MemoriesService.prewarmCache());
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
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/user_discovery.callbacks.dart';
|
||||
|
||||
Future<void> initFlutterCallbacksForRust() async {
|
||||
await initFlutterCallbacks(
|
||||
callbackId: isolateCallbackId,
|
||||
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
|
||||
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
|
||||
userDiscoveryGetShareForContact:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -16,8 +15,7 @@ class LoggingCallbacks {
|
|||
Log.info(log.split('INFO ')[1]);
|
||||
} else if (log.contains('DEBUG ')) {
|
||||
Log.info(log.split('DEBUG ')[1]);
|
||||
} else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
// ignore: avoid_print
|
||||
} else if (kDebugMode) {
|
||||
print(log);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,14 +35,9 @@ class Routes {
|
|||
'/settings/privacy/block_users';
|
||||
static const String settingsPrivacyUserDiscovery =
|
||||
'/settings/privacy/user_discovery';
|
||||
static const String settingsPrivacyProfileSelection =
|
||||
'/settings/privacy/profile_selection';
|
||||
static const String settingsNotification = '/settings/notification';
|
||||
static const String settingsStorage = '/settings/storage_data';
|
||||
static const String settingsStorageManage = '/settings/storage_data/manage';
|
||||
static const String settingsStorageImport = '/settings/storage_data/import';
|
||||
static const String settingsStorageImportGallery =
|
||||
'/settings/storage_data/import_gallery';
|
||||
static const String settingsStorageExport = '/settings/storage_data/export';
|
||||
static const String settingsHelp = '/settings/help';
|
||||
static const String settingsHelpFaq = '/settings/help/faq';
|
||||
|
|
@ -62,7 +57,5 @@ class Routes {
|
|||
'/settings/developer/automated_testing';
|
||||
static const String settingsDeveloperReduceFlames =
|
||||
'/settings/developer/reduce_flames';
|
||||
static const String settingsDeveloperInformations =
|
||||
'/settings/developer/informations';
|
||||
static const String settingsInvite = '/settings/invite';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,4 @@ class SecureStorageKeys {
|
|||
// Not required for backup...
|
||||
static const String receivingPushKeys = 'push_keys_receiving';
|
||||
static const String sendingPushKeys = 'push_keys_sending';
|
||||
static const String lastFcmMessageTimestamp = 'last_fcm_message_timestamp';
|
||||
static const String lastServerMessageTimestamp =
|
||||
'last_server_message_timestamp';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,13 +103,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
return select(contacts).get();
|
||||
}
|
||||
|
||||
Future<int> getContactsCount() async {
|
||||
final count = contacts.userId.count();
|
||||
final query = selectOnly(contacts)..addColumns([count]);
|
||||
final result = await query.map((row) => row.read(count)).getSingle();
|
||||
return result ?? 0;
|
||||
}
|
||||
|
||||
Stream<int?> watchContactsBlocked() {
|
||||
final count = contacts.userId.count();
|
||||
final query = selectOnly(contacts)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:clock/clock.dart' show clock;
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/flame.service.dart';
|
||||
|
|
@ -294,27 +292,6 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
return query.map((row) => row.readTable(groups)).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Group?> createOrGetDirectChat(int contactId) async {
|
||||
var directChat = await getDirectChat(contactId);
|
||||
if (directChat == null) {
|
||||
final contact = await attachedDatabase.contactsDao.getContactById(
|
||||
contactId,
|
||||
);
|
||||
if (contact == null) {
|
||||
Log.error('Contact $contactId not found, cannot create direct chat');
|
||||
return null;
|
||||
}
|
||||
await createNewDirectChat(
|
||||
contactId,
|
||||
GroupsCompanion(
|
||||
groupName: Value(getContactDisplayName(contact)),
|
||||
),
|
||||
);
|
||||
directChat = await getDirectChat(contactId);
|
||||
}
|
||||
return directChat;
|
||||
}
|
||||
|
||||
Stream<int> watchSumTotalMediaCounter() {
|
||||
final query = selectOnly(groups)
|
||||
..addColumns([groups.totalMediaCounter.sum()]);
|
||||
|
|
@ -328,16 +305,12 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
String groupId,
|
||||
DateTime newLastMessage,
|
||||
) async {
|
||||
final now = clock.now();
|
||||
final clampedLastMessage = newLastMessage.isAfter(now)
|
||||
? now
|
||||
: newLastMessage;
|
||||
await (update(groups)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.lastMessageExchange.isSmallerThanValue(clampedLastMessage)),
|
||||
(t.lastMessageExchange.isSmallerThanValue(newLastMessage)),
|
||||
))
|
||||
.write(GroupsCompanion(lastMessageExchange: Value(clampedLastMessage)));
|
||||
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
|
||||
}
|
||||
|
||||
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
|
|
@ -28,8 +27,7 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
KeyVerificationDao(super.db);
|
||||
|
||||
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
||||
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
|
||||
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
|
||||
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
|
||||
return (select(
|
||||
verificationTokens,
|
||||
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
||||
|
|
@ -217,7 +215,6 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
);
|
||||
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||
callbackId: isolateCallbackId,
|
||||
contactId: contactId,
|
||||
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
|
@ -226,40 +223,4 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,15 +114,16 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
|
||||
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
|
||||
return (select(mediaFiles)..where(
|
||||
(t) =>
|
||||
t.stored.equals(true) &
|
||||
(t.storedFileHash.isNull() |
|
||||
t.hasCropAnalyzed.equals(false) |
|
||||
(t.hasThumbnail.equals(false) &
|
||||
t.type.equals(MediaType.audio.name).not()) |
|
||||
t.sizeInBytes.isNull()),
|
||||
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllUnanalyzedStoredMediaFiles() async {
|
||||
return (select(mediaFiles)..where(
|
||||
(t) => t.stored.equals(true) & t.hasCropAnalyzed.equals(false),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
|
@ -141,9 +142,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
||||
final query =
|
||||
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
|
||||
..groupBy([
|
||||
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)')
|
||||
]);
|
||||
..groupBy([mediaFiles.storedFileHash]);
|
||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||
}
|
||||
|
||||
|
|
@ -186,23 +185,4 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
final rows = await query.get();
|
||||
return rows.map((row) => row.readTable(db.messages).messageId).toList();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getMediaByHash(Uint8List hash) async {
|
||||
final query = select(db.mediaFiles)
|
||||
..where((t) => t.storedFileHash.equals(hash));
|
||||
return query.get();
|
||||
}
|
||||
|
||||
Future<Map<MediaType, int>> getStorageStats() async {
|
||||
final rows = await select(mediaFiles).get();
|
||||
final stats = <MediaType, int>{};
|
||||
|
||||
for (final row in rows) {
|
||||
final type = row.type;
|
||||
final size = row.sizeInBytes ?? 0;
|
||||
stats[type] = (stats[type] ?? 0) + size;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
])..where(
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
|
|
@ -61,8 +60,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
messages.mediaId.isNotNull() &
|
||||
messages.senderId.isNotNull() &
|
||||
messages.type.equals(MessageType.media.name),
|
||||
)
|
||||
..orderBy([OrderingTerm.asc(messages.createdAt)]);
|
||||
);
|
||||
return query.map((row) => row.readTable(messages)).watch();
|
||||
}
|
||||
|
||||
|
|
@ -95,33 +93,24 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
milliseconds: group!.deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
);
|
||||
final query =
|
||||
select(messages).join([
|
||||
leftOuterJoin(
|
||||
mediaFiles,
|
||||
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||
),
|
||||
])
|
||||
..where(
|
||||
messages.groupId.equals(groupId) &
|
||||
(messages.openedAt.isBiggerThanValue(deletionTime) |
|
||||
messages.openedAt.isNull() |
|
||||
messages.mediaStored.equals(true)) &
|
||||
(messages.isDeletedFromSender.equals(true) |
|
||||
(messages.type.equals(MessageType.text.name).not() &
|
||||
messages.type.equals(MessageType.media.name).not()) |
|
||||
(messages.type.equals(MessageType.text.name) &
|
||||
messages.content.isNotNull()) |
|
||||
(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();
|
||||
return ((select(messages)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
// messages in groups will only be removed in case all members have received it...
|
||||
// so ensuring that this message is not shown in the messages anymore
|
||||
(t.openedAt.isBiggerThanValue(deletionTime) |
|
||||
t.openedAt.isNull() |
|
||||
t.mediaStored.equals(true)) &
|
||||
(t.isDeletedFromSender.equals(true) |
|
||||
(t.type.equals(MessageType.text.name).not() |
|
||||
t.type.equals(MessageType.media.name).not()) |
|
||||
(t.type.equals(MessageType.text.name) &
|
||||
t.content.isNotNull()) |
|
||||
(t.type.equals(MessageType.media.name) &
|
||||
t.mediaId.isNotNull())),
|
||||
))
|
||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) {
|
||||
|
|
@ -164,26 +153,18 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
final groupIds = entry.value;
|
||||
|
||||
final deletedCount =
|
||||
await (delete(messages)..where(
|
||||
(m) =>
|
||||
m.groupId.isIn(groupIds) &
|
||||
((m.mediaStored.equals(true) &
|
||||
m.isDeletedFromSender.equals(true)) |
|
||||
(m.mediaStored.equals(true) &
|
||||
m.isDeletedFromSender.equals(true) |
|
||||
m.mediaStored.equals(false)) &
|
||||
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
|
||||
((m.openedByAll.isNotNull() &
|
||||
m.openedByAll.isSmallerThanValue(deletionTime)) |
|
||||
(m.openedByAll.isSmallerThanValue(deletionTime) |
|
||||
(m.isDeletedFromSender.equals(true) &
|
||||
m.createdAt.isSmallerThanValue(deletionTime))),
|
||||
))
|
||||
.go();
|
||||
|
||||
if (deletedCount > 0) {
|
||||
Log.info(
|
||||
'Deleted $deletedCount messages for groups $groupIds due to retention policy.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,70 +249,41 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Future<void> handleMessagesOpened(
|
||||
Value<int> contactId,
|
||||
int contactId,
|
||||
List<String> messageIds,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
await batch((batch) async {
|
||||
for (final messageId in messageIds) {
|
||||
try {
|
||||
var actionTimestamp = timestamp;
|
||||
final msg = await getMessageById(messageId).getSingleOrNull();
|
||||
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
|
||||
Log.warn(
|
||||
'Receiver clock skew detected for message $messageId. '
|
||||
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
|
||||
'Clamping to creation time.',
|
||||
);
|
||||
actionTimestamp = msg.createdAt;
|
||||
}
|
||||
|
||||
final ts = actionTimestamp;
|
||||
await transaction(() async {
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
batch.insert(
|
||||
messageActions,
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
contactId: contactId,
|
||||
contactId: Value(contactId),
|
||||
type: const Value(MessageActionType.openedAt),
|
||||
actionAt: Value(ts),
|
||||
actionAt: Value(timestamp),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
for (final messageId in messageIds) {
|
||||
final isOpenedByAll = await haveAllMembers(
|
||||
messageId,
|
||||
MessageActionType.openedAt,
|
||||
);
|
||||
await (update(
|
||||
messages,
|
||||
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||
final now = clock.now();
|
||||
|
||||
batch.update(
|
||||
twonlyDB.messages,
|
||||
MessagesCompanion(
|
||||
openedAt: Value(ts),
|
||||
openedByAll: Value(isOpenedByAll ? ts : null),
|
||||
openedAt: Value(now),
|
||||
openedByAll: Value(isOpenedByAll ? now : null),
|
||||
),
|
||||
where: (tbl) => tbl.messageId.equals(messageId),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Read-back verification: confirm the write was persisted.
|
||||
final verified = await getMessageById(messageId).getSingleOrNull();
|
||||
if (verified != null && verified.openedAt == null) {
|
||||
Log.warn(
|
||||
'handleMessagesOpened read-back failed for $messageId, retrying',
|
||||
);
|
||||
await (update(
|
||||
messages,
|
||||
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||
MessagesCompanion(
|
||||
openedAt: Value(actionTimestamp),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'handleMessagesOpened completed for message $messageId',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('handleMessagesOpened failed for $messageId: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleMessageAckByServer(
|
||||
|
|
@ -339,7 +291,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
await transaction(() async {
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
|
|
@ -350,16 +301,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(ackByServer: Value(timestamp)),
|
||||
MessagesCompanion(ackByServer: Value(clock.now())),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> haveAllMembers(
|
||||
String messageId,
|
||||
MessageActionType action,
|
||||
) async {
|
||||
try {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
|
|
@ -370,36 +319,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
|
||||
final actions =
|
||||
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();
|
||||
|
||||
return members.length == actions.length;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateMessageId(
|
||||
String messageId,
|
||||
MessagesCompanion updatedValues,
|
||||
) async {
|
||||
final count = await (update(
|
||||
await (update(
|
||||
messages,
|
||||
)..where((c) => c.messageId.equals(messageId))).write(updatedValues);
|
||||
Log.info('Updated $count message(s) with messageId $messageId');
|
||||
}
|
||||
|
||||
Future<void> updateMessagesByMediaId(
|
||||
String mediaId,
|
||||
MessagesCompanion updatedValues,
|
||||
) async {
|
||||
final count = await (update(
|
||||
) {
|
||||
return (update(
|
||||
messages,
|
||||
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
|
||||
Log.info('Updated $count message(s) with mediaId $mediaId');
|
||||
}
|
||||
|
||||
Future<Message?> insertMessage(MessagesCompanion message) async {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.error('updateReaction: Message $messageId not found!');
|
||||
return;
|
||||
}
|
||||
if (msg.groupId != groupId) {
|
||||
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId');
|
||||
return;
|
||||
}
|
||||
if (msg == null || msg.groupId != groupId) return;
|
||||
|
||||
try {
|
||||
if (remove) {
|
||||
|
|
|
|||
|
|
@ -228,12 +228,6 @@ 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() =>
|
||||
select(userDiscoveryAnnouncedUsers).watch();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class DriftLoggingInterceptor extends QueryInterceptor {
|
||||
bool get _isEnabled {
|
||||
try {
|
||||
if (!userService.isUserCreated) return false;
|
||||
return userService.currentUser.enableDatabaseLogging;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _findUuids(dynamic value) {
|
||||
if (value == null) return const [];
|
||||
final uuids = <String>[];
|
||||
final uuidRegex = RegExp(
|
||||
'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',
|
||||
);
|
||||
if (value is String) {
|
||||
for (final match in uuidRegex.allMatches(value)) {
|
||||
uuids.add(match.group(0)!);
|
||||
}
|
||||
} else if (value is Iterable) {
|
||||
for (final element in value) {
|
||||
uuids.addAll(_findUuids(element));
|
||||
}
|
||||
} else if (value is Map) {
|
||||
for (final element in value.values) {
|
||||
uuids.addAll(_findUuids(element));
|
||||
}
|
||||
} else {
|
||||
final str = value.toString();
|
||||
for (final match in uuidRegex.allMatches(str)) {
|
||||
uuids.add(match.group(0)!);
|
||||
}
|
||||
}
|
||||
return uuids.toSet().toList();
|
||||
}
|
||||
|
||||
Future<T> _run<T>(
|
||||
String operation,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
Future<T> Function() query,
|
||||
) async {
|
||||
if (!_isEnabled) {
|
||||
return query();
|
||||
}
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
final result = await query();
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
final uuids = _findUuids(args);
|
||||
if (uuids.isNotEmpty) {
|
||||
Log.info(
|
||||
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement" | UUIDs: $uuids',
|
||||
);
|
||||
} else {
|
||||
Log.info(
|
||||
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement"',
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
final uuids = _findUuids(args);
|
||||
if (uuids.isNotEmpty) {
|
||||
Log.info(
|
||||
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement" | UUIDs: $uuids',
|
||||
);
|
||||
} else {
|
||||
Log.info(
|
||||
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement"',
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run('INSERT', statement, args, () => executor.runInsert(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run('UPDATE', statement, args, () => executor.runUpdate(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run('DELETE', statement, args, () => executor.runDelete(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(
|
||||
QueryExecutor executor,
|
||||
String statement,
|
||||
List<Object?> args,
|
||||
) {
|
||||
return _run('CUSTOM', statement, args, () => executor.runCustom(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(
|
||||
QueryExecutor executor,
|
||||
BatchedStatements statements,
|
||||
) async {
|
||||
if (!_isEnabled) {
|
||||
return executor.runBatched(statements);
|
||||
}
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
await executor.runBatched(statements);
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
final uuids = <String>[];
|
||||
for (final batchArg in statements.arguments) {
|
||||
uuids.addAll(_findUuids(batchArg.arguments));
|
||||
}
|
||||
final statementsStr = statements.statements.join('; ');
|
||||
if (uuids.isNotEmpty) {
|
||||
Log.info(
|
||||
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr" | UUIDs: $uuids',
|
||||
);
|
||||
} else {
|
||||
Log.info(
|
||||
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr"',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
final uuids = <String>[];
|
||||
for (final batchArg in statements.arguments) {
|
||||
uuids.addAll(_findUuids(batchArg.arguments));
|
||||
}
|
||||
final statementsStr = statements.statements.join('; ');
|
||||
if (uuids.isNotEmpty) {
|
||||
Log.info(
|
||||
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr" | UUIDs: $uuids',
|
||||
);
|
||||
} else {
|
||||
Log.info(
|
||||
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr"',
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -3,13 +3,14 @@ import 'dart:convert';
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
|
||||
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
|
||||
final storeSerialized = await SecureStorage.instance.read(
|
||||
key: 'signed_pre_key_store',
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
);
|
||||
final store = HashMap<int, Uint8List>();
|
||||
if (storeSerialized == null) {
|
||||
|
|
|
|||
|
|
@ -69,11 +69,6 @@ class MediaFiles extends Table {
|
|||
|
||||
BlobColumn get storedFileHash => blob().nullable()();
|
||||
|
||||
BoolColumn get hasThumbnail =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get sizeInBytes => integer().nullable()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get createdAtMonth => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
|||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
||||
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||
enum MessageType { media, text, contacts, restoreFlameCounter }
|
||||
|
||||
@DataClassName('Message')
|
||||
class Messages extends Table {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ class UserDiscoveryAnnouncedUsers extends Table {
|
|||
BoolColumn get wasShownToTheUser =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get wasAskedFriends =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {announcedUserId};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart'
|
||||
show DriftNativeOptions, driftDatabase;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||
|
|
@ -12,7 +12,6 @@ import 'package:twonly/src/database/daos/reactions.dao.dart';
|
|||
import 'package:twonly/src/database/daos/receipts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
|
||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||
import 'package:twonly/src/database/drift_logging_interceptor.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
|
@ -82,29 +81,21 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 17;
|
||||
int get schemaVersion => 15;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
final connection = driftDatabase(
|
||||
return driftDatabase(
|
||||
name: 'twonly',
|
||||
native: DriftNativeOptions(
|
||||
databaseDirectory: getApplicationSupportDirectory,
|
||||
shareAcrossIsolates: true,
|
||||
setup: (rawDb) {
|
||||
rawDb
|
||||
..execute('PRAGMA journal_mode=DELETE;')
|
||||
..execute('PRAGMA synchronous=FULL;')
|
||||
..execute('PRAGMA journal_mode=WAL;')
|
||||
..execute('PRAGMA busy_timeout=5000;');
|
||||
},
|
||||
),
|
||||
);
|
||||
try {
|
||||
if (userService.isUserCreated &&
|
||||
userService.currentUser.enableDatabaseLogging) {
|
||||
return connection.interceptWith(DriftLoggingInterceptor());
|
||||
}
|
||||
} catch (_) {}
|
||||
return connection;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -220,30 +211,14 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
from14To15: (m, schema) async {
|
||||
await m.createTable(schema.signalSignedPreKeyStores);
|
||||
},
|
||||
from15To16: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.mediaFiles,
|
||||
schema.mediaFiles.hasThumbnail,
|
||||
);
|
||||
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
|
||||
},
|
||||
from16To17: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.userDiscoveryAnnouncedUsers,
|
||||
schema.userDiscoveryAnnouncedUsers.wasAskedFriends,
|
||||
);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void markUpdated() {
|
||||
notifyUpdates({
|
||||
TableUpdate.onTable(messages, kind: UpdateKind.update),
|
||||
TableUpdate.onTable(contacts, kind: UpdateKind.update),
|
||||
TableUpdate.onTable(groups, kind: UpdateKind.update),
|
||||
});
|
||||
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
|
||||
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
|
||||
}
|
||||
|
||||
Future<void> printTableSizes() async {
|
||||
|
|
@ -257,4 +232,38 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
Log.info('Table: $tableName, Size: $tableSize bytes');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDataForTwonlySafe() async {
|
||||
await (delete(messages)..where(
|
||||
(t) =>
|
||||
(t.mediaStored.equals(false) &
|
||||
t.isDeletedFromSender.equals(false)),
|
||||
))
|
||||
.go();
|
||||
await update(messages).write(
|
||||
const MessagesCompanion(
|
||||
downloadToken: Value(null),
|
||||
),
|
||||
);
|
||||
await (delete(mediaFiles)..where(
|
||||
(t) => (t.stored.equals(false)),
|
||||
))
|
||||
.go();
|
||||
await delete(receipts).go();
|
||||
await delete(receivedReceipts).go();
|
||||
await update(contacts).write(
|
||||
const ContactsCompanion(
|
||||
avatarSvgCompressed: Value(null),
|
||||
senderProfileCounter: Value(0),
|
||||
),
|
||||
);
|
||||
await (delete(signalPreKeyStores)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
clock.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2810,32 +2810,6 @@ class $MediaFilesTable extends MediaFiles
|
|||
type: DriftSqlType.blob,
|
||||
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(
|
||||
'createdAt',
|
||||
);
|
||||
|
|
@ -2879,8 +2853,6 @@ class $MediaFilesTable extends MediaFiles
|
|||
encryptionMac,
|
||||
encryptionNonce,
|
||||
storedFileHash,
|
||||
hasThumbnail,
|
||||
sizeInBytes,
|
||||
createdAt,
|
||||
createdAtMonth,
|
||||
];
|
||||
|
|
@ -3015,24 +2987,6 @@ 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')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
|
|
@ -3138,14 +3092,6 @@ class $MediaFilesTable extends MediaFiles
|
|||
DriftSqlType.blob,
|
||||
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(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
|
|
@ -3201,8 +3147,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
final Uint8List? encryptionMac;
|
||||
final Uint8List? encryptionNonce;
|
||||
final Uint8List? storedFileHash;
|
||||
final bool hasThumbnail;
|
||||
final int? sizeInBytes;
|
||||
final DateTime createdAt;
|
||||
final String? createdAtMonth;
|
||||
const MediaFile({
|
||||
|
|
@ -3224,8 +3168,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
this.encryptionMac,
|
||||
this.encryptionNonce,
|
||||
this.storedFileHash,
|
||||
required this.hasThumbnail,
|
||||
this.sizeInBytes,
|
||||
required this.createdAt,
|
||||
this.createdAtMonth,
|
||||
});
|
||||
|
|
@ -3286,10 +3228,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
if (!nullToAbsent || storedFileHash != null) {
|
||||
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);
|
||||
if (!nullToAbsent || createdAtMonth != null) {
|
||||
map['created_at_month'] = Variable<String>(createdAtMonth);
|
||||
|
|
@ -3340,10 +3278,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
storedFileHash: storedFileHash == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(storedFileHash),
|
||||
hasThumbnail: Value(hasThumbnail),
|
||||
sizeInBytes: sizeInBytes == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(sizeInBytes),
|
||||
createdAt: Value(createdAt),
|
||||
createdAtMonth: createdAtMonth == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
|
|
@ -3389,8 +3323,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
|
||||
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
|
||||
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
||||
hasThumbnail: serializer.fromJson<bool>(json['hasThumbnail']),
|
||||
sizeInBytes: serializer.fromJson<int?>(json['sizeInBytes']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
||||
);
|
||||
|
|
@ -3425,8 +3357,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
|
||||
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
|
||||
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
||||
'hasThumbnail': serializer.toJson<bool>(hasThumbnail),
|
||||
'sizeInBytes': serializer.toJson<int?>(sizeInBytes),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
||||
};
|
||||
|
|
@ -3451,8 +3381,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||
Value<Uint8List?> encryptionNonce = const Value.absent(),
|
||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||
bool? hasThumbnail,
|
||||
Value<int?> sizeInBytes = const Value.absent(),
|
||||
DateTime? createdAt,
|
||||
Value<String?> createdAtMonth = const Value.absent(),
|
||||
}) => MediaFile(
|
||||
|
|
@ -3493,8 +3421,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
storedFileHash: storedFileHash.present
|
||||
? storedFileHash.value
|
||||
: this.storedFileHash,
|
||||
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||
sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
createdAtMonth: createdAtMonth.present
|
||||
? createdAtMonth.value
|
||||
|
|
@ -3550,12 +3476,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
storedFileHash: data.storedFileHash.present
|
||||
? data.storedFileHash.value
|
||||
: 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,
|
||||
createdAtMonth: data.createdAtMonth.present
|
||||
? data.createdAtMonth.value
|
||||
|
|
@ -3584,8 +3504,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
..write('encryptionMac: $encryptionMac, ')
|
||||
..write('encryptionNonce: $encryptionNonce, ')
|
||||
..write('storedFileHash: $storedFileHash, ')
|
||||
..write('hasThumbnail: $hasThumbnail, ')
|
||||
..write('sizeInBytes: $sizeInBytes, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('createdAtMonth: $createdAtMonth')
|
||||
..write(')'))
|
||||
|
|
@ -3593,7 +3511,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
int get hashCode => Object.hash(
|
||||
mediaId,
|
||||
type,
|
||||
uploadState,
|
||||
|
|
@ -3612,11 +3530,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
$driftBlobEquality.hash(encryptionMac),
|
||||
$driftBlobEquality.hash(encryptionNonce),
|
||||
$driftBlobEquality.hash(storedFileHash),
|
||||
hasThumbnail,
|
||||
sizeInBytes,
|
||||
createdAt,
|
||||
createdAtMonth,
|
||||
]);
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
|
|
@ -3645,8 +3561,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
other.storedFileHash,
|
||||
this.storedFileHash,
|
||||
) &&
|
||||
other.hasThumbnail == this.hasThumbnail &&
|
||||
other.sizeInBytes == this.sizeInBytes &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.createdAtMonth == this.createdAtMonth);
|
||||
}
|
||||
|
|
@ -3670,8 +3584,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
final Value<Uint8List?> encryptionMac;
|
||||
final Value<Uint8List?> encryptionNonce;
|
||||
final Value<Uint8List?> storedFileHash;
|
||||
final Value<bool> hasThumbnail;
|
||||
final Value<int?> sizeInBytes;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<String?> createdAtMonth;
|
||||
final Value<int> rowid;
|
||||
|
|
@ -3694,8 +3606,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.encryptionMac = const Value.absent(),
|
||||
this.encryptionNonce = const Value.absent(),
|
||||
this.storedFileHash = const Value.absent(),
|
||||
this.hasThumbnail = const Value.absent(),
|
||||
this.sizeInBytes = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.createdAtMonth = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
|
|
@ -3719,8 +3629,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.encryptionMac = const Value.absent(),
|
||||
this.encryptionNonce = const Value.absent(),
|
||||
this.storedFileHash = const Value.absent(),
|
||||
this.hasThumbnail = const Value.absent(),
|
||||
this.sizeInBytes = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.createdAtMonth = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
|
|
@ -3745,8 +3653,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Expression<Uint8List>? encryptionMac,
|
||||
Expression<Uint8List>? encryptionNonce,
|
||||
Expression<Uint8List>? storedFileHash,
|
||||
Expression<bool>? hasThumbnail,
|
||||
Expression<int>? sizeInBytes,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<String>? createdAtMonth,
|
||||
Expression<int>? rowid,
|
||||
|
|
@ -3774,8 +3680,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
if (encryptionMac != null) 'encryption_mac': encryptionMac,
|
||||
if (encryptionNonce != null) 'encryption_nonce': encryptionNonce,
|
||||
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 (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
|
|
@ -3801,8 +3705,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Value<Uint8List?>? encryptionMac,
|
||||
Value<Uint8List?>? encryptionNonce,
|
||||
Value<Uint8List?>? storedFileHash,
|
||||
Value<bool>? hasThumbnail,
|
||||
Value<int?>? sizeInBytes,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<String?>? createdAtMonth,
|
||||
Value<int>? rowid,
|
||||
|
|
@ -3829,8 +3731,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
encryptionMac: encryptionMac ?? this.encryptionMac,
|
||||
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
|
||||
storedFileHash: storedFileHash ?? this.storedFileHash,
|
||||
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||
sizeInBytes: sizeInBytes ?? this.sizeInBytes,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
||||
rowid: rowid ?? this.rowid,
|
||||
|
|
@ -3910,12 +3810,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
if (storedFileHash.present) {
|
||||
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) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
|
|
@ -3949,8 +3843,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
..write('encryptionMac: $encryptionMac, ')
|
||||
..write('encryptionNonce: $encryptionNonce, ')
|
||||
..write('storedFileHash: $storedFileHash, ')
|
||||
..write('hasThumbnail: $hasThumbnail, ')
|
||||
..write('sizeInBytes: $sizeInBytes, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('createdAtMonth: $createdAtMonth, ')
|
||||
..write('rowid: $rowid')
|
||||
|
|
@ -10318,21 +10210,6 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
|||
),
|
||||
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
|
||||
List<GeneratedColumn> get $columns => [
|
||||
announcedUserId,
|
||||
|
|
@ -10341,7 +10218,6 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
|||
username,
|
||||
wasShownToTheUser,
|
||||
isHidden,
|
||||
wasAskedFriends,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
|
|
@ -10404,15 +10280,6 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
|||
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('was_asked_friends')) {
|
||||
context.handle(
|
||||
_wasAskedFriendsMeta,
|
||||
wasAskedFriends.isAcceptableOrUnknown(
|
||||
data['was_asked_friends']!,
|
||||
_wasAskedFriendsMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
@ -10449,10 +10316,6 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
|||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_hidden'],
|
||||
)!,
|
||||
wasAskedFriends: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}was_asked_friends'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -10470,7 +10333,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
final String? username;
|
||||
final bool wasShownToTheUser;
|
||||
final bool isHidden;
|
||||
final bool wasAskedFriends;
|
||||
const UserDiscoveryAnnouncedUser({
|
||||
required this.announcedUserId,
|
||||
required this.announcedPublicKey,
|
||||
|
|
@ -10478,7 +10340,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
this.username,
|
||||
required this.wasShownToTheUser,
|
||||
required this.isHidden,
|
||||
required this.wasAskedFriends,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
|
|
@ -10491,7 +10352,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
}
|
||||
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser);
|
||||
map['is_hidden'] = Variable<bool>(isHidden);
|
||||
map['was_asked_friends'] = Variable<bool>(wasAskedFriends);
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
@ -10505,7 +10365,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
: Value(username),
|
||||
wasShownToTheUser: Value(wasShownToTheUser),
|
||||
isHidden: Value(isHidden),
|
||||
wasAskedFriends: Value(wasAskedFriends),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -10523,7 +10382,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
username: serializer.fromJson<String?>(json['username']),
|
||||
wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']),
|
||||
isHidden: serializer.fromJson<bool>(json['isHidden']),
|
||||
wasAskedFriends: serializer.fromJson<bool>(json['wasAskedFriends']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
|
@ -10536,7 +10394,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
'username': serializer.toJson<String?>(username),
|
||||
'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser),
|
||||
'isHidden': serializer.toJson<bool>(isHidden),
|
||||
'wasAskedFriends': serializer.toJson<bool>(wasAskedFriends),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -10547,7 +10404,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
Value<String?> username = const Value.absent(),
|
||||
bool? wasShownToTheUser,
|
||||
bool? isHidden,
|
||||
bool? wasAskedFriends,
|
||||
}) => UserDiscoveryAnnouncedUser(
|
||||
announcedUserId: announcedUserId ?? this.announcedUserId,
|
||||
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
|
||||
|
|
@ -10555,7 +10411,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
username: username.present ? username.value : this.username,
|
||||
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
|
||||
);
|
||||
UserDiscoveryAnnouncedUser copyWithCompanion(
|
||||
UserDiscoveryAnnouncedUsersCompanion data,
|
||||
|
|
@ -10573,9 +10428,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
? data.wasShownToTheUser.value
|
||||
: this.wasShownToTheUser,
|
||||
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
|
||||
wasAskedFriends: data.wasAskedFriends.present
|
||||
? data.wasAskedFriends.value
|
||||
: this.wasAskedFriends,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -10587,8 +10439,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
..write('publicId: $publicId, ')
|
||||
..write('username: $username, ')
|
||||
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
||||
..write('isHidden: $isHidden, ')
|
||||
..write('wasAskedFriends: $wasAskedFriends')
|
||||
..write('isHidden: $isHidden')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
|
@ -10601,7 +10452,6 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
username,
|
||||
wasShownToTheUser,
|
||||
isHidden,
|
||||
wasAskedFriends,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
|
|
@ -10615,8 +10465,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
|||
other.publicId == this.publicId &&
|
||||
other.username == this.username &&
|
||||
other.wasShownToTheUser == this.wasShownToTheUser &&
|
||||
other.isHidden == this.isHidden &&
|
||||
other.wasAskedFriends == this.wasAskedFriends);
|
||||
other.isHidden == this.isHidden);
|
||||
}
|
||||
|
||||
class UserDiscoveryAnnouncedUsersCompanion
|
||||
|
|
@ -10627,7 +10476,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
final Value<String?> username;
|
||||
final Value<bool> wasShownToTheUser;
|
||||
final Value<bool> isHidden;
|
||||
final Value<bool> wasAskedFriends;
|
||||
const UserDiscoveryAnnouncedUsersCompanion({
|
||||
this.announcedUserId = const Value.absent(),
|
||||
this.announcedPublicKey = const Value.absent(),
|
||||
|
|
@ -10635,7 +10483,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
this.username = const Value.absent(),
|
||||
this.wasShownToTheUser = const Value.absent(),
|
||||
this.isHidden = const Value.absent(),
|
||||
this.wasAskedFriends = const Value.absent(),
|
||||
});
|
||||
UserDiscoveryAnnouncedUsersCompanion.insert({
|
||||
this.announcedUserId = const Value.absent(),
|
||||
|
|
@ -10644,7 +10491,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
this.username = const Value.absent(),
|
||||
this.wasShownToTheUser = const Value.absent(),
|
||||
this.isHidden = const Value.absent(),
|
||||
this.wasAskedFriends = const Value.absent(),
|
||||
}) : announcedPublicKey = Value(announcedPublicKey),
|
||||
publicId = Value(publicId);
|
||||
static Insertable<UserDiscoveryAnnouncedUser> custom({
|
||||
|
|
@ -10654,7 +10500,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
Expression<String>? username,
|
||||
Expression<bool>? wasShownToTheUser,
|
||||
Expression<bool>? isHidden,
|
||||
Expression<bool>? wasAskedFriends,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (announcedUserId != null) 'announced_user_id': announcedUserId,
|
||||
|
|
@ -10664,7 +10509,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
if (username != null) 'username': username,
|
||||
if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser,
|
||||
if (isHidden != null) 'is_hidden': isHidden,
|
||||
if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -10675,7 +10519,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
Value<String?>? username,
|
||||
Value<bool>? wasShownToTheUser,
|
||||
Value<bool>? isHidden,
|
||||
Value<bool>? wasAskedFriends,
|
||||
}) {
|
||||
return UserDiscoveryAnnouncedUsersCompanion(
|
||||
announcedUserId: announcedUserId ?? this.announcedUserId,
|
||||
|
|
@ -10684,7 +10527,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
username: username ?? this.username,
|
||||
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -10711,9 +10553,6 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
if (isHidden.present) {
|
||||
map['is_hidden'] = Variable<bool>(isHidden.value);
|
||||
}
|
||||
if (wasAskedFriends.present) {
|
||||
map['was_asked_friends'] = Variable<bool>(wasAskedFriends.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
@ -10725,8 +10564,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
|||
..write('publicId: $publicId, ')
|
||||
..write('username: $username, ')
|
||||
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
||||
..write('isHidden: $isHidden, ')
|
||||
..write('wasAskedFriends: $wasAskedFriends')
|
||||
..write('isHidden: $isHidden')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
|
@ -15506,8 +15344,6 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
|
|||
Value<Uint8List?> encryptionMac,
|
||||
Value<Uint8List?> encryptionNonce,
|
||||
Value<Uint8List?> storedFileHash,
|
||||
Value<bool> hasThumbnail,
|
||||
Value<int?> sizeInBytes,
|
||||
Value<DateTime> createdAt,
|
||||
Value<String?> createdAtMonth,
|
||||
Value<int> rowid,
|
||||
|
|
@ -15532,8 +15368,6 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
|
|||
Value<Uint8List?> encryptionMac,
|
||||
Value<Uint8List?> encryptionNonce,
|
||||
Value<Uint8List?> storedFileHash,
|
||||
Value<bool> hasThumbnail,
|
||||
Value<int?> sizeInBytes,
|
||||
Value<DateTime> createdAt,
|
||||
Value<String?> createdAtMonth,
|
||||
Value<int> rowid,
|
||||
|
|
@ -15665,16 +15499,6 @@ class $$MediaFilesTableFilterComposer
|
|||
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(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
|
|
@ -15810,16 +15634,6 @@ class $$MediaFilesTableOrderingComposer
|
|||
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(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
|
|
@ -15927,16 +15741,6 @@ class $$MediaFilesTableAnnotationComposer
|
|||
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 =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
|
|
@ -16017,8 +15821,6 @@ class $$MediaFilesTableTableManager
|
|||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||
Value<Uint8List?> encryptionNonce = 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<String?> createdAtMonth = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -16041,8 +15843,6 @@ class $$MediaFilesTableTableManager
|
|||
encryptionMac: encryptionMac,
|
||||
encryptionNonce: encryptionNonce,
|
||||
storedFileHash: storedFileHash,
|
||||
hasThumbnail: hasThumbnail,
|
||||
sizeInBytes: sizeInBytes,
|
||||
createdAt: createdAt,
|
||||
createdAtMonth: createdAtMonth,
|
||||
rowid: rowid,
|
||||
|
|
@ -16067,8 +15867,6 @@ class $$MediaFilesTableTableManager
|
|||
Value<Uint8List?> encryptionMac = const Value.absent(),
|
||||
Value<Uint8List?> encryptionNonce = 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<String?> createdAtMonth = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -16091,8 +15889,6 @@ class $$MediaFilesTableTableManager
|
|||
encryptionMac: encryptionMac,
|
||||
encryptionNonce: encryptionNonce,
|
||||
storedFileHash: storedFileHash,
|
||||
hasThumbnail: hasThumbnail,
|
||||
sizeInBytes: sizeInBytes,
|
||||
createdAt: createdAt,
|
||||
createdAtMonth: createdAtMonth,
|
||||
rowid: rowid,
|
||||
|
|
@ -21588,7 +21384,6 @@ typedef $$UserDiscoveryAnnouncedUsersTableCreateCompanionBuilder =
|
|||
Value<String?> username,
|
||||
Value<bool> wasShownToTheUser,
|
||||
Value<bool> isHidden,
|
||||
Value<bool> wasAskedFriends,
|
||||
});
|
||||
typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
|
||||
UserDiscoveryAnnouncedUsersCompanion Function({
|
||||
|
|
@ -21598,7 +21393,6 @@ typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
|
|||
Value<String?> username,
|
||||
Value<bool> wasShownToTheUser,
|
||||
Value<bool> isHidden,
|
||||
Value<bool> wasAskedFriends,
|
||||
});
|
||||
|
||||
final class $$UserDiscoveryAnnouncedUsersTableReferences
|
||||
|
|
@ -21687,11 +21481,6 @@ class $$UserDiscoveryAnnouncedUsersTableFilterComposer
|
|||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get wasAskedFriends => $composableBuilder(
|
||||
column: $table.wasAskedFriends,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
Expression<bool> userDiscoveryUserRelationsRefs(
|
||||
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
|
||||
f,
|
||||
|
|
@ -21758,11 +21547,6 @@ class $$UserDiscoveryAnnouncedUsersTableOrderingComposer
|
|||
column: $table.isHidden,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get wasAskedFriends => $composableBuilder(
|
||||
column: $table.wasAskedFriends,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
|
||||
|
|
@ -21798,11 +21582,6 @@ class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
|
|||
GeneratedColumn<bool> get isHidden =>
|
||||
$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> Function(
|
||||
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
|
||||
|
|
@ -21881,7 +21660,6 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
|||
Value<String?> username = const Value.absent(),
|
||||
Value<bool> wasShownToTheUser = const Value.absent(),
|
||||
Value<bool> isHidden = const Value.absent(),
|
||||
Value<bool> wasAskedFriends = const Value.absent(),
|
||||
}) => UserDiscoveryAnnouncedUsersCompanion(
|
||||
announcedUserId: announcedUserId,
|
||||
announcedPublicKey: announcedPublicKey,
|
||||
|
|
@ -21889,7 +21667,6 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
|||
username: username,
|
||||
wasShownToTheUser: wasShownToTheUser,
|
||||
isHidden: isHidden,
|
||||
wasAskedFriends: wasAskedFriends,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
|
|
@ -21899,7 +21676,6 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
|||
Value<String?> username = const Value.absent(),
|
||||
Value<bool> wasShownToTheUser = const Value.absent(),
|
||||
Value<bool> isHidden = const Value.absent(),
|
||||
Value<bool> wasAskedFriends = const Value.absent(),
|
||||
}) => UserDiscoveryAnnouncedUsersCompanion.insert(
|
||||
announcedUserId: announcedUserId,
|
||||
announcedPublicKey: announcedPublicKey,
|
||||
|
|
@ -21907,7 +21683,6 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
|||
username: username,
|
||||
wasShownToTheUser: wasShownToTheUser,
|
||||
isHidden: isHidden,
|
||||
wasAskedFriends: wasAskedFriends,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -98,10 +98,16 @@ abstract class AppLocalizations {
|
|||
Locale('en'),
|
||||
];
|
||||
|
||||
/// No description provided for @registerTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Welcome to twonly!'**
|
||||
String get registerTitle;
|
||||
|
||||
/// No description provided for @registerSlogan.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stay in touch privately.'**
|
||||
/// **'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing'**
|
||||
String get registerSlogan;
|
||||
|
||||
/// No description provided for @onboardingWelcomeTitle.
|
||||
|
|
@ -140,6 +146,18 @@ abstract class AppLocalizations {
|
|||
/// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -152,10 +170,16 @@ abstract class AppLocalizations {
|
|||
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create your account'**
|
||||
/// **'Please select a username so others can find you!'**
|
||||
String get registerUsernameSlogan;
|
||||
|
||||
/// No description provided for @registerUsernameDecoration.
|
||||
|
|
@ -167,7 +191,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @registerUsernameLimits.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'At least 3 characters.'**
|
||||
/// **'Your username must be at least 3 characters long.'**
|
||||
String get registerUsernameLimits;
|
||||
|
||||
/// No description provided for @registerProofOfWorkFailed.
|
||||
|
|
@ -302,12 +326,24 @@ abstract class AppLocalizations {
|
|||
/// **'Username not found'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open requests'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -482,6 +518,12 @@ abstract class AppLocalizations {
|
|||
/// **'Store in Gallery'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -500,36 +542,6 @@ abstract class AppLocalizations {
|
|||
/// **'When using WI-FI'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -575,13 +587,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @settingsPrivacyBlockUsers.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Block contacts'**
|
||||
/// **'Block users'**
|
||||
String get settingsPrivacyBlockUsers;
|
||||
|
||||
/// No description provided for @settingsPrivacyBlockUsersDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.'**
|
||||
/// **'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'**
|
||||
String get settingsPrivacyBlockUsersDesc;
|
||||
|
||||
/// No description provided for @settingsPrivacyBlockUsersCount.
|
||||
|
|
@ -590,48 +602,6 @@ abstract class AppLocalizations {
|
|||
/// **'{len} contact(s)'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -893,19 +863,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @contactVerifyNumberTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verify contacts'**
|
||||
/// **'Verify contact'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact verified'**
|
||||
/// **'User verified'**
|
||||
String get userVerifiedTitle;
|
||||
|
||||
/// No description provided for @contactVerifiedBy.
|
||||
|
|
@ -923,8 +887,8 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationTypeSecretQrToken.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{username} has scanned your QR code.'**
|
||||
String verificationTypeSecretQrToken(Object username);
|
||||
/// **'The other person scanned your QR code.'**
|
||||
String get verificationTypeSecretQrToken;
|
||||
|
||||
/// No description provided for @verificationTypeLink.
|
||||
///
|
||||
|
|
@ -977,13 +941,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @contactBlockBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'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.'**
|
||||
/// **'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.'**
|
||||
String get contactBlockBody;
|
||||
|
||||
/// No description provided for @contactRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove contact'**
|
||||
/// **'Remove user'**
|
||||
String get contactRemove;
|
||||
|
||||
/// No description provided for @contactRemoveTitle.
|
||||
|
|
@ -995,7 +959,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @contactRemoveBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.'**
|
||||
/// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'**
|
||||
String get contactRemoveBody;
|
||||
|
||||
/// No description provided for @undo.
|
||||
|
|
@ -1169,8 +1133,8 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @userFoundBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Do you want to connect with {username}?'**
|
||||
String userFoundBody(String username);
|
||||
/// **'Do you want to create a follow request?'**
|
||||
String get userFoundBody;
|
||||
|
||||
/// No description provided for @errorInternalError.
|
||||
///
|
||||
|
|
@ -1424,12 +1388,6 @@ abstract class AppLocalizations {
|
|||
/// **'Delete for all'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1448,12 +1406,6 @@ abstract class AppLocalizations {
|
|||
/// **'The image will be irrevocably deleted.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1601,9 +1553,15 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @twonlySafeRecoverTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Restore backup'**
|
||||
/// **'Recovery'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1685,7 +1643,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @reportUser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Report contact'**
|
||||
/// **'Report user'**
|
||||
String get reportUser;
|
||||
|
||||
/// No description provided for @newDeviceRegistered.
|
||||
|
|
@ -2324,6 +2282,18 @@ abstract class AppLocalizations {
|
|||
/// **'Draft'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2354,12 +2324,6 @@ abstract class AppLocalizations {
|
|||
/// **'Open your own QR code'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2453,13 +2417,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @userDiscoverySettingsManualApproval.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ask every time before sharing'**
|
||||
/// **'Manual approval'**
|
||||
String get userDiscoverySettingsManualApproval;
|
||||
|
||||
/// No description provided for @userDiscoverySettingsManualApprovalDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Before one of your friends is shared, you will be asked every time.'**
|
||||
/// **'Before someone is shared, you\'ll be asked first.'**
|
||||
String get userDiscoverySettingsManualApprovalDesc;
|
||||
|
||||
/// No description provided for @onboardingUserDiscoveryLetFriendsFindYou.
|
||||
|
|
@ -2717,13 +2681,13 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationBadgeGeneralDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'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.'**
|
||||
/// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'**
|
||||
String get verificationBadgeGeneralDesc;
|
||||
|
||||
/// No description provided for @verificationBadgeGreenDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A contact you have *personally verified* using the QR code.'**
|
||||
/// **'A contact you have *personally* verified.'**
|
||||
String get verificationBadgeGreenDesc;
|
||||
|
||||
/// No description provided for @verificationBadgeYellowDesc.
|
||||
|
|
@ -2738,42 +2702,6 @@ abstract class AppLocalizations {
|
|||
/// **'A contact whose identity has *not* yet been verified.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2846,6 +2774,12 @@ abstract class AppLocalizations {
|
|||
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2900,12 +2834,6 @@ abstract class AppLocalizations {
|
|||
/// **'Mutual Friends'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2978,66 +2906,6 @@ abstract class AppLocalizations {
|
|||
/// **'Request'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3182,6 +3050,12 @@ abstract class AppLocalizations {
|
|||
/// **'Back'**
|
||||
String get back;
|
||||
|
||||
/// No description provided for @onboardingExampleLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Example'**
|
||||
String get onboardingExampleLabel;
|
||||
|
||||
/// No description provided for @makerChangedUsername.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3314,6 +3188,12 @@ abstract class AppLocalizations {
|
|||
/// **'Emoji already used or invalid'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3338,309 +3218,17 @@ abstract class AppLocalizations {
|
|||
/// **'twonly will never show advertisements or sell your private data.'**
|
||||
String get subscriptionPledgeNoAdsDesc;
|
||||
|
||||
/// No description provided for @subscriptionPledgeSubtitle.
|
||||
/// No description provided for @subscriptionPledgeFundedTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zero ads. Total privacy.'**
|
||||
String get subscriptionPledgeSubtitle;
|
||||
/// **'Independent and funded by Users'**
|
||||
String get subscriptionPledgeFundedTitle;
|
||||
|
||||
/// No description provided for @dragToZoom.
|
||||
/// No description provided for @subscriptionPledgeFundedDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Drag to Zoom'**
|
||||
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;
|
||||
/// **'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.'**
|
||||
String get subscriptionPledgeFundedDesc;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get registerSlogan => 'Privat in Kontakt bleiben.';
|
||||
String get registerTitle => 'Willkommen bei twonly!';
|
||||
|
||||
@override
|
||||
String get registerSlogan =>
|
||||
'twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.';
|
||||
|
||||
@override
|
||||
String get onboardingWelcomeTitle => 'Willkommen bei twonly!';
|
||||
|
|
@ -33,6 +37,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get onboardingFocusBody =>
|
||||
'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
|
||||
String get onboardingNotProductTitle => 'Du bist nicht das Produkt!';
|
||||
|
||||
|
|
@ -41,13 +52,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Konto erstellen';
|
||||
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan =>
|
||||
'Bitte wähle einen Benutzernamen, damit dich andere finden können!';
|
||||
|
||||
@override
|
||||
String get registerUsernameDecoration => 'Benutzername';
|
||||
|
||||
@override
|
||||
String get registerUsernameLimits => 'Mindestens 3 Zeichen.';
|
||||
String get registerUsernameLimits =>
|
||||
'Der Benutzername muss mindestens 3 Zeichen lang sein.';
|
||||
|
||||
@override
|
||||
String get registerProofOfWorkFailed =>
|
||||
|
|
@ -116,9 +132,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get searchUsernameNotFound => 'Benutzername nicht gefunden';
|
||||
|
||||
@override
|
||||
String searchUsernameNotFoundBody(Object username) {
|
||||
return 'Es wurde kein Benutzer mit dem Benutzernamen \"$username\" gefunden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
|
||||
|
||||
@override
|
||||
String get chatListViewSearchUserNameBtn =>
|
||||
'Füge deinen ersten twonly-Kontakt hinzu!';
|
||||
|
||||
@override
|
||||
String get chatListDetailInput => 'Nachricht eingeben';
|
||||
|
||||
|
|
@ -210,6 +235,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
|
||||
|
||||
@override
|
||||
String get settingsStorageDataStoreInGSubtitle =>
|
||||
'Speichere Bilder zusätzlich in der Systemgalerie.';
|
||||
|
||||
@override
|
||||
String get settingsStorageDataMediaAutoDownload =>
|
||||
'Automatischer Mediendownload';
|
||||
|
|
@ -220,21 +249,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settingsProfileCustomizeAvatar => 'Avatar anpassen';
|
||||
|
||||
|
|
@ -257,41 +271,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get settingsPrivacy => 'Datenschutz & Sicherheit';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsers => 'Kontakte blockieren';
|
||||
String get settingsPrivacyBlockUsers => 'Benutzer blockieren';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsersDesc =>
|
||||
'Blockierte Kontakte können nicht mit dir kommunizieren. Du kannst einen blockierten Kontakt jederzeit wieder entsperren.';
|
||||
'Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.';
|
||||
|
||||
@override
|
||||
String settingsPrivacyBlockUsersCount(Object len) {
|
||||
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
|
||||
String get settingsNotification => 'Benachrichtigung';
|
||||
|
||||
|
|
@ -433,14 +423,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get contactVerifyNumberTitle => 'Kontakte verifizieren';
|
||||
String get contactVerifyNumberTitle => 'Benutzer verifizieren';
|
||||
|
||||
@override
|
||||
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';
|
||||
String get userVerifiedTitle => 'Benutzer verifiziert';
|
||||
|
||||
@override
|
||||
String contactVerifiedBy(Object username) {
|
||||
|
|
@ -451,9 +437,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||
|
||||
@override
|
||||
String verificationTypeSecretQrToken(Object username) {
|
||||
return '$username hat deinen QR-Code gescannt.';
|
||||
}
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'Die andere Person hat deinen QR-Code gescannt.';
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||
|
|
@ -485,10 +470,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactBlockBody =>
|
||||
'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.';
|
||||
'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.';
|
||||
|
||||
@override
|
||||
String get contactRemove => 'Kontakt löschen';
|
||||
String get contactRemove => 'Benutzer löschen';
|
||||
|
||||
@override
|
||||
String contactRemoveTitle(Object username) {
|
||||
|
|
@ -497,7 +482,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactRemoveBody =>
|
||||
'Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.';
|
||||
'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.';
|
||||
|
||||
@override
|
||||
String get undo => 'Rückgängig';
|
||||
|
|
@ -587,9 +572,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String userFoundBody(String username) {
|
||||
return 'Möchtest du dich mit $username vernetzen?';
|
||||
}
|
||||
String get userFoundBody => 'Möchtest du eine Folgeanfrage stellen?';
|
||||
|
||||
@override
|
||||
String get errorInternalError =>
|
||||
|
|
@ -730,17 +713,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get deleteOkBtnForMe => 'Für mich löschen';
|
||||
|
||||
|
|
@ -750,17 +722,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settingsBackup => 'Backup';
|
||||
|
||||
|
|
@ -840,7 +801,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get backupChangePassword => 'Password ändern';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
|
||||
String get twonlySafeRecoverTitle => 'Recovery';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverDesc =>
|
||||
'Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverBtn => 'Backup wiederherstellen';
|
||||
|
|
@ -887,7 +852,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get reportUserReason => 'Meldegrund';
|
||||
|
||||
@override
|
||||
String get reportUser => 'Kontakt melden';
|
||||
String get reportUser => 'Benutzer melden';
|
||||
|
||||
@override
|
||||
String get newDeviceRegistered =>
|
||||
|
|
@ -1284,6 +1249,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get draftMessage => 'Entwurf';
|
||||
|
||||
@override
|
||||
String get exportMemories => 'Memories exportieren (Beta)';
|
||||
|
||||
@override
|
||||
String get importMemories => 'Memories importieren (Beta)';
|
||||
|
||||
@override
|
||||
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
|
||||
|
||||
|
|
@ -1299,10 +1270,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
|
||||
|
||||
@override
|
||||
String get addContactQrSheetSubtext =>
|
||||
'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get finishSetupCardTitle => 'Profil vervollständigen';
|
||||
|
||||
|
|
@ -1356,11 +1323,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'Erfahre, wer dich anfragt';
|
||||
|
||||
@override
|
||||
String get userDiscoverySettingsManualApproval => 'Vor jedem Teilen fragen';
|
||||
String get userDiscoverySettingsManualApproval => 'Manuelle Zustimmung';
|
||||
|
||||
@override
|
||||
String get userDiscoverySettingsManualApprovalDesc =>
|
||||
'Bevor einer deiner Freunde geteilt wird, wirst du jedes Mal gefragt.';
|
||||
'Bevor jemand geteilt wird, wirst du zuerst gefragt.';
|
||||
|
||||
@override
|
||||
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
||||
|
|
@ -1528,11 +1495,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.';
|
||||
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
'Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.';
|
||||
'Ein Kontakt, den du *persönlich verifiziert* hast.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeYellowDesc =>
|
||||
|
|
@ -1542,35 +1509,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'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
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
return '$count Flammen wiederhergestellt';
|
||||
|
|
@ -1616,6 +1554,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get settingsTypingIndicationSubtitle =>
|
||||
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
||||
|
||||
@override
|
||||
String get scanQrOrShow => 'QR scannen / anzeigen';
|
||||
|
||||
@override
|
||||
String get contactActionBlock => 'Blockieren';
|
||||
|
||||
|
|
@ -1647,9 +1588,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
|
||||
|
||||
@override
|
||||
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
|
||||
|
||||
@override
|
||||
String get userDiscoveryDisabledLearnMore => 'Mehr erfahren';
|
||||
|
||||
|
|
@ -1693,43 +1631,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
||||
return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.';
|
||||
|
|
@ -1814,6 +1715,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get back => 'Zurück';
|
||||
|
||||
@override
|
||||
String get onboardingExampleLabel => 'Beispiel';
|
||||
|
||||
@override
|
||||
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
||||
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
|
||||
|
|
@ -1894,6 +1798,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get errorEmojiUsedOrInvalid =>
|
||||
'Emoji wird bereits verwendet oder ist ungültig';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeTitle => 'Unterstütze unabhängigen Datenschutz.';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||
|
||||
|
|
@ -1909,185 +1816,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
'twonly wird niemals Werbung anzeigen oder deine privaten Daten verkaufen.';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeSubtitle => 'Keine Werbung. Volle Privatsphäre.';
|
||||
String get subscriptionPledgeFundedTitle =>
|
||||
'Unabhängig und durch Nutzer finanziert';
|
||||
|
||||
@override
|
||||
String get dragToZoom => 'Zum Zoomen ziehen';
|
||||
|
||||
@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';
|
||||
String get subscriptionPledgeFundedDesc =>
|
||||
'twonly wird rein durch Nutzer-Abonnements finanziert, um unsere Unabhängigkeit und die Zukunft von twonly zu sichern.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get registerSlogan => 'Stay in touch privately.';
|
||||
String get registerTitle => 'Welcome to twonly!';
|
||||
|
||||
@override
|
||||
String get registerSlogan =>
|
||||
'twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing';
|
||||
|
||||
@override
|
||||
String get onboardingWelcomeTitle => 'Welcome to twonly!';
|
||||
|
|
@ -32,6 +36,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get onboardingFocusBody =>
|
||||
'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
|
||||
String get onboardingNotProductTitle => 'You are not the product!';
|
||||
|
||||
|
|
@ -40,13 +51,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'twonly is financed by donations and an optional subscription. Your data will never be sold.';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan => 'Create your account';
|
||||
String get onboardingGetStartedTitle => 'Let\'s go!';
|
||||
|
||||
@override
|
||||
String get registerUsernameSlogan =>
|
||||
'Please select a username so others can find you!';
|
||||
|
||||
@override
|
||||
String get registerUsernameDecoration => 'Username';
|
||||
|
||||
@override
|
||||
String get registerUsernameLimits => 'At least 3 characters.';
|
||||
String get registerUsernameLimits =>
|
||||
'Your username must be at least 3 characters long.';
|
||||
|
||||
@override
|
||||
String get registerProofOfWorkFailed =>
|
||||
|
|
@ -115,9 +131,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get searchUsernameNotFound => 'Username not found';
|
||||
|
||||
@override
|
||||
String searchUsernameNotFoundBody(Object username) {
|
||||
return 'There is no user with the username \"$username\" registered';
|
||||
}
|
||||
|
||||
@override
|
||||
String get searchUsernameNewFollowerTitle => 'Open requests';
|
||||
|
||||
@override
|
||||
String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!';
|
||||
|
||||
@override
|
||||
String get chatListDetailInput => 'Type a message';
|
||||
|
||||
|
|
@ -208,6 +232,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
|
||||
|
||||
@override
|
||||
String get settingsStorageDataStoreInGSubtitle =>
|
||||
'Store saved images additional in the systems gallery.';
|
||||
|
||||
@override
|
||||
String get settingsStorageDataMediaAutoDownload => 'Media auto-download';
|
||||
|
||||
|
|
@ -217,21 +245,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settingsProfileCustomizeAvatar => 'Customize your avatar';
|
||||
|
||||
|
|
@ -254,41 +267,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get settingsPrivacy => 'Privacy & Security';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsers => 'Block contacts';
|
||||
String get settingsPrivacyBlockUsers => 'Block users';
|
||||
|
||||
@override
|
||||
String get settingsPrivacyBlockUsersDesc =>
|
||||
'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.';
|
||||
'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.';
|
||||
|
||||
@override
|
||||
String settingsPrivacyBlockUsersCount(Object len) {
|
||||
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
|
||||
String get settingsNotification => 'Notification';
|
||||
|
||||
|
|
@ -429,14 +418,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'Your account will be deleted. There is no change to restore it.';
|
||||
|
||||
@override
|
||||
String get contactVerifyNumberTitle => 'Verify contacts';
|
||||
String get contactVerifyNumberTitle => 'Verify contact';
|
||||
|
||||
@override
|
||||
String get contactVerifyNumberSubtitle =>
|
||||
'Verify the identity of your contacts to make sure you are texting the right person.';
|
||||
|
||||
@override
|
||||
String get userVerifiedTitle => 'Contact verified';
|
||||
String get userVerifiedTitle => 'User verified';
|
||||
|
||||
@override
|
||||
String contactVerifiedBy(Object username) {
|
||||
|
|
@ -447,9 +432,8 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||
|
||||
@override
|
||||
String verificationTypeSecretQrToken(Object username) {
|
||||
return '$username has scanned your QR code.';
|
||||
}
|
||||
String get verificationTypeSecretQrToken =>
|
||||
'The other person scanned your QR code.';
|
||||
|
||||
@override
|
||||
String get verificationTypeLink => 'Verified via link.';
|
||||
|
|
@ -481,10 +465,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactBlockBody =>
|
||||
'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.';
|
||||
'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.';
|
||||
|
||||
@override
|
||||
String get contactRemove => 'Remove contact';
|
||||
String get contactRemove => 'Remove user';
|
||||
|
||||
@override
|
||||
String contactRemoveTitle(Object username) {
|
||||
|
|
@ -493,7 +477,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contactRemoveBody =>
|
||||
'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.';
|
||||
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
|
||||
|
||||
@override
|
||||
String get undo => 'Undo';
|
||||
|
|
@ -583,9 +567,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
}
|
||||
|
||||
@override
|
||||
String userFoundBody(String username) {
|
||||
return 'Do you want to connect with $username?';
|
||||
}
|
||||
String get userFoundBody => 'Do you want to create a follow request?';
|
||||
|
||||
@override
|
||||
String get errorInternalError =>
|
||||
|
|
@ -725,17 +707,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get deleteOkBtnForMe => 'Delete for me';
|
||||
|
||||
|
|
@ -745,17 +716,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settingsBackup => 'Backup';
|
||||
|
||||
|
|
@ -835,7 +795,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get backupChangePassword => 'Change password';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Restore backup';
|
||||
String get twonlySafeRecoverTitle => 'Recovery';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverDesc =>
|
||||
'If you have created a backup with twonly Backup, you can restore it here.';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverBtn => 'Restore backup';
|
||||
|
|
@ -882,7 +846,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get reportUserReason => 'Reporting reason';
|
||||
|
||||
@override
|
||||
String get reportUser => 'Report contact';
|
||||
String get reportUser => 'Report user';
|
||||
|
||||
@override
|
||||
String get newDeviceRegistered =>
|
||||
|
|
@ -1276,6 +1240,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get draftMessage => 'Draft';
|
||||
|
||||
@override
|
||||
String get exportMemories => 'Export memories (Beta)';
|
||||
|
||||
@override
|
||||
String get importMemories => 'Import memories (Beta)';
|
||||
|
||||
@override
|
||||
String get voiceMessageSlideToCancel => 'Slide to cancel';
|
||||
|
||||
|
|
@ -1291,10 +1261,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get openYourOwnQRcode => 'Open your own QR code';
|
||||
|
||||
@override
|
||||
String get addContactQrSheetSubtext =>
|
||||
'Let a friend scan this QR code to add you';
|
||||
|
||||
@override
|
||||
String get finishSetupCardTitle => 'Complete your profile';
|
||||
|
||||
|
|
@ -1348,12 +1314,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'Be informed about who is requesting';
|
||||
|
||||
@override
|
||||
String get userDiscoverySettingsManualApproval =>
|
||||
'Ask every time before sharing';
|
||||
String get userDiscoverySettingsManualApproval => 'Manual approval';
|
||||
|
||||
@override
|
||||
String get userDiscoverySettingsManualApprovalDesc =>
|
||||
'Before one of your friends is shared, you will be asked every time.';
|
||||
'Before someone is shared, you\'ll be asked first.';
|
||||
|
||||
@override
|
||||
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
||||
|
|
@ -1515,11 +1480,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGeneralDesc =>
|
||||
'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.';
|
||||
'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
'A contact you have *personally verified* using the QR code.';
|
||||
'A contact you have *personally* verified.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeYellowDesc =>
|
||||
|
|
@ -1529,35 +1494,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get verificationBadgeRedDesc =>
|
||||
'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
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
return '$count flames restored';
|
||||
|
|
@ -1603,6 +1539,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get settingsTypingIndicationSubtitle =>
|
||||
'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
|
||||
String get contactActionBlock => 'Block';
|
||||
|
||||
|
|
@ -1634,9 +1573,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get userDiscoverySettingsTitle => 'Mutual Friends';
|
||||
|
||||
@override
|
||||
String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
|
||||
|
||||
@override
|
||||
String get userDiscoveryDisabledLearnMore => 'Learn more';
|
||||
|
||||
|
|
@ -1680,43 +1616,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
||||
return '$imagesLeft more images are needed until your friends are shared with $username.';
|
||||
|
|
@ -1801,6 +1700,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get back => 'Back';
|
||||
|
||||
@override
|
||||
String get onboardingExampleLabel => 'Example';
|
||||
|
||||
@override
|
||||
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
||||
return '$maker changed their username from $oldName to $newName.';
|
||||
|
|
@ -1880,6 +1782,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get errorEmojiUsedOrInvalid => 'Emoji already used or invalid';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeTitle => 'Support independent privacy.';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeSecureTitle => 'Secure by Design';
|
||||
|
||||
|
|
@ -1895,184 +1800,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
'twonly will never show advertisements or sell your private data.';
|
||||
|
||||
@override
|
||||
String get subscriptionPledgeSubtitle => 'Zero ads. Total privacy.';
|
||||
String get subscriptionPledgeFundedTitle => 'Independent and funded by Users';
|
||||
|
||||
@override
|
||||
String get dragToZoom => 'Drag to Zoom';
|
||||
|
||||
@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';
|
||||
String get subscriptionPledgeFundedDesc =>
|
||||
'twonly is funded purely by user subscriptions to secure our independence and support the future of twonly.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 673f6d8c3036d64060b1114912bd5bf5515d5420
|
||||
Subproject commit f649128fd875a12f23518ff2641190cc129a9339
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:twonly/src/services/profile.service.dart';
|
||||
part 'userdata.model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
|
|
@ -11,7 +10,6 @@ class UserData {
|
|||
required this.displayName,
|
||||
required this.subscriptionPlan,
|
||||
required this.currentSetupPage,
|
||||
required this.appVersion,
|
||||
});
|
||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataFromJson(json);
|
||||
|
|
@ -37,12 +35,6 @@ class UserData {
|
|||
@JsonKey(defaultValue: 0)
|
||||
int deviceId = 0;
|
||||
|
||||
@JsonKey(defaultValue: SetupProfile.standard)
|
||||
SetupProfile setupProfile = SetupProfile.standard;
|
||||
|
||||
@JsonKey(defaultValue: SecurityProfile.normal)
|
||||
SecurityProfile securityProfile = SecurityProfile.normal;
|
||||
|
||||
// --- SUBSCRIPTION DTA ---
|
||||
|
||||
@JsonKey(defaultValue: 'Free')
|
||||
|
|
@ -65,9 +57,6 @@ class UserData {
|
|||
@JsonKey(defaultValue: false)
|
||||
bool requestedAudioPermission = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool enableDatabaseLogging = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool automaticallyMarkEqualMediaFilesAsOpened = false;
|
||||
|
||||
|
|
@ -87,8 +76,8 @@ class UserData {
|
|||
|
||||
Map<String, List<String>>? autoDownloadOptions;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool storeMediaFilesInGallery = true;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool storeMediaFilesInGallery = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool autoStoreAllSendUnlimitedMediaFiles = false;
|
||||
|
|
@ -114,8 +103,8 @@ class UserData {
|
|||
@JsonKey(defaultValue: 4)
|
||||
int requiredSendImages = 4;
|
||||
|
||||
@JsonKey(defaultValue: 3)
|
||||
int userDiscoveryThreshold = 3;
|
||||
@JsonKey(defaultValue: 2)
|
||||
int userDiscoveryThreshold = 2;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool userDiscoveryRequiresManualApproval = false;
|
||||
|
|
@ -176,9 +165,6 @@ class UserData {
|
|||
@JsonKey(defaultValue: false)
|
||||
bool skipSetupPages = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool hasZoomed = false;
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,22 +13,13 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
displayName: json['displayName'] as String,
|
||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||
currentSetupPage: json['currentSetupPage'] as String?,
|
||||
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
||||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||
..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?
|
||||
..lastImageSend = json['lastImageSend'] == null
|
||||
? null
|
||||
|
|
@ -42,9 +33,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
|
||||
..requestedAudioPermission =
|
||||
json['requestedAudioPermission'] as bool? ?? false
|
||||
..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false
|
||||
..automaticallyMarkEqualMediaFilesAsOpened =
|
||||
json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false
|
||||
..videoStabilizationEnabled =
|
||||
json['videoStabilizationEnabled'] as bool? ?? true
|
||||
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
||||
|
|
@ -62,7 +50,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
),
|
||||
)
|
||||
..storeMediaFilesInGallery =
|
||||
json['storeMediaFilesInGallery'] as bool? ?? true
|
||||
json['storeMediaFilesInGallery'] as bool? ?? false
|
||||
..autoStoreAllSendUnlimitedMediaFiles =
|
||||
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
|
||||
..typingIndicators = json['typingIndicators'] as bool? ?? true
|
||||
|
|
@ -78,7 +66,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
json['isUserDiscoveryEnabled'] as bool? ?? false
|
||||
..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4
|
||||
..userDiscoveryThreshold =
|
||||
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 3
|
||||
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 2
|
||||
..userDiscoveryRequiresManualApproval =
|
||||
json['userDiscoveryRequiresManualApproval'] as bool? ?? false
|
||||
..userDiscoverySharePromotion =
|
||||
|
|
@ -112,8 +100,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String)
|
||||
..skipSetupPages = json['skipSetupPages'] as bool? ?? false
|
||||
..hasZoomed = json['hasZoomed'] as bool? ?? false;
|
||||
..skipSetupPages = json['skipSetupPages'] as bool? ?? false;
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
|
|
@ -125,8 +112,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
'deviceId': instance.deviceId,
|
||||
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
|
||||
'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
|
|
@ -136,9 +121,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'requestedAudioPermission': instance.requestedAudioPermission,
|
||||
'enableDatabaseLogging': instance.enableDatabaseLogging,
|
||||
'automaticallyMarkEqualMediaFilesAsOpened':
|
||||
instance.automaticallyMarkEqualMediaFilesAsOpened,
|
||||
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
|
||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
|
||||
|
|
@ -178,18 +160,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
?.toIso8601String(),
|
||||
'currentSetupPage': instance.currentSetupPage,
|
||||
'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 = {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ message AdditionalMessageData {
|
|||
LINK = 0;
|
||||
CONTACTS = 1;
|
||||
RESTORED_FLAME_COUNTER = 2;
|
||||
ASK_ABOUT_USER = 3;
|
||||
}
|
||||
Type type = 1;
|
||||
|
||||
optional string link = 2;
|
||||
repeated SharedContact contacts = 3;
|
||||
optional int64 restored_flame_counter = 4;
|
||||
optional int64 ask_about_user_id = 5;
|
||||
}
|
||||
|
|
@ -105,7 +105,6 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
$core.String? link,
|
||||
$core.Iterable<SharedContact>? contacts,
|
||||
$fixnum.Int64? restoredFlameCounter,
|
||||
$fixnum.Int64? askAboutUserId,
|
||||
}) {
|
||||
final result = create();
|
||||
if (type != null) result.type = type;
|
||||
|
|
@ -113,7 +112,6 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
if (contacts != null) result.contacts.addAll(contacts);
|
||||
if (restoredFlameCounter != null)
|
||||
result.restoredFlameCounter = restoredFlameCounter;
|
||||
if (askAboutUserId != null) result.askAboutUserId = askAboutUserId;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +133,6 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
..pPM<SharedContact>(3, _omitFieldNames ? '' : 'contacts',
|
||||
subBuilder: SharedContact.create)
|
||||
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
|
||||
..aInt64(5, _omitFieldNames ? '' : 'askAboutUserId')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -187,15 +184,6 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
|||
$core.bool hasRestoredFlameCounter() => $_has(3);
|
||||
@$pb.TagNumber(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 =
|
||||
|
|
|
|||
|
|
@ -22,19 +22,16 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
|
|||
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
|
||||
AdditionalMessageData_Type._(
|
||||
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 =
|
||||
<AdditionalMessageData_Type>[
|
||||
LINK,
|
||||
CONTACTS,
|
||||
RESTORED_FLAME_COUNTER,
|
||||
ASK_ABOUT_USER,
|
||||
];
|
||||
|
||||
static final $core.List<AdditionalMessageData_Type?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 3);
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 2);
|
||||
static AdditionalMessageData_Type? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
|
|
|
|||
|
|
@ -67,21 +67,11 @@ const AdditionalMessageData$json = {
|
|||
'10': 'restoredFlameCounter',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'ask_about_user_id',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'9': 2,
|
||||
'10': 'askAboutUserId',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'4': [AdditionalMessageData_Type$json],
|
||||
'8': [
|
||||
{'1': '_link'},
|
||||
{'1': '_restored_flame_counter'},
|
||||
{'1': '_ask_about_user_id'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -92,7 +82,6 @@ const AdditionalMessageData_Type$json = {
|
|||
{'1': 'LINK', '2': 0},
|
||||
{'1': 'CONTACTS', '2': 1},
|
||||
{'1': 'RESTORED_FLAME_COUNTER', '2': 2},
|
||||
{'1': 'ASK_ABOUT_USER', '2': 3},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -101,7 +90,6 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec
|
|||
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
|
||||
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
|
||||
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
|
||||
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQESLgoRYXNrX2Fib3V0X3VzZXJfaWQY'
|
||||
'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQEiTgoEVHlwZRIICgRMSU5LEAASDAoIQ09OVEFDVF'
|
||||
'MQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAISEgoOQVNLX0FCT1VUX1VTRVIQA0IHCgVf'
|
||||
'bGlua0IZChdfcmVzdG9yZWRfZmxhbWVfY291bnRlckIUChJfYXNrX2Fib3V0X3VzZXJfaWQ=');
|
||||
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI'
|
||||
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3'
|
||||
'RvcmVkX2ZsYW1lX2NvdW50ZXI=');
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? signedPrekeySignature,
|
||||
$fixnum.Int64? signedPrekeyId,
|
||||
$core.List<$core.int>? secretVerificationToken,
|
||||
$fixnum.Int64? timestamp,
|
||||
}) {
|
||||
final result = create();
|
||||
if (userId != null) result.userId = userId;
|
||||
|
|
@ -110,7 +109,6 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
||||
if (secretVerificationToken != null)
|
||||
result.secretVerificationToken = secretVerificationToken;
|
||||
if (timestamp != null) result.timestamp = timestamp;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +136,6 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
||||
..a<$core.List<$core.int>>(
|
||||
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
||||
..aInt64(9, _omitFieldNames ? '' : 'timestamp')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -233,15 +230,6 @@ class PublicProfile extends $pb.GeneratedMessage {
|
|||
$core.bool hasSecretVerificationToken() => $_has(7);
|
||||
@$pb.TagNumber(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 =
|
||||
|
|
|
|||
|
|
@ -77,19 +77,9 @@ const PublicProfile$json = {
|
|||
'10': 'secretVerificationToken',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'timestamp',
|
||||
'3': 9,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'9': 1,
|
||||
'10': 'timestamp',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_secret_verification_token'},
|
||||
{'1': '_timestamp'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -101,5 +91,4 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
|
|||
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
||||
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
||||
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX'
|
||||
'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA==');
|
||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');
|
||||
|
|
|
|||
|
|
@ -17,5 +17,4 @@ message PublicProfile {
|
|||
bytes signed_prekey_signature = 6;
|
||||
int64 signed_prekey_id = 7;
|
||||
optional bytes secret_verification_token = 8;
|
||||
optional int64 timestamp = 9;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
|
|
@ -22,11 +21,10 @@ 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_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/import_from_gallery.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/data_and_storage/manage_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_media.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/informations.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/help/changelog.view.dart';
|
||||
|
|
@ -39,7 +37,6 @@ 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/privacy.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/profile/modify_avatar.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
|
||||
|
|
@ -49,10 +46,7 @@ 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_welcome.view.dart';
|
||||
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final routerProvider = GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.home,
|
||||
|
|
@ -206,10 +200,6 @@ final routerProvider = GoRouter(
|
|||
path: 'user_discovery',
|
||||
builder: (context, state) => const UserDiscoverySettingsView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'profile_selection',
|
||||
builder: (context, state) => const ProfileSelectionSettingsView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
|
|
@ -221,12 +211,12 @@ final routerProvider = GoRouter(
|
|||
builder: (context, state) => const DataAndStorageView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'manage',
|
||||
builder: (context, state) => const ManageStorageView(),
|
||||
path: 'import',
|
||||
builder: (context, state) => const ImportMediaView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'import_gallery',
|
||||
builder: (context, state) => const ImportFromGalleryView(),
|
||||
path: 'export',
|
||||
builder: (context, state) => const ExportMediaView(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -289,10 +279,6 @@ final routerProvider = GoRouter(
|
|||
path: 'automated_testing',
|
||||
builder: (context, state) => const AutomatedTestingView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'informations',
|
||||
builder: (context, state) => const DeveloperInformationsView(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'reduce_flames',
|
||||
builder: (context, state) => const ReduceFlamesView(),
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import 'package:package_info_plus/package_info_plus.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/twonly.db.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';
|
||||
|
|
@ -96,27 +97,19 @@ class ApiService {
|
|||
Uri.parse(apiUrl),
|
||||
pingInterval: const Duration(seconds: 30),
|
||||
);
|
||||
|
||||
try {
|
||||
await channel.ready.timeout(const Duration(seconds: 10));
|
||||
} catch (e) {
|
||||
channel.sink.close().ignore();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
_channel = channel;
|
||||
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
||||
await _channel!.ready;
|
||||
Log.info('websocket connected to $apiUrl');
|
||||
return true;
|
||||
} catch (e) {
|
||||
_channel = null;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function is called after the user is authenticated at the server
|
||||
Future<void> onAuthenticated() async {
|
||||
await FcmNotificationService.initFCMAfterAuthenticated();
|
||||
await initFCMAfterAuthenticated();
|
||||
_connectionStateController.add(true);
|
||||
|
||||
if (AppState.isInBackgroundTask) {
|
||||
|
|
@ -156,7 +149,6 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> onClosed() async {
|
||||
if (_channel == null) return;
|
||||
Log.info('websocket connection closed');
|
||||
_channel = null;
|
||||
isAuthenticated = false;
|
||||
|
|
@ -188,19 +180,15 @@ class ApiService {
|
|||
_reconnectionDelay = 3;
|
||||
}
|
||||
|
||||
Future<void> close(Function? callback) async {
|
||||
Future<void> close(Function callback) async {
|
||||
Log.info('closing websocket connection');
|
||||
if (_channel != null) {
|
||||
try {
|
||||
await _channel!.sink.close().timeout(const Duration(seconds: 2));
|
||||
} catch (e) {
|
||||
Log.warn('Timeout or error closing websocket: $e');
|
||||
}
|
||||
await _channel!.sink.close();
|
||||
await onClosed();
|
||||
callback?.call();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback?.call();
|
||||
callback();
|
||||
}
|
||||
|
||||
Future<void> listenToNetworkChanges() async {
|
||||
|
|
@ -258,10 +246,7 @@ class ApiService {
|
|||
|
||||
Future<void> _onData(dynamic msgBuffer) async {
|
||||
try {
|
||||
if (msgBuffer is! Uint8List) {
|
||||
msgBuffer = Uint8List.fromList(msgBuffer as List<int>);
|
||||
}
|
||||
final msg = server.ServerToClient.fromBuffer(msgBuffer);
|
||||
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
|
||||
if (msg.v0.hasResponse()) {
|
||||
final completer = _pendingRequests.remove(msg.v0.seq);
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
|
|
@ -441,7 +426,7 @@ class ApiService {
|
|||
|
||||
Future<bool> tryAuthenticateWithToken() async {
|
||||
final apiAuthToken = await SecureStorage.instance.read(
|
||||
key: 'api_auth_token',
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
|
||||
if (apiAuthToken != null) {
|
||||
|
|
@ -479,7 +464,7 @@ class ApiService {
|
|||
Log.info('Switch was successfully.');
|
||||
await UserService.update((u) => u.canUseLoginTokenForAuth = true);
|
||||
await SecureStorage.instance.delete(
|
||||
key: 'api_auth_token',
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -601,7 +586,7 @@ class ApiService {
|
|||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
|
||||
await SecureStorage.instance.write(
|
||||
key: 'api_auth_token',
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ Future<void> handleAdditionalDataMessage(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_AdditionalDataMessage message,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info(
|
||||
'[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId',
|
||||
'Got a additional data message: ${message.senderMessageId} from $groupId',
|
||||
);
|
||||
|
||||
// Prevent message overwrite: reject if a message with this ID already
|
||||
|
|
@ -23,7 +22,7 @@ Future<void> handleAdditionalDataMessage(
|
|||
.getSingleOrNull();
|
||||
if (existing != null && existing.senderId != fromUserId) {
|
||||
Log.warn(
|
||||
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -46,6 +45,6 @@ Future<void> handleAdditionalDataMessage(
|
|||
fromTimestamp(message.timestamp),
|
||||
);
|
||||
if (msg != null) {
|
||||
Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}');
|
||||
Log.info('Inserted a new text message with ID: ${msg.messageId}');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
|||
await handleContactAccept(fromUserId);
|
||||
}
|
||||
|
||||
// contact was already accepted, so just accept the request in the background.
|
||||
await sendCipherText(
|
||||
contact.userId,
|
||||
EncryptedContent(
|
||||
|
|
@ -35,7 +36,6 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
|||
type: EncryptedContent_ContactRequest_Type.ACCEPT,
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -88,17 +88,16 @@ Future<void> handleContactAccept(int fromUserId) async {
|
|||
Future<bool> handleContactRequest(
|
||||
int fromUserId,
|
||||
EncryptedContent_ContactRequest contactRequest,
|
||||
String receiptId,
|
||||
) async {
|
||||
switch (contactRequest.type) {
|
||||
case EncryptedContent_ContactRequest_Type.REQUEST:
|
||||
Log.info('[$receiptId] Got a contact request from $fromUserId');
|
||||
Log.info('Got a contact request from $fromUserId');
|
||||
return handleNewContactRequest(fromUserId);
|
||||
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
||||
Log.info('[$receiptId] Got a contact accept from $fromUserId');
|
||||
Log.info('Got a contact accept from $fromUserId');
|
||||
await handleContactAccept(fromUserId);
|
||||
case EncryptedContent_ContactRequest_Type.REJECT:
|
||||
Log.info('[$receiptId] Got a contact reject from $fromUserId');
|
||||
Log.info('Got a contact reject from $fromUserId');
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
const ContactsCompanion(
|
||||
|
|
@ -115,15 +114,14 @@ Future<void> handleContactUpdate(
|
|||
int fromUserId,
|
||||
EncryptedContent_ContactUpdate contactUpdate,
|
||||
int? senderProfileCounter,
|
||||
String receiptId,
|
||||
) async {
|
||||
switch (contactUpdate.type) {
|
||||
case EncryptedContent_ContactUpdate_Type.REQUEST:
|
||||
Log.info('[$receiptId] Got a contact update request from $fromUserId');
|
||||
Log.info('Got a contact update request from $fromUserId');
|
||||
await sendContactMyProfileData(fromUserId);
|
||||
|
||||
case EncryptedContent_ContactUpdate_Type.UPDATE:
|
||||
Log.info('[$receiptId] Got a contact update $fromUserId');
|
||||
Log.info('Got a contact update $fromUserId');
|
||||
Uint8List? avatarSvgCompressed;
|
||||
if (contactUpdate.hasAvatarSvgCompressed()) {
|
||||
avatarSvgCompressed = Uint8List.fromList(
|
||||
|
|
@ -190,9 +188,8 @@ Future<void> handleContactUpdate(
|
|||
Future<void> handleFlameSync(
|
||||
String groupId,
|
||||
EncryptedContent_FlameSync flameSync,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info('[$receiptId] Got a flameSync for group $groupId');
|
||||
Log.info('Got a flameSync for group $groupId');
|
||||
|
||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null || group.lastFlameCounterChange == null) return;
|
||||
|
|
@ -238,7 +235,6 @@ Future<int?> checkForProfileUpdate(
|
|||
type: EncryptedContent_ContactUpdate_Type.REQUEST,
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ import 'package:twonly/src/utils/log.dart';
|
|||
Future<void> handleErrorMessage(
|
||||
int fromUserId,
|
||||
EncryptedContent_ErrorMessages error,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.error('[$receiptId] Got error from $fromUserId: $error');
|
||||
Log.error('Got error from $fromUserId: $error');
|
||||
|
||||
switch (error.type) {
|
||||
case EncryptedContent_ErrorMessages_Type
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ Future<void> handleGroupCreate(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_GroupCreate newGroup,
|
||||
String receiptId,
|
||||
) async {
|
||||
final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull();
|
||||
final user = await twonlyDB.contactsDao
|
||||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
if (user == null) {
|
||||
// Only contacts can invite other contacts, so this can (via the UI) not happen.
|
||||
Log.error(
|
||||
'[$receiptId] User is not a contact. Aborting.',
|
||||
'User is not a contact. Aborting.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@ Future<void> handleGroupCreate(
|
|||
|
||||
if (group == null) {
|
||||
Log.error(
|
||||
'[$receiptId] Could not create new group. Probably because the group already existed.',
|
||||
'Could not create new group. Probably because the group already existed.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,13 +108,12 @@ Future<void> handleGroupUpdate(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_GroupUpdate update,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info('[$receiptId] Got group update for $groupId from $fromUserId');
|
||||
Log.info('Got group update for $groupId from $fromUserId');
|
||||
|
||||
final actionType = groupActionTypeFromString(update.groupActionType);
|
||||
if (actionType == null) {
|
||||
Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.');
|
||||
Log.error('Group action ${update.groupActionType} is unknown ignoring.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -189,11 +189,10 @@ Future<bool> handleGroupJoin(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_GroupJoin join,
|
||||
String receiptId,
|
||||
) async {
|
||||
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
|
||||
if (!await addNewHiddenContact(fromUserId)) {
|
||||
Log.error('[$receiptId] Got group join, but could not load contact.');
|
||||
Log.error('Got group join, but could not load contact.');
|
||||
// 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
|
||||
// will resend this message.
|
||||
|
|
@ -214,7 +213,6 @@ Future<void> handleResendGroupPublicKey(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_GroupJoin join,
|
||||
String receiptId,
|
||||
) async {
|
||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null || group.myGroupPrivateKey == null) return;
|
||||
|
|
@ -227,7 +225,6 @@ Future<void> handleResendGroupPublicKey(
|
|||
groupPublicKey: keyPair.getPublicKey().serialize(),
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +232,6 @@ Future<void> handleTypingIndicator(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_TypingIndicator indicator,
|
||||
String receiptId,
|
||||
) async {
|
||||
var lastTypeIndicator = const Value<DateTime?>.absent();
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ Future<void> handleMedia(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_Media media,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info(
|
||||
'[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
|
||||
'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
|
||||
);
|
||||
|
||||
late MediaType mediaType;
|
||||
|
|
@ -34,7 +33,7 @@ Future<void> handleMedia(
|
|||
message.senderId != fromUserId ||
|
||||
message.mediaId == null) {
|
||||
Log.warn(
|
||||
'[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
||||
'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -83,13 +82,13 @@ Future<void> handleMedia(
|
|||
if (messageTmp != null) {
|
||||
if (messageTmp.senderId != fromUserId) {
|
||||
Log.warn(
|
||||
'[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
||||
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (messageTmp.mediaId == null) {
|
||||
Log.warn(
|
||||
'[$receiptId] This message already exit without a mediaId. Message is dropped.',
|
||||
'This message already exit without a mediaId. Message is dropped.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -98,7 +97,7 @@ Future<void> handleMedia(
|
|||
);
|
||||
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
|
||||
Log.warn(
|
||||
'[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
|
||||
'This message and media file already exit and was not requested again. Dropping it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -122,9 +121,7 @@ Future<void> handleMedia(
|
|||
MediaFile? mediaFile;
|
||||
Message? message;
|
||||
|
||||
Log.info(
|
||||
'[$receiptId] Starting transaction for media message ${media.senderMessageId}',
|
||||
);
|
||||
Log.info('Starting transaction for media message ${media.senderMessageId}');
|
||||
await twonlyDB.transaction(() async {
|
||||
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
||||
MediaFilesCompanion(
|
||||
|
|
@ -144,7 +141,7 @@ Future<void> handleMedia(
|
|||
);
|
||||
|
||||
if (mediaFile == null) {
|
||||
Log.error('[$receiptId] Could not insert media file into database');
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +165,7 @@ Future<void> handleMedia(
|
|||
);
|
||||
});
|
||||
Log.info(
|
||||
'[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
|
||||
'Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
|
||||
);
|
||||
|
||||
if (message != null && mediaFile != null) {
|
||||
|
|
@ -176,9 +173,7 @@ Future<void> handleMedia(
|
|||
groupId,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
Log.info(
|
||||
'[$receiptId] Inserted a new media message with ID: ${message!.messageId}',
|
||||
);
|
||||
Log.info('Inserted a new media message with ID: ${message!.messageId}');
|
||||
await incFlameCounter(
|
||||
message!.groupId,
|
||||
true,
|
||||
|
|
@ -189,16 +184,12 @@ Future<void> handleMedia(
|
|||
} else {
|
||||
if (mediaFile == null && message == null) {
|
||||
Log.error(
|
||||
'[$receiptId] Could not insert new message as both the message and mediaFile are empty.',
|
||||
'Could not insert new message as both the message and mediaFile are empty.',
|
||||
);
|
||||
} else if (mediaFile == null) {
|
||||
Log.error(
|
||||
'[$receiptId] Could not insert new message as the mediaFile is empty.',
|
||||
);
|
||||
Log.error('Could not insert new message as the mediaFile is empty.');
|
||||
} else {
|
||||
Log.error(
|
||||
'[$receiptId] Could not insert new message as the message is empty.',
|
||||
);
|
||||
Log.error('Could not insert new message as the message is empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -206,7 +197,6 @@ Future<void> handleMedia(
|
|||
Future<void> handleMediaUpdate(
|
||||
int fromUserId,
|
||||
EncryptedContent_MediaUpdate mediaUpdate,
|
||||
String receiptId,
|
||||
) async {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(mediaUpdate.targetMessageId)
|
||||
|
|
@ -214,14 +204,14 @@ Future<void> handleMediaUpdate(
|
|||
if (message == null) {
|
||||
// this can happen, in case the message was already deleted.
|
||||
Log.info(
|
||||
'[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
|
||||
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (message.mediaId == null) {
|
||||
// this can happen, in case the message was already deleted.
|
||||
Log.warn(
|
||||
'[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
|
||||
'Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -230,14 +220,14 @@ Future<void> handleMediaUpdate(
|
|||
);
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'[$receiptId] Got media file update, but media file was not found ${message.mediaId}',
|
||||
'Got media file update, but media file was not found ${message.mediaId}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mediaUpdate.type) {
|
||||
case EncryptedContent_MediaUpdate_Type.REOPENED:
|
||||
Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}');
|
||||
Log.info('Got media file reopened ${mediaFile.mediaId}');
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
message.messageId,
|
||||
const MessagesCompanion(
|
||||
|
|
@ -245,7 +235,7 @@ Future<void> handleMediaUpdate(
|
|||
),
|
||||
);
|
||||
case EncryptedContent_MediaUpdate_Type.STORED:
|
||||
Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}');
|
||||
Log.info('Got media file stored ${mediaFile.mediaId}');
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
await mediaService.storeMediaFile();
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
|
|
@ -256,9 +246,7 @@ Future<void> handleMediaUpdate(
|
|||
);
|
||||
|
||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||
Log.info(
|
||||
'[$receiptId] Got media file decryption error ${mediaFile.mediaId}',
|
||||
);
|
||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.api.dart';
|
||||
|
|
@ -7,27 +6,26 @@ import 'package:twonly/src/utils/log.dart';
|
|||
Future<void> handleMessageUpdate(
|
||||
int contactId,
|
||||
EncryptedContent_MessageUpdate messageUpdate,
|
||||
String receiptId,
|
||||
) async {
|
||||
switch (messageUpdate.type) {
|
||||
case EncryptedContent_MessageUpdate_Type.OPENED:
|
||||
Log.info(
|
||||
'[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}',
|
||||
'Opened message ${messageUpdate.multipleTargetMessageIds}',
|
||||
);
|
||||
try {
|
||||
await twonlyDB.messagesDao.handleMessagesOpened(
|
||||
Value(contactId),
|
||||
contactId,
|
||||
messageUpdate.multipleTargetMessageIds,
|
||||
fromTimestamp(messageUpdate.timestamp),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error handling messages opened: $e');
|
||||
Log.warn(e);
|
||||
}
|
||||
case EncryptedContent_MessageUpdate_Type.DELETE:
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||
return;
|
||||
}
|
||||
Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}');
|
||||
Log.info('Delete message ${messageUpdate.senderMessageId}');
|
||||
try {
|
||||
await twonlyDB.messagesDao.handleMessageDeletion(
|
||||
contactId,
|
||||
|
|
@ -35,13 +33,13 @@ Future<void> handleMessageUpdate(
|
|||
fromTimestamp(messageUpdate.timestamp),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error handling message deletion: $e');
|
||||
Log.warn(e);
|
||||
}
|
||||
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||
return;
|
||||
}
|
||||
Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}');
|
||||
Log.info('Edit message ${messageUpdate.senderMessageId}');
|
||||
try {
|
||||
await twonlyDB.messagesDao.handleTextEdit(
|
||||
contactId,
|
||||
|
|
@ -50,12 +48,12 @@ Future<void> handleMessageUpdate(
|
|||
fromTimestamp(messageUpdate.timestamp),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error handling text edit: $e');
|
||||
Log.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isSender(int fromUserId, String messageId, String receiptId) async {
|
||||
Future<bool> isSender(int fromUserId, String messageId) async {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
|
|
@ -63,6 +61,6 @@ Future<bool> isSender(int fromUserId, String messageId, String receiptId) async
|
|||
if (message.senderId == fromUserId) {
|
||||
return true;
|
||||
}
|
||||
Log.error('[$receiptId] Contact $fromUserId tried to modify the message $messageId');
|
||||
Log.error('Contact $fromUserId tried to modify the message $messageId');
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
|||
Future<void> handlePushKey(
|
||||
int contactId,
|
||||
EncryptedContent_PushKeys pushKeys,
|
||||
String receiptId,
|
||||
) async {
|
||||
switch (pushKeys.type) {
|
||||
case EncryptedContent_PushKeys_Type.REQUEST:
|
||||
Log.info('[$receiptId] Got a pushkey request from $contactId');
|
||||
Log.info('Got a pushkey request from $contactId');
|
||||
if (lastPushKeyRequest.isBefore(
|
||||
clock.now().subtract(const Duration(seconds: 60)),
|
||||
)) {
|
||||
|
|
@ -23,7 +22,7 @@ Future<void> handlePushKey(
|
|||
}
|
||||
|
||||
case EncryptedContent_PushKeys_Type.UPDATE:
|
||||
Log.info('[$receiptId] Got a pushkey update from $contactId');
|
||||
Log.info('Got a pushkey update from $contactId');
|
||||
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,8 @@ Future<void> handleReaction(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_Reaction reaction,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info(
|
||||
'[$receiptId] Got a reaction from for ${reaction.targetMessageId} (remove=${reaction.remove})',
|
||||
);
|
||||
|
||||
Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})');
|
||||
await twonlyDB.reactionsDao.updateReaction(
|
||||
fromUserId,
|
||||
reaction.targetMessageId,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@ Future<void> handleTextMessage(
|
|||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_TextMessage textMessage,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info(
|
||||
'[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId',
|
||||
'Got a text message: ${textMessage.senderMessageId} from $groupId',
|
||||
);
|
||||
|
||||
// Prevent message overwrite: reject if a message with this ID already
|
||||
|
|
@ -24,7 +23,7 @@ Future<void> handleTextMessage(
|
|||
.getSingleOrNull();
|
||||
if (existing != null && existing.senderId != fromUserId) {
|
||||
Log.warn(
|
||||
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -48,6 +47,6 @@ Future<void> handleTextMessage(
|
|||
fromTimestamp(textMessage.timestamp),
|
||||
);
|
||||
if (message != null) {
|
||||
Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}');
|
||||
Log.info('Inserted a new text message with ID: ${message.messageId}');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ void resetUserDiscoveryRequestUpdates() {
|
|||
Future<void> checkForUserDiscoveryChanges(
|
||||
int fromUserId,
|
||||
List<int> receivedVersion,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info('[$receiptId] Checking for a new user discovery version.');
|
||||
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
|
||||
fromUserId,
|
||||
receivedVersion,
|
||||
|
|
@ -28,7 +26,7 @@ Future<void> checkForUserDiscoveryChanges(
|
|||
// Only request a new version once per app session
|
||||
return;
|
||||
}
|
||||
Log.info('[$receiptId] Having old version from contact. Requesting new version.');
|
||||
Log.info('Having old version from contact. Requesting new version.');
|
||||
_requestedUpdates.add(fromUserId);
|
||||
await sendCipherText(
|
||||
fromUserId,
|
||||
|
|
@ -37,7 +35,6 @@ Future<void> checkForUserDiscoveryChanges(
|
|||
currentVersion: currentVersion.toList(),
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,19 +42,18 @@ Future<void> checkForUserDiscoveryChanges(
|
|||
Future<void> handleUserDiscoveryRequest(
|
||||
int fromUserId,
|
||||
EncryptedContent_UserDiscoveryRequest request,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info('[$receiptId] Got a user discovery request');
|
||||
Log.info('Got a user discovery request');
|
||||
|
||||
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
||||
Log.warn('[$receiptId] Got a user discovery request while it is disabled');
|
||||
Log.warn('Got a user discovery request while it is disabled');
|
||||
return;
|
||||
}
|
||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||
|
||||
if (!UserDiscoveryService.isContactAllowed(contact)) {
|
||||
Log.warn(
|
||||
'[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
|
||||
'Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -67,7 +63,7 @@ Future<void> handleUserDiscoveryRequest(
|
|||
request.currentVersion,
|
||||
);
|
||||
if (newMessages != null && newMessages.isNotEmpty) {
|
||||
Log.info('[$receiptId] Sending ${newMessages.length} user discovery messages');
|
||||
Log.info('Sending ${newMessages.length} user discovery messages');
|
||||
await sendCipherText(
|
||||
fromUserId,
|
||||
EncryptedContent(
|
||||
|
|
@ -75,23 +71,21 @@ Future<void> handleUserDiscoveryRequest(
|
|||
messages: newMessages,
|
||||
),
|
||||
),
|
||||
blocking: false,
|
||||
);
|
||||
} else {
|
||||
Log.info('[$receiptId] Got update request, but there are no new updates for the user');
|
||||
Log.info('Got update request, but there are no new updates for the user');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleUserDiscoveryUpdate(
|
||||
int fromUserId,
|
||||
EncryptedContent_UserDiscoveryUpdate update,
|
||||
String receiptId,
|
||||
) async {
|
||||
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
||||
Log.warn('[$receiptId] Got a user discovery update while it is disabled');
|
||||
Log.warn('Got a user discovery update while it is disabled');
|
||||
return;
|
||||
}
|
||||
Log.info('[$receiptId] Got ${update.messages.length} user discovery messages');
|
||||
Log.info('Got ${update.messages.length} user discovery messages');
|
||||
await UserDiscoveryService.handleNewMessages(
|
||||
fromUserId,
|
||||
update.messages.map(Uint8List.fromList).toList(),
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ Future<void> downloadFileFast(
|
|||
} else {
|
||||
if (response.statusCode == 404 || response.statusCode == 403) {
|
||||
Log.error(
|
||||
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
|
||||
'Got ${response.statusCode} from server. Requesting upload again',
|
||||
);
|
||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
||||
await requestMediaReupload(media.mediaId);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ Future<void> _protectMediaUpload(
|
|||
) async {
|
||||
final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new);
|
||||
await mutex.protect(action);
|
||||
_uploadMutexes.remove(mediaId);
|
||||
}
|
||||
|
||||
Future<void> reuploadMediaFiles() async {
|
||||
|
|
@ -51,7 +52,7 @@ Future<void> reuploadMediaFiles() async {
|
|||
|
||||
final contacts = <int, Contact>{};
|
||||
|
||||
for (final receipt in receipts) {
|
||||
for (var receipt in receipts) {
|
||||
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
|
||||
final twentyFourHoursAgo = DateTime.now().subtract(
|
||||
const Duration(hours: 6),
|
||||
|
|
@ -64,6 +65,20 @@ 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;
|
||||
if (receipt.messageId == null) {
|
||||
Log.info('Message not in receipt. Loading it from the content.');
|
||||
|
|
@ -399,9 +414,6 @@ Future<void> insertMediaFileInMessagesTable(
|
|||
);
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
if (message != null) {
|
||||
Log.info(
|
||||
'Created message ${message.messageId} for media ${message.mediaId}',
|
||||
);
|
||||
// de-archive contact when sending a new message
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
message.groupId,
|
||||
|
|
@ -433,10 +445,6 @@ Future<void> _startBackgroundMediaUploadInternal(
|
|||
|
||||
if (mediaService.mediaFile.uploadState == UploadState.initialized ||
|
||||
mediaService.mediaFile.uploadState == UploadState.preprocessing) {
|
||||
Log.info(
|
||||
'Hanlding media file ${mediaService.mediaFile.mediaId} in ${mediaService.mediaFile.uploadState}',
|
||||
);
|
||||
|
||||
await mediaService.setUploadState(UploadState.preprocessing);
|
||||
|
||||
if (!mediaService.tempPath.existsSync()) {
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ Future<void> retransmitAllMessages() async {
|
|||
});
|
||||
}
|
||||
|
||||
final Map<String, Mutex> _tryToSendLocks = {};
|
||||
|
||||
// When the ackByServerAt is set this value is written in the receipted
|
||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||
String? receiptId,
|
||||
|
|
@ -70,41 +68,15 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
bool onlyReturnEncryptedData = false,
|
||||
bool blocking = true,
|
||||
}) 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 (receiptId == null && receipt == null) return null;
|
||||
|
||||
try {
|
||||
if (receiptId == null && receipt == null) return null;
|
||||
if (receipt == null) {
|
||||
// ignore: parameter_assignments
|
||||
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
|
||||
if (receipt == null) {
|
||||
Log.warn('[$receiptId] Receipt not found.');
|
||||
Log.warn('Receipt not found.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +120,8 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
message.encryptedContent,
|
||||
);
|
||||
|
||||
Log.info('Uploading ${receipt.receiptId}.');
|
||||
|
||||
Uint8List? pushData;
|
||||
if (receipt.retryCount == 0) {
|
||||
final pushNotification = await getPushNotificationFromEncryptedContent(
|
||||
|
|
@ -192,12 +166,9 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
}
|
||||
|
||||
if (onlyReturnEncryptedData) {
|
||||
Log.info('Returning message with receiptID ${receipt.receiptId}.');
|
||||
return (message.writeToBuffer(), pushData);
|
||||
}
|
||||
|
||||
Log.info('Uploading message with receiptID ${receipt.receiptId}.');
|
||||
|
||||
final resp = await apiService.sendTextMessage(
|
||||
receipt.contactId,
|
||||
message.writeToBuffer(),
|
||||
|
|
@ -205,7 +176,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
);
|
||||
|
||||
if (resp.isError) {
|
||||
Log.warn('Could not transmit ${receipt.receiptId} got ${resp.error}.');
|
||||
Log.warn('Could not transmit message got ${resp.error}.');
|
||||
if (resp.error == ErrorCode.UserIdNotFound) {
|
||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
|
|
@ -239,7 +210,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('[$receiptId] unknown error when sending message: $e');
|
||||
Log.error('Unknown Error when sending message: $e');
|
||||
if (receipt != null) {
|
||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||
}
|
||||
|
|
@ -345,54 +316,6 @@ 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(
|
||||
String groupId,
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
|
|
@ -486,17 +409,6 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
);
|
||||
|
||||
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(
|
||||
receipt: receipt,
|
||||
onlyReturnEncryptedData: onlyReturnEncryptedData,
|
||||
|
|
@ -580,23 +492,5 @@ Future<void> sendContactMyProfileData(int contactId) async {
|
|||
username: userService.currentUser.username,
|
||||
),
|
||||
);
|
||||
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(', ');
|
||||
await sendCipherText(contactId, encryptedContent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:clock/clock.dart';
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
|
|
@ -32,7 +31,6 @@ import 'package:twonly/src/services/api/messages.api.dart';
|
|||
import 'package:twonly/src/services/group.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/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -75,7 +73,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
|||
|
||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||
AppState.gotMessageFromServer = true;
|
||||
Log.info('All messages from the server proccessed.');
|
||||
Log.info('Message from server proccessed.');
|
||||
}
|
||||
|
||||
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
||||
|
|
@ -88,19 +86,10 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
|||
final receiptId = message.receiptId;
|
||||
|
||||
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 {
|
||||
try {
|
||||
await _handleClient2ClientMessage(newMessage, message);
|
||||
} finally {
|
||||
_messageLocks.remove(receiptId);
|
||||
}
|
||||
});
|
||||
_messageLocks.remove(receiptId);
|
||||
}
|
||||
|
||||
Future<void> _handleClient2ClientMessage(
|
||||
|
|
@ -114,11 +103,11 @@ Future<void> _handleClient2ClientMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
Log.info('[$receiptId] Started processing message');
|
||||
Log.info('Started processing message with receiptId $receiptId');
|
||||
|
||||
switch (message.type) {
|
||||
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
||||
Log.info('[$receiptId] Got delivery receipt!');
|
||||
Log.info('Got delivery receipt for $receiptId!');
|
||||
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
|
||||
|
||||
case Message_Type.PLAINTEXT_CONTENT:
|
||||
|
|
@ -131,13 +120,13 @@ Future<void> _handleClient2ClientMessage(
|
|||
await handleSessionResync(fromUserId);
|
||||
}
|
||||
Log.info(
|
||||
'[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}',
|
||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
||||
);
|
||||
retry = true;
|
||||
}
|
||||
if (message.plaintextContent.hasRetryControlError()) {
|
||||
Log.info(
|
||||
'[$receiptId] Got access control error. Resending message.',
|
||||
'Got access control error for $receiptId. Resending message.',
|
||||
);
|
||||
retry = true;
|
||||
}
|
||||
|
|
@ -152,13 +141,7 @@ Future<void> _handleClient2ClientMessage(
|
|||
ackByServerAt: const Value(null),
|
||||
),
|
||||
);
|
||||
Log.info(
|
||||
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
|
||||
);
|
||||
await tryToSendCompleteMessage(
|
||||
receiptId: newReceiptId,
|
||||
blocking: false,
|
||||
);
|
||||
await tryToSendCompleteMessage(receiptId: newReceiptId);
|
||||
}
|
||||
|
||||
case Message_Type.CIPHERTEXT:
|
||||
|
|
@ -214,6 +197,7 @@ Future<void> _handleClient2ClientMessage(
|
|||
receiptIdDB = const Value.absent();
|
||||
} else {
|
||||
// Message was successful processed
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,9 +213,9 @@ Future<void> _handleClient2ClientMessage(
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('[$receiptId] Error inserting receipt: $e');
|
||||
Log.warn(e);
|
||||
}
|
||||
await tryToSendCompleteMessage(receiptId: receiptId, blocking: false);
|
||||
await tryToSendCompleteMessage(receiptId: receiptId);
|
||||
}
|
||||
case Message_Type.TEST_NOTIFICATION:
|
||||
break;
|
||||
|
|
@ -239,9 +223,9 @@ Future<void> _handleClient2ClientMessage(
|
|||
|
||||
try {
|
||||
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
||||
Log.info('[$receiptId] Finished processing');
|
||||
Log.info('Got a message with receiptId $receiptId');
|
||||
} catch (e) {
|
||||
Log.error('[$receiptId] Error marking message as received: $e');
|
||||
Log.error('Error marking message as received $receiptId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,26 +235,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
Message_Type messageType,
|
||||
String receiptId,
|
||||
) async {
|
||||
Log.info('[$receiptId] calling signalDecryptMessage');
|
||||
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
|
||||
final (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
|
||||
fromUserId,
|
||||
encryptedContentRaw,
|
||||
messageType.value,
|
||||
);
|
||||
|
||||
if (encryptedContent == null) {
|
||||
if (decryptionErrorType == null) {
|
||||
// Duplicate message
|
||||
return (null, null);
|
||||
}
|
||||
return (
|
||||
null,
|
||||
PlaintextContent(
|
||||
decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage(
|
||||
type: decryptionErrorType ??=
|
||||
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||
),
|
||||
),
|
||||
PlaintextContent()
|
||||
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
|
||||
..type = decryptionErrorType),
|
||||
);
|
||||
}
|
||||
|
||||
Log.info('[$receiptId] Calling handleEncryptedMessage');
|
||||
Log.info('Calling handleEncryptedMessage for $receiptId');
|
||||
|
||||
final (a, b) = await handleEncryptedMessage(
|
||||
fromUserId,
|
||||
|
|
@ -279,17 +263,11 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
|||
receiptId,
|
||||
);
|
||||
|
||||
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||
Log.info('Finished handleEncryptedMessage for $receiptId');
|
||||
|
||||
if (a == null && b == null) {
|
||||
unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
|
||||
if (Platform.isAndroid) {
|
||||
// Message was handled without any error. Show push notification to the user for Android.
|
||||
await showPushNotificationFromServerMessages(
|
||||
fromUserId,
|
||||
encryptedContent,
|
||||
);
|
||||
}
|
||||
if (Platform.isAndroid && a == null && b == null) {
|
||||
// Message was handled without any error -> Show push notification to the user.
|
||||
await showPushNotificationFromServerMessages(fromUserId, encryptedContent);
|
||||
}
|
||||
|
||||
return (a, b);
|
||||
|
|
@ -316,16 +294,11 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await checkForUserDiscoveryChanges(
|
||||
fromUserId,
|
||||
content.senderUserDiscoveryVersion,
|
||||
receiptId,
|
||||
);
|
||||
}
|
||||
|
||||
if (content.hasContactRequest()) {
|
||||
if (!await handleContactRequest(
|
||||
fromUserId,
|
||||
content.contactRequest,
|
||||
receiptId,
|
||||
)) {
|
||||
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
|
||||
return (
|
||||
null,
|
||||
PlaintextContent()
|
||||
|
|
@ -339,7 +312,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await handleErrorMessage(
|
||||
fromUserId,
|
||||
content.errorMessages,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -349,7 +321,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.contactUpdate,
|
||||
senderProfileCounter,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -358,7 +329,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await handleUserDiscoveryRequest(
|
||||
fromUserId,
|
||||
content.userDiscoveryRequest,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -367,13 +337,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await handleUserDiscoveryUpdate(
|
||||
fromUserId,
|
||||
content.userDiscoveryUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasPushKeys()) {
|
||||
await handlePushKey(fromUserId, content.pushKeys, receiptId);
|
||||
await handlePushKey(fromUserId, content.pushKeys);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +350,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await handleMessageUpdate(
|
||||
fromUserId,
|
||||
content.messageUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -398,13 +366,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
await handleMediaUpdate(
|
||||
fromUserId,
|
||||
content.mediaUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (!content.hasGroupId()) {
|
||||
Log.error('[$receiptId] Messages should have a groupId $fromUserId.');
|
||||
Log.error('Messages should have a groupId $fromUserId.');
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
|
|
@ -413,7 +380,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupCreate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -426,12 +392,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
Log.info(
|
||||
'[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
|
||||
'Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
|
||||
);
|
||||
if (contact == null || !contact.accepted || contact.deletedByUser) {
|
||||
await handleNewContactRequest(fromUserId);
|
||||
Log.error(
|
||||
'[$receiptId] User tries to send message to direct chat while the user does not exist!',
|
||||
'User tries to send message to direct chat while the user does not exists !',
|
||||
);
|
||||
return (
|
||||
EncryptedContent(
|
||||
|
|
@ -445,7 +411,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
);
|
||||
}
|
||||
Log.info(
|
||||
'[$receiptId] Creating new DirectChat between two users',
|
||||
'Creating new DirectChat between two users',
|
||||
);
|
||||
await twonlyDB.groupsDao.createNewDirectChat(
|
||||
fromUserId,
|
||||
|
|
@ -456,7 +422,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
} else {
|
||||
if (content.hasGroupJoin()) {
|
||||
Log.error(
|
||||
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
|
||||
'Got group join message, but group does not exists 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.
|
||||
return (
|
||||
|
|
@ -466,15 +432,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
);
|
||||
}
|
||||
|
||||
Log.error(
|
||||
'[$receiptId] User $fromUserId tried to access group ${content.groupId}.',
|
||||
);
|
||||
Log.error('User $fromUserId tried to access group ${content.groupId}.');
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (content.hasFlameSync()) {
|
||||
await handleFlameSync(content.groupId, content.flameSync, receiptId);
|
||||
await handleFlameSync(content.groupId, content.flameSync);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
|
|
@ -483,7 +447,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupUpdate,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -493,7 +456,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupJoin,
|
||||
receiptId,
|
||||
)) {
|
||||
return (
|
||||
null,
|
||||
|
|
@ -509,7 +471,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.groupJoin,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -519,7 +480,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.additionalDataMessage,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -529,7 +489,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.textMessage,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -539,7 +498,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.reaction,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -549,7 +507,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.media,
|
||||
receiptId,
|
||||
);
|
||||
return (null, null);
|
||||
}
|
||||
|
|
@ -559,7 +516,6 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
fromUserId,
|
||||
content.groupId,
|
||||
content.typingIndicator,
|
||||
receiptId,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/key_manager.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/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
||||
|
|
@ -128,7 +129,7 @@ Future<Map<String, String>?> getAuthenticationHeader() async {
|
|||
};
|
||||
} else {
|
||||
final apiAuthTokenRaw = await SecureStorage.instance.read(
|
||||
key: 'api_auth_token',
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
|
||||
if (apiAuthTokenRaw == null) {
|
||||
|
|
|
|||
|
|
@ -119,15 +119,9 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
|
|||
if (!shouldBeExecuted) return;
|
||||
|
||||
Log.info('eu.twonly.periodic_task was called.');
|
||||
AppState.gotMessageFromServer = false;
|
||||
|
||||
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()) {
|
||||
Log.info('Could not connect to the api. Returning early.');
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -65,18 +65,23 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
|||
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
||||
const zero = (counter: 0, isExpiring: false);
|
||||
if (group == null) return zero;
|
||||
if (group.lastMessageSend == null || group.lastMessageReceived == null || group.lastFlameCounterChange == null) {
|
||||
if (group.lastMessageSend == null ||
|
||||
group.lastMessageReceived == null ||
|
||||
group.lastFlameCounterChange == null) {
|
||||
return zero;
|
||||
}
|
||||
final now = clock.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) && group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
||||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
||||
// Flame is expiring when today no exchange has happened yet:
|
||||
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
||||
final isExpiring = group.lastMessageSend!.isBefore(oneDayAgo) || group.lastMessageReceived!.isBefore(oneDayAgo);
|
||||
final isExpiring =
|
||||
group.lastMessageSend!.isBefore(oneDayAgo) ||
|
||||
group.lastMessageReceived!.isBefore(oneDayAgo);
|
||||
return (counter: group.flameCounter, isExpiring: isExpiring);
|
||||
} else {
|
||||
return zero;
|
||||
|
|
@ -117,7 +122,8 @@ Future<void> incFlameCounter(
|
|||
final now = clock.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||
if (group.lastMessageSend!.isBefore(twoDaysAgo) || group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
if (group.lastMessageSend!.isBefore(twoDaysAgo) ||
|
||||
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||
flameCounter = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -129,21 +135,25 @@ Future<void> incFlameCounter(
|
|||
final now = clock.now();
|
||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||
|
||||
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||
if (group.lastFlameCounterChange == null ||
|
||||
group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||
// last flame update was yesterday. check if it can be updated.
|
||||
var updateFlame = false;
|
||||
if (received) {
|
||||
if (group.lastMessageSend != null && group.lastMessageSend!.isAfter(startOfToday)) {
|
||||
if (group.lastMessageSend != null &&
|
||||
group.lastMessageSend!.isAfter(startOfToday)) {
|
||||
// today a message was already send -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
} else if (group.lastMessageReceived != null && group.lastMessageReceived!.isAfter(startOfToday)) {
|
||||
} else if (group.lastMessageReceived != null &&
|
||||
group.lastMessageReceived!.isAfter(startOfToday)) {
|
||||
// today a message was already received -> update flame
|
||||
updateFlame = true;
|
||||
}
|
||||
if (updateFlame) {
|
||||
flameCounter += 1;
|
||||
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(timestamp)) {
|
||||
if (group.lastFlameCounterChange == null ||
|
||||
group.lastFlameCounterChange!.isBefore(timestamp)) {
|
||||
// only update if the timestamp is newer
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
}
|
||||
|
|
@ -160,11 +170,13 @@ Future<void> incFlameCounter(
|
|||
}
|
||||
|
||||
if (received) {
|
||||
if (group.lastMessageReceived == null || group.lastMessageReceived!.isBefore(timestamp)) {
|
||||
if (group.lastMessageReceived == null ||
|
||||
group.lastMessageReceived!.isBefore(timestamp)) {
|
||||
lastMessageReceived = Value(timestamp);
|
||||
}
|
||||
} else {
|
||||
if (group.lastMessageSend == null || group.lastMessageSend!.isBefore(timestamp)) {
|
||||
if (group.lastMessageSend == null ||
|
||||
group.lastMessageSend!.isBefore(timestamp)) {
|
||||
lastMessageSend = Value(timestamp);
|
||||
}
|
||||
}
|
||||
|
|
@ -191,18 +203,3 @@ bool isItPossibleToRestoreFlames(Group group) {
|
|||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,14 @@ import 'dart:typed_data';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.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/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/providers/routing.provider.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/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
|
||||
class KeyVerificationService {
|
||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||
|
|
@ -73,18 +70,6 @@ class KeyVerificationService {
|
|||
VerificationType.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:io';
|
|||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:path/path.dart';
|
||||
|
|
@ -73,13 +72,6 @@ class MediaFileService {
|
|||
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] ?? [];
|
||||
|
||||
// in case messages in empty the file will be deleted, as delete is true by default
|
||||
|
|
@ -92,15 +84,6 @@ class MediaFileService {
|
|||
if (message.openedAt == null) {
|
||||
// Message was not yet opened from all persons, so wait...
|
||||
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 ||
|
||||
mediaFile.displayLimitInMilliseconds != null) {
|
||||
// Message was opened by all persons, and they can not reopen the image.
|
||||
|
|
@ -213,40 +196,18 @@ class MediaFileService {
|
|||
}
|
||||
|
||||
Future<void> createThumbnail() async {
|
||||
if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
|
||||
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();
|
||||
}
|
||||
if (!storedPath.existsSync()) {
|
||||
Log.error('Could not create Thumbnail as stored media does not exists.');
|
||||
return;
|
||||
}
|
||||
var success = false;
|
||||
switch (mediaFile.type) {
|
||||
case MediaType.gif:
|
||||
success = await createThumbnailsForGif(storedPath, thumbnailPath);
|
||||
case MediaType.image:
|
||||
success = await createThumbnailsForImage(storedPath, thumbnailPath);
|
||||
case MediaType.video:
|
||||
success = await createThumbnailsForVideo(storedPath, thumbnailPath);
|
||||
case MediaType.audio:
|
||||
case MediaType.image:
|
||||
// all images are already compress..
|
||||
break;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
const MediaFilesCompanion(hasThumbnail: Value(true)),
|
||||
);
|
||||
await updateFromDB();
|
||||
case MediaType.video:
|
||||
await createThumbnailsForVideo(storedPath, thumbnailPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,11 +253,7 @@ class MediaFileService {
|
|||
tempPath.existsSync();
|
||||
|
||||
bool get imagePreviewAvailable =>
|
||||
mediaFile.hasThumbnail ||
|
||||
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
|
||||
mediaFile.type == MediaType.audio ||
|
||||
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
|
||||
storedPath.existsSync() && storedPath.lengthSync() > 0);
|
||||
thumbnailPath.existsSync() || storedPath.existsSync();
|
||||
|
||||
Future<void> storeMediaFile() async {
|
||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||
|
|
@ -318,7 +275,6 @@ class MediaFileService {
|
|||
} else {
|
||||
await saveImageToGallery(
|
||||
storedPath.readAsBytesSync(),
|
||||
createdAt: mediaFile.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -328,24 +284,10 @@ class MediaFileService {
|
|||
);
|
||||
}
|
||||
unawaited(createThumbnail());
|
||||
await calculateAndSaveSize();
|
||||
await hashMediaFile();
|
||||
// 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 {
|
||||
late final List<int> checksum;
|
||||
if (storedPath.existsSync()) {
|
||||
|
|
@ -446,7 +388,7 @@ class MediaFileService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!storedPath.existsSync() || storedPath.lengthSync() == 0) {
|
||||
if (!storedPath.existsSync()) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||
|
|
@ -455,71 +397,15 @@ class MediaFileService {
|
|||
}
|
||||
|
||||
try {
|
||||
final bytes = await storedPath.readAsBytes();
|
||||
final result = await compute(_processImageCrop, bytes);
|
||||
|
||||
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(
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CropResult {
|
||||
const _CropResult(this.pngBytes, this.isCropped);
|
||||
final Uint8List? pngBytes;
|
||||
final bool isCropped;
|
||||
}
|
||||
|
||||
_CropResult _processImageCrop(Uint8List bytes) {
|
||||
final bytes = storedPath.readAsBytesSync();
|
||||
final image = img.decodeImage(bytes);
|
||||
if (image == null) return const _CropResult(null, false);
|
||||
if (image == null) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
const MediaFilesCompanion(hasCropAnalyzed: Value(true)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var minY = 0;
|
||||
var maxY = image.height - 1;
|
||||
|
|
@ -590,9 +476,44 @@ _CropResult _processImageCrop(Uint8List bytes) {
|
|||
height: newHeight,
|
||||
);
|
||||
final pngBytes = img.encodePng(cropped);
|
||||
return _CropResult(pngBytes, true);
|
||||
final webpBytes = await FlutterImageCompress.compressWithList(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return const _CropResult(null, false);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,18 @@
|
|||
import 'dart:io';
|
||||
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:twonly/src/utils/log.dart';
|
||||
|
||||
Future<bool> createThumbnailsForVideo(
|
||||
Future<void> createThumbnailsForVideo(
|
||||
File sourceFile,
|
||||
File destinationFile,
|
||||
) async {
|
||||
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()) {
|
||||
destinationFile.deleteSync();
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (destinationFile.existsSync()) {
|
||||
if (destinationFile.lengthSync() > 0) {
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
destinationFile.deleteSync();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final images = await ProVideoEditor.instance.getThumbnails(
|
||||
ThumbnailConfigs(
|
||||
video: EditorVideo.file(sourceFile),
|
||||
|
|
@ -44,174 +24,15 @@ Future<bool> createThumbnailsForVideo(
|
|||
),
|
||||
);
|
||||
|
||||
if (images.isNotEmpty && images.first.isNotEmpty) {
|
||||
if (images.isNotEmpty) {
|
||||
stopwatch.stop();
|
||||
await destinationFile.writeAsBytes(images.first);
|
||||
if (destinationFile.existsSync() && destinationFile.lengthSync() > 0) {
|
||||
destinationFile.writeAsBytesSync(images.first);
|
||||
Log.info(
|
||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the video thumbnail.',
|
||||
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error creating video thumbnail: $e');
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.warn(
|
||||
'Thumbnail creation failed for the video.',
|
||||
'Thumbnail creation failed for the video with exit code.',
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
|
@ -15,7 +14,6 @@ import 'package:twonly/src/utils/log.dart';
|
|||
class MemoriesState {
|
||||
const MemoriesState({
|
||||
required this.filesToMigrate,
|
||||
required this.totalFilesToMigrate,
|
||||
required this.galleryItems,
|
||||
required this.months,
|
||||
required this.orderedByMonth,
|
||||
|
|
@ -23,21 +21,16 @@ class MemoriesState {
|
|||
});
|
||||
|
||||
final int filesToMigrate;
|
||||
final int totalFilesToMigrate;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final List<String> months;
|
||||
final Map<String, List<int>> orderedByMonth;
|
||||
final Map<int, List<MemoryItem>> galleryItemsLastYears;
|
||||
|
||||
bool get isLoading => filesToMigrate > 0;
|
||||
double get migrationProgress => totalFilesToMigrate > 0
|
||||
? (totalFilesToMigrate - filesToMigrate) / totalFilesToMigrate
|
||||
: 0;
|
||||
bool get isEmpty => galleryItems.isEmpty && filesToMigrate == 0;
|
||||
|
||||
MemoriesState copyWith({
|
||||
int? filesToMigrate,
|
||||
int? totalFilesToMigrate,
|
||||
List<MemoryItem>? galleryItems,
|
||||
List<String>? months,
|
||||
Map<String, List<int>>? orderedByMonth,
|
||||
|
|
@ -45,7 +38,6 @@ class MemoriesState {
|
|||
}) {
|
||||
return MemoriesState(
|
||||
filesToMigrate: filesToMigrate ?? this.filesToMigrate,
|
||||
totalFilesToMigrate: totalFilesToMigrate ?? this.totalFilesToMigrate,
|
||||
galleryItems: galleryItems ?? this.galleryItems,
|
||||
months: months ?? this.months,
|
||||
orderedByMonth: orderedByMonth ?? this.orderedByMonth,
|
||||
|
|
@ -70,7 +62,6 @@ class MemoriesService {
|
|||
|
||||
MemoriesState _currentState = const MemoriesState(
|
||||
filesToMigrate: 0,
|
||||
totalFilesToMigrate: 0,
|
||||
galleryItems: [],
|
||||
months: [],
|
||||
orderedByMonth: {},
|
||||
|
|
@ -97,10 +88,14 @@ class MemoriesService {
|
|||
final mediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
|
||||
mediaIds,
|
||||
);
|
||||
final mediaFileMap = {for (final m in mediaFiles) m.mediaId: m};
|
||||
|
||||
final allContacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
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) {
|
||||
final map = itemJson as Map<String, dynamic>;
|
||||
|
|
@ -108,41 +103,20 @@ class MemoriesService {
|
|||
final senderUserId = map['senderUserId'] as int?;
|
||||
if (mediaId == null) continue;
|
||||
|
||||
mediaIdToSender[mediaId] = senderUserId != null
|
||||
? contactMap[senderUserId]
|
||||
: null;
|
||||
}
|
||||
final mediaFile = mediaFileMap[mediaId];
|
||||
if (mediaFile == null) continue;
|
||||
|
||||
_cachedState = _computeState(
|
||||
mediaFiles: mediaFiles,
|
||||
mediaIdToSender: mediaIdToSender,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error prewarming memories cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static MemoriesState _computeState({
|
||||
required List<MediaFile> mediaFiles,
|
||||
required Map<String, Contact?> mediaIdToSender,
|
||||
int filesToMigrate = 0,
|
||||
}) {
|
||||
final now = clock.now();
|
||||
final tempGalleryItems = <MemoryItem>[];
|
||||
final tempGalleryItemsLastYears = <int, List<MemoryItem>>{};
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
|
||||
final senderContact = mediaIdToSender[mediaFile.mediaId];
|
||||
final contact = senderUserId != null
|
||||
? contactMap[senderUserId]
|
||||
: null;
|
||||
final item = MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
sender: senderContact,
|
||||
sender: contact,
|
||||
);
|
||||
|
||||
tempGalleryItems.add(item);
|
||||
|
||||
if (mediaFile.createdAt.month == now.month &&
|
||||
|
|
@ -154,13 +128,6 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort descending by creation date
|
||||
tempGalleryItems.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
|
||||
final tempOrderedByMonth = <String, List<int>>{};
|
||||
final tempMonths = <String>[];
|
||||
var lastMonth = '';
|
||||
|
|
@ -188,108 +155,63 @@ class MemoriesService {
|
|||
final sortedGalleryItemsLastYears =
|
||||
SplayTreeMap<int, List<MemoryItem>>.from(tempGalleryItemsLastYears);
|
||||
|
||||
return MemoriesState(
|
||||
filesToMigrate: filesToMigrate,
|
||||
totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating.
|
||||
_cachedState = MemoriesState(
|
||||
filesToMigrate: 0,
|
||||
galleryItems: tempGalleryItems,
|
||||
months: tempMonths,
|
||||
orderedByMonth: tempOrderedByMonth,
|
||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error prewarming memories cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initAsync() async {
|
||||
try {
|
||||
// Start DB subscription first so files with existing thumbnails are shown immediately.
|
||||
// 1. Perform Inventory / Migration of non-hashed stored files
|
||||
final nonHashedFiles = await twonlyDB.mediaFilesDao
|
||||
.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);
|
||||
|
||||
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 now = clock.now();
|
||||
final tempGalleryItems = <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,
|
||||
|
|
@ -308,24 +230,82 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
final newState = _computeState(
|
||||
mediaFiles: mediaFiles,
|
||||
mediaIdToSender: mediaIdToSenderContact,
|
||||
filesToMigrate: _currentState.filesToMigrate,
|
||||
).copyWith(totalFilesToMigrate: _currentState.totalFilesToMigrate);
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final mediaService = MediaFileService(mediaFile);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
|
||||
for (final item in newState.galleryItems) {
|
||||
if (!item.mediaService.mediaFile.hasThumbnail &&
|
||||
item.mediaService.mediaFile.type != MediaType.audio) {
|
||||
unawaited(item.mediaService.createThumbnail());
|
||||
if (mediaService.mediaFile.type == MediaType.video) {
|
||||
if (!mediaService.thumbnailPath.existsSync()) {
|
||||
unawaited(mediaService.createThumbnail());
|
||||
}
|
||||
}
|
||||
|
||||
final senderContact = mediaIdToSenderContact[mediaFile.mediaId];
|
||||
final item = MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
sender: senderContact,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort descending by creation date
|
||||
tempGalleryItems.sort(
|
||||
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
|
||||
a.mediaService.mediaFile.createdAt,
|
||||
),
|
||||
);
|
||||
|
||||
final tempOrderedByMonth = <String, List<int>>{};
|
||||
final tempMonths = <String>[];
|
||||
var lastMonth = '';
|
||||
|
||||
// High performance grouping leveraging pre-computed createdAtMonth column
|
||||
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);
|
||||
|
||||
final newState = MemoriesState(
|
||||
filesToMigrate: _currentState.filesToMigrate,
|
||||
galleryItems: tempGalleryItems,
|
||||
months: tempMonths,
|
||||
orderedByMonth: tempOrderedByMonth,
|
||||
galleryItemsLastYears: sortedGalleryItemsLastYears,
|
||||
);
|
||||
|
||||
_cachedState = newState;
|
||||
_updateState(newState);
|
||||
_updateStateWithObject(newState);
|
||||
|
||||
// Persist to KeyValueStore cache asynchronously
|
||||
final cacheList = newState.galleryItems
|
||||
final cacheList = tempGalleryItems
|
||||
.map(
|
||||
(item) => {
|
||||
'mediaId': item.mediaService.mediaFile.mediaId,
|
||||
|
|
@ -339,17 +319,15 @@ class MemoriesService {
|
|||
}
|
||||
}
|
||||
|
||||
void _updateState(MemoriesState newState) {
|
||||
void _updateStateWithObject(MemoriesState newState) {
|
||||
_currentState = newState;
|
||||
_notifyState();
|
||||
if (!_stateController.isClosed) {
|
||||
_stateController.add(_currentState);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMigrationCount(int filesToMigrate) {
|
||||
void _updateState({int? filesToMigrate}) {
|
||||
_currentState = _currentState.copyWith(filesToMigrate: filesToMigrate);
|
||||
_notifyState();
|
||||
}
|
||||
|
||||
void _notifyState() {
|
||||
if (!_stateController.isClosed) {
|
||||
_stateController.add(_currentState);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
await AppEnvironment.init();
|
||||
final isInitialized = await initBackgroundExecution();
|
||||
await setupPushNotification();
|
||||
Log.info('Handling a background message: ${message.messageId}');
|
||||
await FcmNotificationService.handleRemoteMessage(message);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (isInitialized) {
|
||||
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
|
||||
}
|
||||
} else {
|
||||
// make sure every thing run...
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
// ignore_for_file: unreachable_from_main
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_app_installations/firebase_app_installations.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.background.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -17,62 +19,9 @@ import '../../../firebase_options.dart';
|
|||
|
||||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||
|
||||
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 {
|
||||
Future<void> checkForTokenUpdates() async {
|
||||
try {
|
||||
if (!userService.isUserCreated) {
|
||||
Log.info(
|
||||
'Checking for FCM token updates skipped: user is not yet created.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!userService.isUserCreated) return;
|
||||
if (Platform.isIOS) {
|
||||
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
for (var i = 0; i < 20; i++) {
|
||||
|
|
@ -102,38 +51,15 @@ class FcmNotificationService {
|
|||
..updateFCMToken = true
|
||||
..fcmToken = fcmToken;
|
||||
});
|
||||
if (apiService.isAuthenticated) {
|
||||
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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
// ignore: avoid_types_on_closure_parameters
|
||||
.listen((String fcmToken) async {
|
||||
.listen((fcmToken) async {
|
||||
await UserService.update((u) {
|
||||
u
|
||||
..updateFCMToken = true
|
||||
..fcmToken = fcmToken;
|
||||
});
|
||||
if (apiService.isAuthenticated) {
|
||||
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!');
|
||||
}
|
||||
}
|
||||
})
|
||||
.onError((err) {
|
||||
Log.error('could not listen on token refresh');
|
||||
|
|
@ -143,8 +69,70 @@ class FcmNotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||
await _updateLastFcmMessageTimestamp();
|
||||
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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Future<void> initFCMService() async {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
|
||||
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 {
|
||||
// make sure every thing run...
|
||||
await Future.delayed(const Duration(milliseconds: 2000));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||
if (!Platform.isAndroid) {
|
||||
Log.error('Got message in Dart while on iOS');
|
||||
}
|
||||
|
|
@ -162,108 +150,10 @@ class FcmNotificationService {
|
|||
message.notification?.body ?? message.data['body'] as String? ?? '';
|
||||
await customLocalPushNotification(title, body);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _updateLastFcmMessageTimestamp() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
try {
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.lastFcmMessageTimestamp,
|
||||
value: nowMs,
|
||||
iOptions: const IOSOptions(
|
||||
groupId: 'CN332ZUGRP.eu.twonly.shared',
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
Log.info('Updated last FCM message timestamp to $nowMs');
|
||||
} catch (e) {
|
||||
Log.error('Could not write last FCM message timestamp: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> updateLastServerMessageTimestamp() async {
|
||||
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');
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// else if (message.data['push_data'] != null) {
|
||||
// await handlePushData(message.data['push_data'] as String);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Future<void> setupNotificationWithUsers({
|
|||
|
||||
// HotFIX: Search for user with id 0 if not there remove all
|
||||
// and create new push keys with all users.
|
||||
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId.toInt() == 0);
|
||||
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0);
|
||||
if (pushUser == null) {
|
||||
Log.info('Clearing push keys');
|
||||
await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
|
||||
|
|
@ -51,7 +51,7 @@ Future<void> setupNotificationWithUsers({
|
|||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
for (final contact in contacts) {
|
||||
final pushUser = pushUsers.firstWhereOrNull(
|
||||
(x) => x.userId.toInt() == contact.userId,
|
||||
(x) => x.userId == contact.userId,
|
||||
);
|
||||
|
||||
if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
|
||||
|
|
@ -124,9 +124,7 @@ Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
|
|||
Future<void> updatePushUser(Contact contact) async {
|
||||
final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
final pushUser = pushKeys.firstWhereOrNull(
|
||||
(x) => x.userId.toInt() == contact.userId,
|
||||
);
|
||||
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
|
||||
|
||||
if (pushUser == null) {
|
||||
pushKeys.add(
|
||||
|
|
@ -150,9 +148,7 @@ Future<void> updatePushUser(Contact contact) async {
|
|||
Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
|
||||
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
|
||||
|
||||
var pushUser = pushKeys.firstWhereOrNull(
|
||||
(x) => x.userId.toInt() == fromUserId,
|
||||
);
|
||||
var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
|
||||
if (pushUser == null) {
|
||||
final contact = await twonlyDB.contactsDao
|
||||
|
|
@ -168,7 +164,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
|
|||
lastMessageId: uuid.v7(),
|
||||
),
|
||||
);
|
||||
pushUser = pushKeys.firstWhereOrNull((x) => x.userId.toInt() == fromUserId);
|
||||
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
}
|
||||
|
||||
if (pushUser == null) {
|
||||
|
|
@ -191,9 +187,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
|
|||
Future<void> updateLastMessageId(int fromUserId, String messageId) async {
|
||||
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
final pushUser = pushUsers.firstWhereOrNull(
|
||||
(x) => x.userId.toInt() == fromUserId,
|
||||
);
|
||||
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
if (pushUser == null) {
|
||||
unawaited(setupNotificationWithUsers());
|
||||
return;
|
||||
|
|
@ -291,7 +285,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
|||
|
||||
if (content.hasMediaUpdate()) {
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageById(content.mediaUpdate.targetMessageId)
|
||||
.getMessageById(content.reaction.targetMessageId)
|
||||
.getSingleOrNull();
|
||||
// These notifications should only be send to the original sender.
|
||||
if (msg == null || msg.senderId != toUserId) {
|
||||
|
|
@ -310,9 +304,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
|||
if (content.hasGroupCreate()) {
|
||||
kind = PushKind.ADDED_TO_GROUP;
|
||||
final group = await twonlyDB.groupsDao.getGroup(content.groupId);
|
||||
if (group != null) {
|
||||
additionalContent = group.groupName;
|
||||
}
|
||||
additionalContent = group!.groupName;
|
||||
}
|
||||
|
||||
if (kind == null) return null;
|
||||
|
|
@ -347,9 +339,7 @@ Future<Uint8List?> encryptPushNotification(
|
|||
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
|
||||
var keyId = 0;
|
||||
|
||||
final pushUser = pushKeys.firstWhereOrNull(
|
||||
(x) => x.userId.toInt() == toUserId,
|
||||
);
|
||||
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
|
||||
|
||||
if (pushUser == null) {
|
||||
// user does not have send any push keys
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
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
Loading…
Reference in a new issue