Compare commits

..

36 commits
v0.1.1 ... main

Author SHA1 Message Date
ef6ebf6d65
Merge pull request #397 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Fix: Reupload of media files was not working properly
- Fix: Chats were sometimes ordered wrongly  
- Fix: Typing indicator was not always shown
- Fix: Multiple smaller issues
2026-04-12 02:31:02 +02:00
2271453d54 bump version 2026-04-12 02:30:33 +02:00
c42ff41eb8 fix notification tap initial page view
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-04-12 02:22:30 +02:00
cfc6e945da fix: reupload of media files
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-12 02:01:31 +02:00
766d482baa Merge remote-tracking branch 'origin/main' into dev
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-11 22:50:51 +02:00
0d975db3e4 fmt 2026-04-11 00:09:14 +02:00
cc3a7b8b64 fix wrong increase of last message
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-10 21:21:54 +02:00
0669f7523a
Merge pull request #396 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- New: Typing and chat open indicator
- New: Screen lock for twonly (Can be enabled in the settings.)
- Improve: Visual indication when connected to the server 
- Improve: Several minor issues with the user interface
- Fix: Poor audio quality and edge distortions in videos sent from Android
2026-04-10 19:26:18 +02:00
6154d7b48c bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-10 19:25:44 +02:00
840dfed950 fixes android video compression issues 2026-04-10 19:15:51 +02:00
6aab76a47e update strings
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-10 18:19:12 +02:00
a73b2737e7 typing indicator 2026-04-10 18:18:24 +02:00
727949c3d9 update strings
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-10 16:34:32 +02:00
391646d243 new: screen lock 2026-04-10 16:34:16 +02:00
f419b3709d smoother response animation
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-10 15:30:44 +02:00
527bf51bff improved websocket connection state info
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-09 22:51:36 +02:00
587740f306 add option to reopen images with context menu
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-09 22:20:15 +02:00
19a53a879b fix check mark shown in send image 2026-04-09 22:20:01 +02:00
1650642cb2 fix ui glitch 2026-04-09 22:19:44 +02:00
1d4a0bdbeb add qr code button 2026-04-09 22:19:36 +02:00
1b7ec19769
Merge pull request #395 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- New: Video stabilization
- New: Crop or rotate images before sharing them.
- New: Clicking on “Text Notifications” will now open the chat directly (Android only)
- New: Developer settings to reduce flames
- Improve: Improved troubleshooting for issues with push notifications 
- Improve: A message appears if someone has deleted their account.
- Improve: Make the verification badge more visible.
- Fix: Flash not activated when starting a video recording
- Fix: Problem sending media when a recipient has deleted their account.
- Fix: Receive push notifications without receiving an in-app message (Android)
- Fix: Issue with sending GIFs from Memories
- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze
2026-04-06 15:47:44 +02:00
aa7a065572 fix test
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-04-06 15:42:41 +02:00
083faaa876 fixes multiple issues 2026-04-06 15:36:01 +02:00
48bfc774c2 show string
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-06 14:24:31 +02:00
3dcefcbed1 make verification badge more visible 2026-04-06 14:24:19 +02:00
b7e6cbfc2f crop images and small improvements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-06 13:35:36 +02:00
8810ecf360 lower lastExecutionInSecondsLimit
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-06 01:53:42 +02:00
fdb11d1a9b bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-06 00:32:25 +02:00
02ae72f679 improve chat list view 2026-04-06 00:24:17 +02:00
267e2bd376 opening chat if clicked on the notification 2026-04-06 00:02:55 +02:00
aa26766bdf fixes duplicated messages from the server
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-05 22:17:45 +02:00
2d5f004222 improve log cleaning 2026-04-05 22:06:08 +02:00
6cc2ad3a65 fix logging issue for background execution 2026-04-05 21:50:31 +02:00
337b9567c6 fix issue with deleted accounts
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-04-05 16:31:25 +02:00
955992d5d2 add troubleshooting for notifications & video stab 2026-04-05 16:22:48 +02:00
c48ce31bc6 add view for reduce flames 2026-04-05 16:21:58 +02:00
193 changed files with 14245 additions and 1958 deletions

View file

@ -1,5 +1,35 @@
# Changelog # Changelog
## 0.1.5
- Fix: Reupload of media files was not working properly
- Fix: Chats were sometimes ordered wrongly
- Fix: Typing indicator was not always shown
- Fix: Multiple smaller issues
## 0.1.4
- New: Typing and chat open indicator
- New: Screen lock for twonly (Can be enabled in the settings.)
- Improve: Visual indication when connected to the server
- Improve: Several minor issues with the user interface
- Fix: Poor audio quality and edge distortions in videos sent from Android
## 0.1.3
- New: Video stabilization
- New: Crop or rotate images before sharing them.
- New: Clicking on “Text Notifications” will now open the chat directly (Android only)
- New: Developer settings to reduce flames
- Improve: Improved troubleshooting for issues with push notifications
- Improve: A message appears if someone has deleted their account.
- Improve: Make the verification badge more visible.
- Fix: Flash not activated when starting a video recording
- Fix: Problem sending media when a recipient has deleted their account.
- Fix: Receive push notifications without receiving an in-app message (Android)
- Fix: Issue with sending GIFs from Memories
- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze
## 0.1.1 ## 0.1.1
- New: Groups can now collect flames as well - New: Groups can now collect flames as well

View file

@ -6,8 +6,8 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
import com.otaliastudios.transcoder.strategy.TrackStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -18,11 +18,6 @@ object VideoCompressionChannel {
// Compression parameters defined natively (as requested) // Compression parameters defined natively (as requested)
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
// Audio parameters defined natively
private const val AUDIO_BITRATE = 128_000L // 128 kbps
private const val AUDIO_SAMPLE_RATE = 44_100
private const val AUDIO_CHANNELS = 2
fun configure(flutterEngine: FlutterEngine, context: Context) { fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
@ -54,6 +49,14 @@ object VideoCompressionChannel {
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy) val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
if (method.name == "createOutputFormat" && result is MediaFormat) { if (method.name == "createOutputFormat" && result is MediaFormat) {
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC) result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
if (result.containsKey(MediaFormat.KEY_WIDTH) && result.containsKey(MediaFormat.KEY_HEIGHT)) {
val width = result.getInteger(MediaFormat.KEY_WIDTH)
val height = result.getInteger(MediaFormat.KEY_HEIGHT)
// Align dimensions to a multiple of 16 to prevent edge artifacts (green lines/distortions)
result.setInteger(MediaFormat.KEY_WIDTH, width - (width % 16))
result.setInteger(MediaFormat.KEY_HEIGHT, height - (height % 16))
}
} }
result result
} as TrackStrategy } as TrackStrategy
@ -61,13 +64,7 @@ object VideoCompressionChannel {
Transcoder.into(outputPath) Transcoder.into(outputPath)
.addDataSource(inputPath) .addDataSource(inputPath)
.setVideoTrackStrategy(hevcStrategy) .setVideoTrackStrategy(hevcStrategy)
.setAudioTrackStrategy( .setAudioTrackStrategy(PassThroughTrackStrategy())
DefaultAudioStrategy.builder()
.channels(AUDIO_CHANNELS)
.sampleRate(AUDIO_SAMPLE_RATE)
.bitRate(AUDIO_BITRATE)
.build()
)
.setListener(object : TranscoderListener { .setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) { override fun onTranscodeProgress(progress: Double) {
mainHandler.post { mainHandler.post {

View file

@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check" viewBox="0 0 16 16"> <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 fill-rule="evenodd" d="M10.354 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708 0"/> <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.01zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708"/>
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 604 B

View file

@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff0000" class="bi bi-patch-check" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ff0000" class="bi bi-patch-exclamation-fill" viewBox="0 0 16 16">
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/> <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 4c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995A.905.905 0 0 1 8 4m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 629 B

View file

@ -8,7 +8,11 @@
// For information on using the generated types, please see the documentation: // For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/ // https://github.com/apple/swift-protobuf/
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation import Foundation
#endif
import SwiftProtobuf import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file // If the compiler emits an error on this type, it is because this file

View file

@ -56,13 +56,20 @@ PODS:
- FirebaseAnalytics (~> 12.9.0) - FirebaseAnalytics (~> 12.9.0)
- Firebase/CoreOnly (12.9.0): - Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0) - FirebaseCore (~> 12.9.0)
- Firebase/Installations (12.9.0):
- Firebase/CoreOnly
- FirebaseInstallations (~> 12.9.0)
- Firebase/Messaging (12.9.0): - Firebase/Messaging (12.9.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 12.9.0) - FirebaseMessaging (~> 12.9.0)
- firebase_core (4.5.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) - Firebase/CoreOnly (= 12.9.0)
- Flutter - Flutter
- firebase_messaging (16.1.2): - firebase_messaging (16.1.3):
- Firebase/Messaging (= 12.9.0) - Firebase/Messaging (= 12.9.0)
- firebase_core - firebase_core
- Flutter - Flutter
@ -278,17 +285,17 @@ PODS:
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- restart_app (1.7.3): - restart_app (1.7.3):
- Flutter - Flutter
- SDWebImage (5.21.6): - SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.6) - SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.6) - SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0): - SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2) - Sentry/HybridSDK (8.58.0)
- sentry_flutter (9.14.0): - sentry_flutter (9.16.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.56.2) - Sentry/HybridSDK (= 8.58.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -297,32 +304,32 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.51.1): - sqlite3 (3.52.0):
- sqlite3/common (= 3.51.1) - sqlite3/common (= 3.52.0)
- sqlite3/common (3.51.1) - sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.51.1): - sqlite3/dbstatvtab (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.51.1): - sqlite3/fts5 (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.51.1): - sqlite3/math (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.51.1): - sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.51.1): - sqlite3/rtree (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.51.1): - sqlite3/session (3.52.0):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.51.1) - sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
- sqlite3/perf-threadsafe - sqlite3/perf-threadsafe
- sqlite3/rtree - sqlite3/rtree
- sqlite3/session - sqlite3/session
- SwiftProtobuf (1.34.1) - SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
@ -343,6 +350,7 @@ DEPENDENCIES:
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase - Firebase
- firebase_app_installations (from `.symlinks/plugins/firebase_app_installations/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- FirebaseCore - FirebaseCore
@ -430,6 +438,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/emoji_picker_flutter/ios" :path: ".symlinks/plugins/emoji_picker_flutter/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
firebase_app_installations:
:path: ".symlinks/plugins/firebase_app_installations/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging: firebase_messaging:
@ -493,7 +503,7 @@ SPEC CHECKSUMS:
app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8 app_links: a754cbec3c255bd4bbb4d236ecc06f28cd9a7ce8
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
@ -502,8 +512,9 @@ SPEC CHECKSUMS:
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56 Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_core: afac1aac13c931e0401c7e74ed1276112030efab firebase_app_installations: 1abd8d071ea2022d7888f7a9713710c37136ff91
firebase_messaging: 7cb2727feb789751fc6936bcc8e08408970e2820 firebase_core: 8e6f58412ca227827c366b92e7cee047a2148c60
firebase_messaging: c3aa897e0d40109cfb7927c40dc0dea799863f3b
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352 FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8 FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72 FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
@ -544,16 +555,16 @@ SPEC CHECKSUMS:
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2 restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
sentry_flutter: 841fa2fe08dc72eb95e2320b76e3f751f3400cf5 sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41 sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart'; import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -36,9 +37,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required isConnected}) async { globalCallbackConnectionState = ({required isConnected}) async {
await context await context.read<CustomChangeProvider>().updateConnectionState(
.read<CustomChangeProvider>() isConnected,
.updateConnectionState(isConnected); );
await setUserPlan(); await setUserPlan();
}; };
@ -134,6 +135,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = false; bool _skipBackup = false;
bool _isTwonlyLocked = true;
int _initialPage = 0; int _initialPage = 0;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@ -149,6 +151,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isUserCreated = await isUserCreated(); _isUserCreated = await isUserCreated();
if (_isUserCreated) { if (_isUserCreated) {
if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point
_isTwonlyLocked = gUser.screenLockEnabled;
}
if (gUser.appVersion < 62) { if (gUser.appVersion < 62) {
_showDatabaseMigration = true; _showDatabaseMigration = true;
} }
@ -164,8 +170,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (proof != null) { if (proof != null) {
Log.info('Starting with proof of work calculation.'); Log.info('Starting with proof of work calculation.');
// Starting with the proof of work. // Starting with the proof of work.
_proofOfWork = _proofOfWork = (
(calculatePoW(proof.prefix, proof.difficulty.toInt()), false); calculatePoW(proof.prefix, proof.difficulty.toInt()),
false,
);
} else { } else {
_proofOfWork = (null, disabled); _proofOfWork = (null, disabled);
} }
@ -187,7 +195,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) { if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.')); child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) { } else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup) { if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () { callBack: () {
_skipBackup = true; _skipBackup = true;

View file

@ -65,5 +65,4 @@ class DefaultFirebaseOptions {
storageBucket: 'twonly-ff605.firebasestorage.app', storageBucket: 'twonly-ff605.firebasestorage.app',
iosBundleId: 'eu.twonly', iosBundleId: 'eu.twonly',
); );
} }

View file

@ -21,7 +21,8 @@ late UserData gUser;
// App widget. // App widget.
// This callback called by the apiProvider // This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState = ({ void Function({required bool isConnected}) globalCallbackConnectionState =
({
required isConnected, required isConnected,
}) {}; }) {};
void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackAppIsOutdated = () {};

View file

@ -6,7 +6,8 @@ class Routes {
static const String chatsStartNewChat = '/chats/start_new_chat'; static const String chatsStartNewChat = '/chats/start_new_chat';
static const String chatsCameraSendTo = '/chats/camera_send_to'; static const String chatsCameraSendTo = '/chats/camera_send_to';
static const String chatsMediaViewer = '/chats/media_viewer'; static const String chatsMediaViewer = '/chats/media_viewer';
static const String chatsMessages = '/chats/messages';
static String chatsMessages(String groupId) => '/chats/messages/$groupId';
static String groupCreateSelectMember(String? groupId) => static String groupCreateSelectMember(String? groupId) =>
'/group/create/select_member${groupId == null ? '' : '/$groupId'}'; '/group/create/select_member${groupId == null ? '' : '/$groupId'}';
@ -53,5 +54,7 @@ class Routes {
'/settings/developer/retransmission_database'; '/settings/developer/retransmission_database';
static const String settingsDeveloperAutomatedTesting = static const String settingsDeveloperAutomatedTesting =
'/settings/developer/automated_testing'; '/settings/developer/automated_testing';
static const String settingsDeveloperReduceFlames =
'/settings/developer/reduce_flames';
static const String settingsInvite = '/settings/invite'; static const String settingsInvite = '/settings/invite';
} }

View file

@ -37,16 +37,16 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Future<Contact?> getContactById(int userId) async { Future<Contact?> getContactById(int userId) async {
return (select(contacts)..where((t) => t.userId.equals(userId))) return (select(
.getSingleOrNull(); contacts,
)..where((t) => t.userId.equals(userId))).getSingleOrNull();
} }
Future<List<Contact>> getContactsByUsername( Future<List<Contact>> getContactsByUsername(
String username, { String username, {
String username2 = '_______', String username2 = '_______',
}) async { }) async {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => t.username.equals(username) | t.username.equals(username2), (t) => t.username.equals(username) | t.username.equals(username2),
)) ))
.get(); .get();
@ -60,8 +60,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
int userId, int userId,
ContactsCompanion updatedValues, ContactsCompanion updatedValues,
) async { ) async {
await (update(contacts)..where((c) => c.userId.equals(userId))) await (update(
.write(updatedValues); contacts,
)..where((c) => c.userId.equals(userId))).write(updatedValues);
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
updatedValues.displayName.present || updatedValues.displayName.present ||
updatedValues.nickName.present || updatedValues.nickName.present ||
@ -83,8 +84,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<List<Contact>> watchNotAcceptedContacts() { Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => (t) =>
t.accepted.equals(false) & t.accepted.equals(false) &
t.blocked.equals(false) & t.blocked.equals(false) &
@ -94,8 +94,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<Contact?> watchContact(int userid) { Stream<Contact?> watchContact(int userid) {
return (select(contacts)..where((t) => t.userId.equals(userid))) return (select(
.watchSingleOrNull(); contacts,
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
} }
Future<List<Contact>> getAllContacts() { Future<List<Contact>> getAllContacts() {
@ -124,8 +125,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<List<Contact>> watchAllAcceptedContacts() { Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => (t) =>
t.blocked.equals(false) & t.blocked.equals(false) &
t.accepted.equals(true) & t.accepted.equals(true) &

View file

@ -14,7 +14,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
// ignore: matching_super_parameters // ignore: matching_super_parameters
MediaFilesDao(super.db); MediaFilesDao(super.db);
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async { Future<MediaFile?> insertOrUpdateMedia(MediaFilesCompanion mediaFile) async {
try { try {
var insertMediaFile = mediaFile; var insertMediaFile = mediaFile;
@ -24,7 +24,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
); );
} }
final rowId = await into(mediaFiles).insert(insertMediaFile); final rowId = await into(
mediaFiles,
).insertOnConflictUpdate(insertMediaFile);
return await (select( return await (select(
mediaFiles, mediaFiles,

View file

@ -318,7 +318,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
} }
final rowId = await into(messages).insert(insertMessage); final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
message.groupId.value, message.groupId.value,

View file

@ -26,14 +26,14 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
Log.error('Did not update reaction as it is not an emoji!'); Log.error('Did not update reaction as it is not an emoji!');
return; return;
} }
final msg = final msg = await twonlyDB.messagesDao
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); .getMessageById(messageId)
.getSingleOrNull();
if (msg == null || msg.groupId != groupId) return; if (msg == null || msg.groupId != groupId) return;
try { try {
if (remove) { if (remove) {
await (delete(reactions) await (delete(reactions)..where(
..where(
(t) => (t) =>
t.senderId.equals(contactId) & t.senderId.equals(contactId) &
t.messageId.equals(messageId) & t.messageId.equals(messageId) &
@ -63,13 +63,13 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
Log.error('Did not update reaction as it is not an emoji!'); Log.error('Did not update reaction as it is not an emoji!');
return; return;
} }
final msg = final msg = await twonlyDB.messagesDao
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); .getMessageById(messageId)
.getSingleOrNull();
if (msg == null) return; if (msg == null) return;
try { try {
await (delete(reactions) await (delete(reactions)..where(
..where(
(t) => (t) =>
t.senderId.isNull() & t.senderId.isNull() &
t.messageId.equals(messageId) & t.messageId.equals(messageId) &
@ -98,9 +98,8 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
} }
Stream<Reaction?> watchLastReactions(String groupId) { Stream<Reaction?> watchLastReactions(String groupId) {
final query = (select(reactions) final query =
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) (select(reactions)).join(
.join(
[ [
innerJoin( innerJoin(
messages, messages,
@ -110,7 +109,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
], ],
) )
..where(messages.groupId.equals(groupId)) ..where(messages.groupId.equals(groupId))
// ..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) ..orderBy([OrderingTerm.desc(messages.createdAt)])
..limit(1); ..limit(1);
return query.map((row) => row.readTable(reactions)).watchSingleOrNull(); return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
} }

View file

@ -31,7 +31,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
if (receipt == null) return; if (receipt == null) return;
if (receipt.messageId != null) { if (receipt.messageId != null) {
await into(messageActions).insert( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(receipt.messageId!), messageId: Value(receipt.messageId!),
contactId: Value(fromUserId), contactId: Value(fromUserId),
@ -54,6 +54,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.go(); .go();
} }
Future<void> deleteReceiptForUser(int contactId) async {
await (delete(receipts)..where(
(t) => t.contactId.equals(contactId),
))
.go();
}
Future<void> purgeReceivedReceipts() async { Future<void> purgeReceivedReceipts() async {
await (delete(receivedReceipts)..where( await (delete(receivedReceipts)..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
@ -106,6 +113,16 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
} }
} }
Future<List<Receipt>> getReceiptsByContactAndMessageId(
int contactId,
String messageId,
) async {
return (select(receipts)..where(
(t) => t.contactId.equals(contactId) & t.messageId.equals(messageId),
))
.get();
}
Future<List<Receipt>> getReceiptsForRetransmission() async { Future<List<Receipt>> getReceiptsForRetransmission() async {
final markedRetriesTime = clock.now().subtract( final markedRetriesTime = clock.now().subtract(
const Duration( const Duration(
@ -125,10 +142,38 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.get(); .get();
} }
Future<List<Receipt>> getReceiptsForMediaRetransmissions() async {
final markedRetriesTime = clock.now().subtract(
const Duration(
// give the server time to transmit all messages to the client
seconds: 20,
),
);
return (select(receipts)..where(
(t) =>
(t.markForRetry.isSmallerThanValue(markedRetriesTime) |
t.markForRetryAfterAccepted.isSmallerThanValue(
markedRetriesTime,
)) &
t.willBeRetriedByMediaUpload.equals(true),
))
.get();
}
Stream<List<Receipt>> watchAll() { Stream<List<Receipt>> watchAll() {
return select(receipts).watch(); return select(receipts).watch();
} }
Future<int> getReceiptCountForContact(int contactId) {
final countExp = countAll();
final query = selectOnly(receipts)
..addColumns([countExp])
..where(receipts.contactId.equals(contactId));
return query.map((row) => row.read(countExp)!).getSingle();
}
Future<void> updateReceipt( Future<void> updateReceipt(
String receiptId, String receiptId,
ReceiptsCompanion updates, ReceiptsCompanion updates,
@ -138,6 +183,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)..where((c) => c.receiptId.equals(receiptId))).write(updates); )..where((c) => c.receiptId.equals(receiptId))).write(updates);
} }
Future<void> updateReceiptByContactAndMessageId(
int contactId,
String messageId,
ReceiptsCompanion updates,
) async {
await (update(
receipts,
)..where(
(c) => c.contactId.equals(contactId) & c.messageId.equals(messageId),
))
.write(updates);
}
Future<void> updateReceiptWidthUserId( Future<void> updateReceiptWidthUserId(
int fromUserId, int fromUserId,
String receiptId, String receiptId,
@ -151,9 +209,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> markMessagesForRetry(int contactId) async { Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where( await (update(receipts)..where(
(c) => (c) => c.contactId.equals(contactId) & c.markForRetry.isNull(),
c.contactId.equals(contactId) &
c.willBeRetriedByMediaUpload.equals(false),
)) ))
.write( .write(
ReceiptsCompanion( ReceiptsCompanion(

View file

@ -19,9 +19,13 @@ class SignalDaoManager {
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$SignalContactPreKeysTableTableManager get signalContactPreKeys => $$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
$$SignalContactPreKeysTableTableManager( $$SignalContactPreKeysTableTableManager(
_db.attachedDatabase, _db.signalContactPreKeys); _db.attachedDatabase,
_db.signalContactPreKeys,
);
$$SignalContactSignedPreKeysTableTableManager $$SignalContactSignedPreKeysTableTableManager
get signalContactSignedPreKeys => get signalContactSignedPreKeys =>
$$SignalContactSignedPreKeysTableTableManager( $$SignalContactSignedPreKeysTableTableManager(
_db.attachedDatabase, _db.signalContactSignedPreKeys); _db.attachedDatabase,
_db.signalContactSignedPreKeys,
);
} }

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override @override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async { Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores) final identity =
..where( await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
(t) => (t) =>
t.deviceId.equals(address.getDeviceId()) & t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()), t.name.equals(address.getName()),
@ -40,8 +40,10 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false; return false;
} }
return trusted == null || return trusted == null ||
const ListEquality<dynamic>() const ListEquality<dynamic>().equals(
.equals(trusted.serialize(), identityKey.serialize()); trusted.serialize(),
identityKey.serialize(),
);
} }
@override @override
@ -53,7 +55,9 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false; return false;
} }
if (await getIdentity(address) == null) { if (await getIdentity(address) == null) {
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( await twonlyDB
.into(twonlyDB.signalIdentityKeyStores)
.insert(
SignalIdentityKeyStoresCompanion( SignalIdentityKeyStoresCompanion(
deviceId: Value(address.getDeviceId()), deviceId: Value(address.getDeviceId()),
name: Value(address.getName()), name: Value(address.getName()),
@ -61,8 +65,7 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
), ),
); );
} else { } else {
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores) await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
..where(
(t) => (t) =>
t.deviceId.equals(address.getDeviceId()) & t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()), t.name.equals(address.getName()),

View file

@ -7,17 +7,17 @@ import 'package:twonly/src/utils/log.dart';
class ConnectPreKeyStore extends PreKeyStore { class ConnectPreKeyStore extends PreKeyStore {
@override @override
Future<bool> containsPreKey(int preKeyId) async { Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores) final preKeyRecord = await (twonlyDB.select(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.get(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
return preKeyRecord.isNotEmpty; return preKeyRecord.isNotEmpty;
} }
@override @override
Future<PreKeyRecord> loadPreKey(int preKeyId) async { Future<PreKeyRecord> loadPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores) final preKeyRecord = await (twonlyDB.select(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.get(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
if (preKeyRecord.isEmpty) { if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException( throw InvalidKeyIdException(
'[PREKEY] No such preKey record!', '[PREKEY] No such preKey record!',
@ -29,9 +29,9 @@ class ConnectPreKeyStore extends PreKeyStore {
@override @override
Future<void> removePreKey(int preKeyId) async { Future<void> removePreKey(int preKeyId) async {
await (twonlyDB.delete(twonlyDB.signalPreKeyStores) await (twonlyDB.delete(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.go(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
} }
@override @override

View file

@ -6,7 +6,8 @@ import 'package:twonly/src/database/twonly.db.dart';
class ConnectSenderKeyStore extends SenderKeyStore { class ConnectSenderKeyStore extends SenderKeyStore {
@override @override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async { Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity = await (twonlyDB.select(twonlyDB.signalSenderKeyStores) final identity =
await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
..where((t) => t.senderKeyName.equals(senderKeyName.serialize()))) ..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
.getSingleOrNull(); .getSingleOrNull();
if (identity == null) { if (identity == null) {
@ -22,7 +23,9 @@ class ConnectSenderKeyStore extends SenderKeyStore {
SenderKeyName senderKeyName, SenderKeyName senderKeyName,
SenderKeyRecord record, SenderKeyRecord record,
) async { ) async {
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( await twonlyDB
.into(twonlyDB.signalSenderKeyStores)
.insert(
SignalSenderKeyStoresCompanion( SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()), senderKey: Value(record.serialize()),
senderKeyName: Value(senderKeyName.serialize()), senderKeyName: Value(senderKeyName.serialize()),

View file

@ -6,8 +6,8 @@ import 'package:twonly/src/database/twonly.db.dart';
class ConnectSessionStore extends SessionStore { class ConnectSessionStore extends SessionStore {
@override @override
Future<bool> containsSession(SignalProtocolAddress address) async { Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores) final sessions =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -18,15 +18,14 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<void> deleteAllSessions(String name) async { Future<void> deleteAllSessions(String name) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores) await (twonlyDB.delete(
..where((tbl) => tbl.name.equals(name))) twonlyDB.signalSessionStores,
.go(); )..where((tbl) => tbl.name.equals(name))).go();
} }
@override @override
Future<void> deleteSession(SignalProtocolAddress address) async { Future<void> deleteSession(SignalProtocolAddress address) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores) await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -36,8 +35,8 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<List<int>> getSubDeviceSessions(String name) async { Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores) final deviceIds =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name), (tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
)) ))
.get(); .get();
@ -46,8 +45,8 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async { Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores) final dbSession =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -77,8 +76,7 @@ class ConnectSessionStore extends SessionStore {
.into(twonlyDB.signalSessionStores) .into(twonlyDB.signalSessionStores)
.insert(sessionCompanion); .insert(sessionCompanion);
} else { } else {
await (twonlyDB.update(twonlyDB.signalSessionStores) await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),

View file

@ -9,8 +9,10 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
IdentityKeyPair identityKeyPair, IdentityKeyPair identityKeyPair,
int registrationId, int registrationId,
) { ) {
_identityKeyStore = _identityKeyStore = ConnectIdentityKeyStore(
ConnectIdentityKeyStore(identityKeyPair, registrationId); identityKeyPair,
registrationId,
);
} }
final preKeyStore = ConnectPreKeyStore(); final preKeyStore = ConnectPreKeyStore();
@ -31,8 +33,7 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
Future<bool> saveIdentity( Future<bool> saveIdentity(
SignalProtocolAddress address, SignalProtocolAddress address,
IdentityKey? identityKey, IdentityKey? identityKey,
) async => ) async => _identityKeyStore.saveIdentity(address, identityKey);
_identityKeyStore.saveIdentity(address, identityKey);
@override @override
Future<bool> isTrustedIdentity( Future<bool> isTrustedIdentity(

View file

@ -30,8 +30,9 @@ class Groups extends Table {
BoolColumn get alsoBestFriend => BoolColumn get alsoBestFriend =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterMilliseconds => integer() IntColumn get deleteMessagesAfterMilliseconds => integer().withDefault(
.withDefault(const Constant(defaultDeleteMessagesAfterMilliseconds))(); const Constant(defaultDeleteMessagesAfterMilliseconds),
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@ -63,6 +64,9 @@ class GroupMembers extends Table {
TextColumn get memberState => textEnum<MemberState>().nullable()(); TextColumn get memberState => textEnum<MemberState>().nullable()();
BlobColumn get groupPublicKey => blob().nullable()(); BlobColumn get groupPublicKey => blob().nullable()();
DateTimeColumn get lastChatOpened => dateTime().nullable()();
DateTimeColumn get lastTypeIndicator => dateTime().nullable()();
DateTimeColumn get lastMessage => dateTime().nullable()(); DateTimeColumn get lastMessage => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -33,7 +33,7 @@ enum DownloadState {
downloading, downloading,
downloaded, downloaded,
ready, ready,
reuploadRequested reuploadRequested,
} }
@DataClassName('MediaFile') @DataClassName('MediaFile')

View file

@ -18,9 +18,11 @@ class Messages extends Table {
TextColumn get type => text()(); TextColumn get type => text()();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();
TextColumn get mediaId => text() TextColumn get mediaId => text().nullable().references(
.nullable() MediaFiles,
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)(); #mediaId,
onDelete: KeyAction.setNull,
)();
BlobColumn get additionalMessageData => blob().nullable()(); BlobColumn get additionalMessageData => blob().nullable()();
@ -75,9 +77,11 @@ class MessageHistories extends Table {
TextColumn get messageId => TextColumn get messageId =>
text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
IntColumn get contactId => integer() IntColumn get contactId => integer().nullable().references(
.nullable() Contacts,
.references(Contacts, #userId, onDelete: KeyAction.cascade)(); #userId,
onDelete: KeyAction.cascade,
)();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();

View file

@ -10,9 +10,11 @@ class Reactions extends Table {
TextColumn get emoji => text()(); TextColumn get emoji => text()();
// in case senderId is null, it was send by user itself // in case senderId is null, it was send by user itself
IntColumn get senderId => integer() IntColumn get senderId => integer().nullable().references(
.nullable() Contacts,
.references(Contacts, #userId, onDelete: KeyAction.cascade)(); #userId,
onDelete: KeyAction.cascade,
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -10,9 +10,11 @@ class Receipts extends Table {
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
// in case a message is deleted, it should be also deleted from the receipts table // in case a message is deleted, it should be also deleted from the receipts table
TextColumn get messageId => text() TextColumn get messageId => text().nullable().references(
.nullable() Messages,
.references(Messages, #messageId, onDelete: KeyAction.cascade)(); #messageId,
onDelete: KeyAction.cascade,
)();
/// This is the protobuf 'Message' /// This is the protobuf 'Message'
BlobColumn get message => blob()(); BlobColumn get message => blob()();

View file

@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 10; int get schemaVersion => 11;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -143,6 +143,16 @@ class TwonlyDB extends _$TwonlyDB {
schema.receipts.willBeRetriedByMediaUpload, schema.receipts.willBeRetriedByMediaUpload,
); );
}, },
from10To11: (m, schema) async {
await m.addColumn(
schema.groupMembers,
schema.groupMembers.lastChatOpened,
);
await m.addColumn(
schema.groupMembers,
schema.groupMembers.lastTypeIndicator,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

View file

@ -5198,6 +5198,30 @@ class $GroupMembersTable extends GroupMembers
type: DriftSqlType.blob, type: DriftSqlType.blob,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _lastChatOpenedMeta = const VerificationMeta(
'lastChatOpened',
);
@override
late final GeneratedColumn<DateTime> lastChatOpened =
GeneratedColumn<DateTime>(
'last_chat_opened',
aliasedName,
true,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const VerificationMeta _lastTypeIndicatorMeta = const VerificationMeta(
'lastTypeIndicator',
);
@override
late final GeneratedColumn<DateTime> lastTypeIndicator =
GeneratedColumn<DateTime>(
'last_type_indicator',
aliasedName,
true,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const VerificationMeta _lastMessageMeta = const VerificationMeta( static const VerificationMeta _lastMessageMeta = const VerificationMeta(
'lastMessage', 'lastMessage',
); );
@ -5227,6 +5251,8 @@ class $GroupMembersTable extends GroupMembers
contactId, contactId,
memberState, memberState,
groupPublicKey, groupPublicKey,
lastChatOpened,
lastTypeIndicator,
lastMessage, lastMessage,
createdAt, createdAt,
]; ];
@ -5267,6 +5293,24 @@ class $GroupMembersTable extends GroupMembers
), ),
); );
} }
if (data.containsKey('last_chat_opened')) {
context.handle(
_lastChatOpenedMeta,
lastChatOpened.isAcceptableOrUnknown(
data['last_chat_opened']!,
_lastChatOpenedMeta,
),
);
}
if (data.containsKey('last_type_indicator')) {
context.handle(
_lastTypeIndicatorMeta,
lastTypeIndicator.isAcceptableOrUnknown(
data['last_type_indicator']!,
_lastTypeIndicatorMeta,
),
);
}
if (data.containsKey('last_message')) { if (data.containsKey('last_message')) {
context.handle( context.handle(
_lastMessageMeta, _lastMessageMeta,
@ -5309,6 +5353,14 @@ class $GroupMembersTable extends GroupMembers
DriftSqlType.blob, DriftSqlType.blob,
data['${effectivePrefix}group_public_key'], data['${effectivePrefix}group_public_key'],
), ),
lastChatOpened: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}last_chat_opened'],
),
lastTypeIndicator: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}last_type_indicator'],
),
lastMessage: attachedDatabase.typeMapping.read( lastMessage: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}last_message'], data['${effectivePrefix}last_message'],
@ -5336,6 +5388,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
final int contactId; final int contactId;
final MemberState? memberState; final MemberState? memberState;
final Uint8List? groupPublicKey; final Uint8List? groupPublicKey;
final DateTime? lastChatOpened;
final DateTime? lastTypeIndicator;
final DateTime? lastMessage; final DateTime? lastMessage;
final DateTime createdAt; final DateTime createdAt;
const GroupMember({ const GroupMember({
@ -5343,6 +5397,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
required this.contactId, required this.contactId,
this.memberState, this.memberState,
this.groupPublicKey, this.groupPublicKey,
this.lastChatOpened,
this.lastTypeIndicator,
this.lastMessage, this.lastMessage,
required this.createdAt, required this.createdAt,
}); });
@ -5359,6 +5415,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
if (!nullToAbsent || groupPublicKey != null) { if (!nullToAbsent || groupPublicKey != null) {
map['group_public_key'] = Variable<Uint8List>(groupPublicKey); map['group_public_key'] = Variable<Uint8List>(groupPublicKey);
} }
if (!nullToAbsent || lastChatOpened != null) {
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened);
}
if (!nullToAbsent || lastTypeIndicator != null) {
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator);
}
if (!nullToAbsent || lastMessage != null) { if (!nullToAbsent || lastMessage != null) {
map['last_message'] = Variable<DateTime>(lastMessage); map['last_message'] = Variable<DateTime>(lastMessage);
} }
@ -5376,6 +5438,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: groupPublicKey == null && nullToAbsent groupPublicKey: groupPublicKey == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(groupPublicKey), : Value(groupPublicKey),
lastChatOpened: lastChatOpened == null && nullToAbsent
? const Value.absent()
: Value(lastChatOpened),
lastTypeIndicator: lastTypeIndicator == null && nullToAbsent
? const Value.absent()
: Value(lastTypeIndicator),
lastMessage: lastMessage == null && nullToAbsent lastMessage: lastMessage == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(lastMessage), : Value(lastMessage),
@ -5395,6 +5463,10 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
serializer.fromJson<String?>(json['memberState']), serializer.fromJson<String?>(json['memberState']),
), ),
groupPublicKey: serializer.fromJson<Uint8List?>(json['groupPublicKey']), groupPublicKey: serializer.fromJson<Uint8List?>(json['groupPublicKey']),
lastChatOpened: serializer.fromJson<DateTime?>(json['lastChatOpened']),
lastTypeIndicator: serializer.fromJson<DateTime?>(
json['lastTypeIndicator'],
),
lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']), lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
); );
@ -5409,6 +5481,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
$GroupMembersTable.$convertermemberStaten.toJson(memberState), $GroupMembersTable.$convertermemberStaten.toJson(memberState),
), ),
'groupPublicKey': serializer.toJson<Uint8List?>(groupPublicKey), 'groupPublicKey': serializer.toJson<Uint8List?>(groupPublicKey),
'lastChatOpened': serializer.toJson<DateTime?>(lastChatOpened),
'lastTypeIndicator': serializer.toJson<DateTime?>(lastTypeIndicator),
'lastMessage': serializer.toJson<DateTime?>(lastMessage), 'lastMessage': serializer.toJson<DateTime?>(lastMessage),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
}; };
@ -5419,6 +5493,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
int? contactId, int? contactId,
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
DateTime? createdAt, DateTime? createdAt,
}) => GroupMember( }) => GroupMember(
@ -5428,6 +5504,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: groupPublicKey.present groupPublicKey: groupPublicKey.present
? groupPublicKey.value ? groupPublicKey.value
: this.groupPublicKey, : this.groupPublicKey,
lastChatOpened: lastChatOpened.present
? lastChatOpened.value
: this.lastChatOpened,
lastTypeIndicator: lastTypeIndicator.present
? lastTypeIndicator.value
: this.lastTypeIndicator,
lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
); );
@ -5441,6 +5523,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: data.groupPublicKey.present groupPublicKey: data.groupPublicKey.present
? data.groupPublicKey.value ? data.groupPublicKey.value
: this.groupPublicKey, : this.groupPublicKey,
lastChatOpened: data.lastChatOpened.present
? data.lastChatOpened.value
: this.lastChatOpened,
lastTypeIndicator: data.lastTypeIndicator.present
? data.lastTypeIndicator.value
: this.lastTypeIndicator,
lastMessage: data.lastMessage.present lastMessage: data.lastMessage.present
? data.lastMessage.value ? data.lastMessage.value
: this.lastMessage, : this.lastMessage,
@ -5455,6 +5543,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('memberState: $memberState, ') ..write('memberState: $memberState, ')
..write('groupPublicKey: $groupPublicKey, ') ..write('groupPublicKey: $groupPublicKey, ')
..write('lastChatOpened: $lastChatOpened, ')
..write('lastTypeIndicator: $lastTypeIndicator, ')
..write('lastMessage: $lastMessage, ') ..write('lastMessage: $lastMessage, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt')
..write(')')) ..write(')'))
@ -5467,6 +5557,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
contactId, contactId,
memberState, memberState,
$driftBlobEquality.hash(groupPublicKey), $driftBlobEquality.hash(groupPublicKey),
lastChatOpened,
lastTypeIndicator,
lastMessage, lastMessage,
createdAt, createdAt,
); );
@ -5481,6 +5573,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
other.groupPublicKey, other.groupPublicKey,
this.groupPublicKey, this.groupPublicKey,
) && ) &&
other.lastChatOpened == this.lastChatOpened &&
other.lastTypeIndicator == this.lastTypeIndicator &&
other.lastMessage == this.lastMessage && other.lastMessage == this.lastMessage &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt);
} }
@ -5490,6 +5584,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
final Value<int> contactId; final Value<int> contactId;
final Value<MemberState?> memberState; final Value<MemberState?> memberState;
final Value<Uint8List?> groupPublicKey; final Value<Uint8List?> groupPublicKey;
final Value<DateTime?> lastChatOpened;
final Value<DateTime?> lastTypeIndicator;
final Value<DateTime?> lastMessage; final Value<DateTime?> lastMessage;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<int> rowid; final Value<int> rowid;
@ -5498,6 +5594,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
this.contactId = const Value.absent(), this.contactId = const Value.absent(),
this.memberState = const Value.absent(), this.memberState = const Value.absent(),
this.groupPublicKey = const Value.absent(), this.groupPublicKey = const Value.absent(),
this.lastChatOpened = const Value.absent(),
this.lastTypeIndicator = const Value.absent(),
this.lastMessage = const Value.absent(), this.lastMessage = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -5507,6 +5605,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
required int contactId, required int contactId,
this.memberState = const Value.absent(), this.memberState = const Value.absent(),
this.groupPublicKey = const Value.absent(), this.groupPublicKey = const Value.absent(),
this.lastChatOpened = const Value.absent(),
this.lastTypeIndicator = const Value.absent(),
this.lastMessage = const Value.absent(), this.lastMessage = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -5517,6 +5617,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
Expression<int>? contactId, Expression<int>? contactId,
Expression<String>? memberState, Expression<String>? memberState,
Expression<Uint8List>? groupPublicKey, Expression<Uint8List>? groupPublicKey,
Expression<DateTime>? lastChatOpened,
Expression<DateTime>? lastTypeIndicator,
Expression<DateTime>? lastMessage, Expression<DateTime>? lastMessage,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<int>? rowid, Expression<int>? rowid,
@ -5526,6 +5628,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
if (contactId != null) 'contact_id': contactId, if (contactId != null) 'contact_id': contactId,
if (memberState != null) 'member_state': memberState, if (memberState != null) 'member_state': memberState,
if (groupPublicKey != null) 'group_public_key': groupPublicKey, if (groupPublicKey != null) 'group_public_key': groupPublicKey,
if (lastChatOpened != null) 'last_chat_opened': lastChatOpened,
if (lastTypeIndicator != null) 'last_type_indicator': lastTypeIndicator,
if (lastMessage != null) 'last_message': lastMessage, if (lastMessage != null) 'last_message': lastMessage,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
@ -5537,6 +5641,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
Value<int>? contactId, Value<int>? contactId,
Value<MemberState?>? memberState, Value<MemberState?>? memberState,
Value<Uint8List?>? groupPublicKey, Value<Uint8List?>? groupPublicKey,
Value<DateTime?>? lastChatOpened,
Value<DateTime?>? lastTypeIndicator,
Value<DateTime?>? lastMessage, Value<DateTime?>? lastMessage,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<int>? rowid, Value<int>? rowid,
@ -5546,6 +5652,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
contactId: contactId ?? this.contactId, contactId: contactId ?? this.contactId,
memberState: memberState ?? this.memberState, memberState: memberState ?? this.memberState,
groupPublicKey: groupPublicKey ?? this.groupPublicKey, groupPublicKey: groupPublicKey ?? this.groupPublicKey,
lastChatOpened: lastChatOpened ?? this.lastChatOpened,
lastTypeIndicator: lastTypeIndicator ?? this.lastTypeIndicator,
lastMessage: lastMessage ?? this.lastMessage, lastMessage: lastMessage ?? this.lastMessage,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
@ -5569,6 +5677,12 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
if (groupPublicKey.present) { if (groupPublicKey.present) {
map['group_public_key'] = Variable<Uint8List>(groupPublicKey.value); map['group_public_key'] = Variable<Uint8List>(groupPublicKey.value);
} }
if (lastChatOpened.present) {
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened.value);
}
if (lastTypeIndicator.present) {
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator.value);
}
if (lastMessage.present) { if (lastMessage.present) {
map['last_message'] = Variable<DateTime>(lastMessage.value); map['last_message'] = Variable<DateTime>(lastMessage.value);
} }
@ -5588,6 +5702,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('memberState: $memberState, ') ..write('memberState: $memberState, ')
..write('groupPublicKey: $groupPublicKey, ') ..write('groupPublicKey: $groupPublicKey, ')
..write('lastChatOpened: $lastChatOpened, ')
..write('lastTypeIndicator: $lastTypeIndicator, ')
..write('lastMessage: $lastMessage, ') ..write('lastMessage: $lastMessage, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
@ -13326,6 +13442,8 @@ typedef $$GroupMembersTableCreateCompanionBuilder =
required int contactId, required int contactId,
Value<MemberState?> memberState, Value<MemberState?> memberState,
Value<Uint8List?> groupPublicKey, Value<Uint8List?> groupPublicKey,
Value<DateTime?> lastChatOpened,
Value<DateTime?> lastTypeIndicator,
Value<DateTime?> lastMessage, Value<DateTime?> lastMessage,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -13336,6 +13454,8 @@ typedef $$GroupMembersTableUpdateCompanionBuilder =
Value<int> contactId, Value<int> contactId,
Value<MemberState?> memberState, Value<MemberState?> memberState,
Value<Uint8List?> groupPublicKey, Value<Uint8List?> groupPublicKey,
Value<DateTime?> lastChatOpened,
Value<DateTime?> lastTypeIndicator,
Value<DateTime?> lastMessage, Value<DateTime?> lastMessage,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -13403,6 +13523,16 @@ class $$GroupMembersTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get lastMessage => $composableBuilder( ColumnFilters<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -13479,6 +13609,16 @@ class $$GroupMembersTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get lastMessage => $composableBuilder( ColumnOrderings<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -13556,6 +13696,16 @@ class $$GroupMembersTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => column,
);
GeneratedColumn<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => column,
);
GeneratedColumn<DateTime> get lastMessage => $composableBuilder( GeneratedColumn<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => column, builder: (column) => column,
@ -13643,6 +13793,8 @@ class $$GroupMembersTableTableManager
Value<int> contactId = const Value.absent(), Value<int> contactId = const Value.absent(),
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -13651,6 +13803,8 @@ class $$GroupMembersTableTableManager
contactId: contactId, contactId: contactId,
memberState: memberState, memberState: memberState,
groupPublicKey: groupPublicKey, groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage, lastMessage: lastMessage,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,
@ -13661,6 +13815,8 @@ class $$GroupMembersTableTableManager
required int contactId, required int contactId,
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -13669,6 +13825,8 @@ class $$GroupMembersTableTableManager
contactId: contactId, contactId: contactId,
memberState: memberState, memberState: memberState,
groupPublicKey: groupPublicKey, groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage, lastMessage: lastMessage,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,

View file

@ -5484,6 +5484,345 @@ i1.GeneratedColumn<int> _column_208(
'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))', 'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))',
defaultValue: const i1.CustomExpression('0'), defaultValue: const i1.CustomExpression('0'),
); );
final class Schema11 extends i0.VersionedSchema {
Schema11({required super.database}) : super(version: 11);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
messageActions,
groupHistories,
];
late final Shape22 contacts = Shape22(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(user_id)'],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id)'],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 mediaFiles = Shape36(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(media_id)'],
columns: [
_column_143,
_column_144,
_column_145,
_column_146,
_column_147,
_column_148,
_column_149,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id)'],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
columns: [_column_174, _column_176, _column_177, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 groupMembers = Shape38(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_209,
_column_210,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape37 receipts = Shape37(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_208,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [_column_182, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_194, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
columns: [_column_195, _column_196, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
columns: [_column_197, _column_198],
attachedDatabase: database,
),
alias: null,
);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_199, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
columns: [_column_174, _column_183, _column_144, _column_200],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_history_id)'],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null,
);
}
class Shape38 extends i0.VersionedTable {
Shape38({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get groupId =>
columnsByName['group_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get memberState =>
columnsByName['member_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get groupPublicKey =>
columnsByName['group_public_key']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get lastChatOpened =>
columnsByName['last_chat_opened']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get lastTypeIndicator =>
columnsByName['last_type_indicator']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get lastMessage =>
columnsByName['last_message']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_209(String aliasedName) =>
i1.GeneratedColumn<int>(
'last_chat_opened',
aliasedName,
true,
type: i1.DriftSqlType.int,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<int> _column_210(String aliasedName) =>
i1.GeneratedColumn<int>(
'last_type_indicator',
aliasedName,
true,
type: i1.DriftSqlType.int,
$customConstraints: 'NULL',
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -5494,6 +5833,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -5542,6 +5882,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema); await from9To10(migrator, schema);
return 10; return 10;
case 10:
final schema = Schema11(database: database);
final migrator = i1.Migrator(database, schema);
await from10To11(migrator, schema);
return 11;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -5558,6 +5903,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -5569,5 +5915,6 @@ i1.OnUpgrade stepByStep({
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10, from9To10: from9To10,
from10To11: from10To11,
), ),
); );

View file

@ -376,11 +376,11 @@ abstract class AppLocalizations {
/// **'Username'** /// **'Username'**
String get searchUsernameInput; String get searchUsernameInput;
/// No description provided for @searchUsernameTitle. /// No description provided for @addFriendTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Search username'** /// **'Add friends'**
String get searchUsernameTitle; String get addFriendTitle;
/// No description provided for @searchUserNamePreview. /// No description provided for @searchUserNamePreview.
/// ///
@ -733,9 +733,33 @@ abstract class AppLocalizations {
/// No description provided for @settingsNotifyTroubleshootingNoProblemDesc. /// No description provided for @settingsNotifyTroubleshootingNoProblemDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.'** /// **'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.'**
String get settingsNotifyTroubleshootingNoProblemDesc; String get settingsNotifyTroubleshootingNoProblemDesc;
/// No description provided for @settingsNotifyResetTitle.
///
/// In en, this message translates to:
/// **'Didn\'t receive a test notification?'**
String get settingsNotifyResetTitle;
/// No description provided for @settingsNotifyResetTitleSubtitle.
///
/// In en, this message translates to:
/// **'If you haven\'t received any test notifications, click here to reset your notification tokens.'**
String get settingsNotifyResetTitleSubtitle;
/// No description provided for @settingsNotifyResetTitleReset.
///
/// In en, this message translates to:
/// **'Your notification tokens have been reset.'**
String get settingsNotifyResetTitleReset;
/// No description provided for @settingsNotifyResetTitleResetDesc.
///
/// In en, this message translates to:
/// **'If the problem persists, please send us your debug log via Settings > Help so we can investigate the issue.'**
String get settingsNotifyResetTitleResetDesc;
/// No description provided for @settingsHelp. /// No description provided for @settingsHelp.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2818,6 +2842,12 @@ abstract class AppLocalizations {
/// **'Scan other profile'** /// **'Scan other profile'**
String get scanOtherProfile; String get scanOtherProfile;
/// No description provided for @openYourOwnQRcode.
///
/// In en, this message translates to:
/// **'Open your own QR code'**
String get openYourOwnQRcode;
/// No description provided for @skipForNow. /// No description provided for @skipForNow.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3057,6 +3087,60 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Your QR code'** /// **'Your QR code'**
String get profileYourQrCode; String get profileYourQrCode;
/// No description provided for @settingsScreenLock.
///
/// In en, this message translates to:
/// **'Screen lock'**
String get settingsScreenLock;
/// No description provided for @settingsScreenLockSubtitle.
///
/// In en, this message translates to:
/// **'To open twonly, you\'ll need to use your smartphone\'s unlock feature.'**
String get settingsScreenLockSubtitle;
/// No description provided for @settingsScreenLockAuthMessageEnable.
///
/// In en, this message translates to:
/// **'Use the screen lock from twonly.'**
String get settingsScreenLockAuthMessageEnable;
/// No description provided for @settingsScreenLockAuthMessageDisable.
///
/// In en, this message translates to:
/// **'Disable the screen lock from twonly.'**
String get settingsScreenLockAuthMessageDisable;
/// No description provided for @unlockTwonly.
///
/// In en, this message translates to:
/// **'Unlock twonly'**
String get unlockTwonly;
/// No description provided for @unlockTwonlyTryAgain.
///
/// In en, this message translates to:
/// **'Try again'**
String get unlockTwonlyTryAgain;
/// No description provided for @unlockTwonlyDesc.
///
/// In en, this message translates to:
/// **'Use your phone\'s unlock settings to unlock twonly'**
String get unlockTwonlyDesc;
/// No description provided for @settingsTypingIndication.
///
/// In en, this message translates to:
/// **'Typing Indicators'**
String get settingsTypingIndication;
/// No description provided for @settingsTypingIndicationSubtitle.
///
/// In en, this message translates to:
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
String get settingsTypingIndicationSubtitle;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -162,7 +162,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get searchUsernameInput => 'Benutzername'; String get searchUsernameInput => 'Benutzername';
@override @override
String get searchUsernameTitle => 'Benutzernamen suchen'; String get addFriendTitle => 'Freunde hinzufügen';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -356,7 +356,22 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsNotifyTroubleshootingNoProblemDesc => String get settingsNotifyTroubleshootingNoProblemDesc =>
'Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.'; 'Um eine Testbenachrichtigung zu erhalten, klicke auf OK. Falls du die Testbenachrichtigung nicht erhältst, klicke bitte auf den neuen Menüpunkt, der nach dem Klicken auf „OK“ angezeigt wird.';
@override
String get settingsNotifyResetTitle => 'Keine Testbenachrichtigung erhalten?';
@override
String get settingsNotifyResetTitleSubtitle =>
'Falls du keine Testbenachrichtigungen erhalten hast, klicke hier, um deine Benachrichtigungstoken zurückzusetzen.';
@override
String get settingsNotifyResetTitleReset =>
'Deine Benachrichtigungstoken wurden zurückgesetzt.';
@override
String get settingsNotifyResetTitleResetDesc =>
'Sollte das Problem weiterhin bestehen, sende uns bitte dein Debug-Protokoll über „Einstellungen“ > „Hilfe“, damit wir das Problem untersuchen können.';
@override @override
String get settingsHelp => 'Hilfe'; String get settingsHelp => 'Hilfe';
@ -1553,6 +1568,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get scanOtherProfile => 'Scanne ein anderes Profil'; String get scanOtherProfile => 'Scanne ein anderes Profil';
@override
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
@override @override
String get skipForNow => 'Vorerst überspringen'; String get skipForNow => 'Vorerst überspringen';
@ -1711,4 +1729,36 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Dein QR-Code'; String get profileYourQrCode => 'Dein QR-Code';
@override
String get settingsScreenLock => 'Bildschirmsperre';
@override
String get settingsScreenLockSubtitle =>
'Um twonly zu öffnen, wird die Entsperrfunktion deines Smartphones verwenden.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Bildschirmsperre von twonly verwenden';
@override
String get settingsScreenLockAuthMessageDisable =>
'Bildschirmsperre von twonly deaktivieren.';
@override
String get unlockTwonly => 'twonly entsperren';
@override
String get unlockTwonlyTryAgain => 'Erneut versuchen';
@override
String get unlockTwonlyDesc =>
'Entsperre twonly über die Sperreinstellungen deines Handys';
@override
String get settingsTypingIndication => 'Tipp-Indikatoren';
@override
String get settingsTypingIndicationSubtitle =>
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
} }

View file

@ -161,7 +161,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get searchUsernameInput => 'Username'; String get searchUsernameInput => 'Username';
@override @override
String get searchUsernameTitle => 'Search username'; String get addFriendTitle => 'Add friends';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -351,7 +351,22 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsNotifyTroubleshootingNoProblemDesc => String get settingsNotifyTroubleshootingNoProblemDesc =>
'Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.'; 'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.';
@override
String get settingsNotifyResetTitle => 'Didn\'t receive a test notification?';
@override
String get settingsNotifyResetTitleSubtitle =>
'If you haven\'t received any test notifications, click here to reset your notification tokens.';
@override
String get settingsNotifyResetTitleReset =>
'Your notification tokens have been reset.';
@override
String get settingsNotifyResetTitleResetDesc =>
'If the problem persists, please send us your debug log via Settings > Help so we can investigate the issue.';
@override @override
String get settingsHelp => 'Help'; String get settingsHelp => 'Help';
@ -1543,6 +1558,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get scanOtherProfile => 'Scan other profile'; String get scanOtherProfile => 'Scan other profile';
@override
String get openYourOwnQRcode => 'Open your own QR code';
@override @override
String get skipForNow => 'Skip for now'; String get skipForNow => 'Skip for now';
@ -1699,4 +1717,36 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
@override
String get settingsTypingIndication => 'Typing Indicators';
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
} }

View file

@ -161,7 +161,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get searchUsernameInput => 'Username'; String get searchUsernameInput => 'Username';
@override @override
String get searchUsernameTitle => 'Search username'; String get addFriendTitle => 'Add friends';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -351,7 +351,22 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settingsNotifyTroubleshootingNoProblemDesc => String get settingsNotifyTroubleshootingNoProblemDesc =>
'Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.'; 'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.';
@override
String get settingsNotifyResetTitle => 'Didn\'t receive a test notification?';
@override
String get settingsNotifyResetTitleSubtitle =>
'If you haven\'t received any test notifications, click here to reset your notification tokens.';
@override
String get settingsNotifyResetTitleReset =>
'Your notification tokens have been reset.';
@override
String get settingsNotifyResetTitleResetDesc =>
'If the problem persists, please send us your debug log via Settings > Help so we can investigate the issue.';
@override @override
String get settingsHelp => 'Help'; String get settingsHelp => 'Help';
@ -1543,6 +1558,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get scanOtherProfile => 'Scan other profile'; String get scanOtherProfile => 'Scan other profile';
@override
String get openYourOwnQRcode => 'Open your own QR code';
@override @override
String get skipForNow => 'Skip for now'; String get skipForNow => 'Skip for now';
@ -1699,4 +1717,36 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
@override
String get settingsTypingIndication => 'Typing Indicators';
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
} }

@ -1 +1 @@
Subproject commit 284c602b507e77addc8f21c4fc8a321f237cac1b Subproject commit 93f2b3daddd98dbb022c34e7c5976a76c3143236

View file

@ -53,6 +53,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool requestedAudioPermission = false; bool requestedAudioPermission = false;
@JsonKey(defaultValue: true)
bool videoStabilizationEnabled = true;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool showFeedbackShortcut = true; bool showFeedbackShortcut = true;
@ -72,6 +75,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool autoStoreAllSendUnlimitedMediaFiles = false; bool autoStoreAllSendUnlimitedMediaFiles = false;
@JsonKey(defaultValue: true)
bool typingIndicators = true;
String? lastPlanBallance; String? lastPlanBallance;
String? additionalUserInvites; String? additionalUserInvites;
@ -84,6 +90,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false; bool allowErrorTrackingViaSentry = false;
@JsonKey(defaultValue: false)
bool screenLockEnabled = false;
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)

View file

@ -30,6 +30,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? true
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending = ..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false json['showShowImagePreviewWhenSending'] as bool? ?? false
@ -48,6 +50,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['storeMediaFilesInGallery'] as bool? ?? false json['storeMediaFilesInGallery'] as bool? ?? false
..autoStoreAllSendUnlimitedMediaFiles = ..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..typingIndicators = json['typingIndicators'] as bool? ?? true
..lastPlanBallance = json['lastPlanBallance'] as String? ..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String?
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?) ..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
@ -60,6 +63,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..allowErrorTrackingViaSentry = ..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false json['allowErrorTrackingViaSentry'] as bool? ?? false
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -105,6 +109,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
'showFeedbackShortcut': instance.showFeedbackShortcut, 'showFeedbackShortcut': instance.showFeedbackShortcut,
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending, 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
'startWithCameraOpen': instance.startWithCameraOpen, 'startWithCameraOpen': instance.startWithCameraOpen,
@ -113,6 +118,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'autoStoreAllSendUnlimitedMediaFiles': 'autoStoreAllSendUnlimitedMediaFiles':
instance.autoStoreAllSendUnlimitedMediaFiles, instance.autoStoreAllSendUnlimitedMediaFiles,
'typingIndicators': instance.typingIndicators,
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed, 'tutorialDisplayed': instance.tutorialDisplayed,
@ -120,6 +126,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
?.toIso8601String(), ?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'screenLockEnabled': instance.screenLockEnabled,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'http_requests.pb.dart'; export 'http_requests.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'client_to_server.pb.dart'; export 'client_to_server.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'error.pb.dart'; export 'error.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'server_to_client.pb.dart'; export 'server_to_client.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'backup.pb.dart'; export 'backup.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'groups.pb.dart'; export 'groups.pb.dart';

View file

@ -1692,6 +1692,79 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
void clearForceUpdate() => $_clearField(4); void clearForceUpdate() => $_clearField(4);
} }
class EncryptedContent_TypingIndicator extends $pb.GeneratedMessage {
factory EncryptedContent_TypingIndicator({
$core.bool? isTyping,
$fixnum.Int64? createdAt,
}) {
final result = create();
if (isTyping != null) result.isTyping = isTyping;
if (createdAt != null) result.createdAt = createdAt;
return result;
}
EncryptedContent_TypingIndicator._();
factory EncryptedContent_TypingIndicator.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_TypingIndicator.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.TypingIndicator',
createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'isTyping')
..aInt64(2, _omitFieldNames ? '' : 'createdAt')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_TypingIndicator clone() =>
EncryptedContent_TypingIndicator()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_TypingIndicator copyWith(
void Function(EncryptedContent_TypingIndicator) updates) =>
super.copyWith(
(message) => updates(message as EncryptedContent_TypingIndicator))
as EncryptedContent_TypingIndicator;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_TypingIndicator create() =>
EncryptedContent_TypingIndicator._();
@$core.override
EncryptedContent_TypingIndicator createEmptyInstance() => create();
static $pb.PbList<EncryptedContent_TypingIndicator> createRepeated() =>
$pb.PbList<EncryptedContent_TypingIndicator>();
@$core.pragma('dart2js:noInline')
static EncryptedContent_TypingIndicator getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_TypingIndicator>(
create);
static EncryptedContent_TypingIndicator? _defaultInstance;
@$pb.TagNumber(1)
$core.bool get isTyping => $_getBF(0);
@$pb.TagNumber(1)
set isTyping($core.bool value) => $_setBool(0, value);
@$pb.TagNumber(1)
$core.bool hasIsTyping() => $_has(0);
@$pb.TagNumber(1)
void clearIsTyping() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get createdAt => $_getI64(1);
@$pb.TagNumber(2)
set createdAt($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasCreatedAt() => $_has(1);
@$pb.TagNumber(2)
void clearCreatedAt() => $_clearField(2);
}
class EncryptedContent extends $pb.GeneratedMessage { class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({ factory EncryptedContent({
$core.String? groupId, $core.String? groupId,
@ -1712,6 +1785,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
EncryptedContent_ErrorMessages? errorMessages, EncryptedContent_ErrorMessages? errorMessages,
EncryptedContent_AdditionalDataMessage? additionalDataMessage, EncryptedContent_AdditionalDataMessage? additionalDataMessage,
EncryptedContent_TypingIndicator? typingIndicator,
}) { }) {
final result = create(); final result = create();
if (groupId != null) result.groupId = groupId; if (groupId != null) result.groupId = groupId;
@ -1735,6 +1809,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (errorMessages != null) result.errorMessages = errorMessages; if (errorMessages != null) result.errorMessages = errorMessages;
if (additionalDataMessage != null) if (additionalDataMessage != null)
result.additionalDataMessage = additionalDataMessage; result.additionalDataMessage = additionalDataMessage;
if (typingIndicator != null) result.typingIndicator = typingIndicator;
return result; return result;
} }
@ -1801,6 +1876,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_AdditionalDataMessage>( ..aOM<EncryptedContent_AdditionalDataMessage>(
19, _omitFieldNames ? '' : 'additionalDataMessage', 19, _omitFieldNames ? '' : 'additionalDataMessage',
subBuilder: EncryptedContent_AdditionalDataMessage.create) subBuilder: EncryptedContent_AdditionalDataMessage.create)
..aOM<EncryptedContent_TypingIndicator>(
20, _omitFieldNames ? '' : 'typingIndicator',
subBuilder: EncryptedContent_TypingIndicator.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2025,6 +2103,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(19) @$pb.TagNumber(19)
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() => EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
$_ensure(17); $_ensure(17);
@$pb.TagNumber(20)
EncryptedContent_TypingIndicator get typingIndicator => $_getN(18);
@$pb.TagNumber(20)
set typingIndicator(EncryptedContent_TypingIndicator value) =>
$_setField(20, value);
@$pb.TagNumber(20)
$core.bool hasTypingIndicator() => $_has(18);
@$pb.TagNumber(20)
void clearTypingIndicator() => $_clearField(20);
@$pb.TagNumber(20)
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -326,6 +326,16 @@ const EncryptedContent$json = {
'10': 'additionalDataMessage', '10': 'additionalDataMessage',
'17': true '17': true
}, },
{
'1': 'typing_indicator',
'3': 20,
'4': 1,
'5': 11,
'6': '.EncryptedContent.TypingIndicator',
'9': 18,
'10': 'typingIndicator',
'17': true
},
], ],
'3': [ '3': [
EncryptedContent_ErrorMessages$json, EncryptedContent_ErrorMessages$json,
@ -342,7 +352,8 @@ const EncryptedContent$json = {
EncryptedContent_ContactRequest$json, EncryptedContent_ContactRequest$json,
EncryptedContent_ContactUpdate$json, EncryptedContent_ContactUpdate$json,
EncryptedContent_PushKeys$json, EncryptedContent_PushKeys$json,
EncryptedContent_FlameSync$json EncryptedContent_FlameSync$json,
EncryptedContent_TypingIndicator$json
], ],
'8': [ '8': [
{'1': '_groupId'}, {'1': '_groupId'},
@ -363,6 +374,7 @@ const EncryptedContent$json = {
{'1': '_resendGroupPublicKey'}, {'1': '_resendGroupPublicKey'},
{'1': '_error_messages'}, {'1': '_error_messages'},
{'1': '_additional_data_message'}, {'1': '_additional_data_message'},
{'1': '_typing_indicator'},
], ],
}; };
@ -840,6 +852,15 @@ const EncryptedContent_FlameSync$json = {
], ],
}; };
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_TypingIndicator$json = {
'1': 'TypingIndicator',
'2': [
{'1': 'is_typing', '3': 1, '4': 1, '5': 8, '10': 'isTyping'},
{'1': 'created_at', '3': 2, '4': 1, '5': 3, '10': 'createdAt'},
],
};
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0' 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
@ -864,68 +885,71 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h' 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
'bERhdGFNZXNzYWdliAEBGvABCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' 'bERhdGFNZXNzYWdliAEBElEKEHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb2'
'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf' '50ZW50LlR5cGluZ0luZGljYXRvckgSUg90eXBpbmdJbmRpY2F0b3KIAQEa8AEKDUVycm9yTWVz'
'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' 'c2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeX'
'1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT' 'BlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3'
'U0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADGlEKC0dyb3VwQ3JlYXRlEhoKCH' 'CgRUeXBlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVU'
'N0YXRlS2V5GAMgASgMUghzdGF0ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQ' 'VTVF9JTlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9P'
'dWJsaWNLZXkaMwoJR3JvdXBKb2luEiYKDmdyb3VwUHVibGljS2V5GAEgASgMUg5ncm91cFB1Ym' 'Rl9TWU5DEAMaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5EiYKDm'
'xpY0tleRoWChRSZXNlbmRHcm91cFB1YmxpY0tleRq2AgoLR3JvdXBVcGRhdGUSKAoPZ3JvdXBB' 'dyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoOZ3Jv'
'Y3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZWRDb250YWN0SWQYAi' 'dXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2'
'ABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1lGAMgASgJSAFSDG5l' 'V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0aW9u'
'd0dyb3VwTmFtZYgBARJTCiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgAS' 'VHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBAR'
'gDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFAoSX2FmZmVjdGVk' 'InCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0ZU1l'
'Q29udGFjdElkQg8KDV9uZXdHcm91cE5hbWVCJQojX25ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW' 'c3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
'xsaXNlY29uZHMaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgBIAEoCVIPc2Vu' '1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFtZUIl'
'ZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW' 'CiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2USKA'
'1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEQoP' 'oPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEoCVIE'
'X3F1b3RlTWVzc2FnZUlkGs4BChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3' 'dGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgEIA'
'NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVz' 'EoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0aW9u'
'dGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgAS' 'YWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZU'
'gMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBAUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2Rh' 'lkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUSOwoX'
'dGEaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSW' 'YWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiA'
'QSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNz' 'EBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRNZXNz'
'YWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdG' 'YWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFgoGcm'
'UuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlk' 'Vtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVu'
'iAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZX' 'Y3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3NhZ2'
'NzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNzYWdl'
'bWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQh' 'SWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZX'
'IKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJ' 'h0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAAS'
'ZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW' 'DQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4dB'
'50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANI' 'rwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMAoE'
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdG' 'dHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNwbG'
'lvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'F5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k'
'bWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg' 'c4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2'
'1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktl' 'F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAYg'
'eRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW' 'ASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93bm'
'5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v' 'xvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQES'
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX' 'KQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb2'
'NzYWdlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxAC' '5Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdl'
'EgcKA0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD1' 'X2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUE'
'9xdW90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5f' 'xPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtfZGlz'
'ZW5jcnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2' 'cGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZF'
'VfZGF0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQu' 'Rva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9u'
'TWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE' 'Tm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2CgR0eX'
'1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElP' 'BlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigKD3Rh'
'Tl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb2' 'cmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUEVORU'
'50ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoG' 'QQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QS'
'UkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' 'OQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdH'
'VuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0Nv' 'lwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoNQ29u'
'bXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIA' 'dGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYX'
'EoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgB' 'RlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdD'
'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' 'b21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3BsYX'
'NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEg' 'lOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQ'
'ASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgAS' 'REFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcGxheU'
'gDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgD' '5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hL'
'SAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2' 'ZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSA'
'V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVy' 'FSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoH'
'GAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbG' 'UkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQaqQ'
'FzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAK' 'EKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0'
'C2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3' 'RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3'
'RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVk' 'RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNlVXBk'
'aWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdE' 'YXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190eXBpbmcYASABKAhSCGlzVHlwaW5nEh0KCm'
'IMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdl' 'NyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdEIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0'
'Qg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW' 'QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDg'
'5kR3JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFf' 'oMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpf'
'bWVzc2FnZQ=='); 'ZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4KDF'
'9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3Jv'
'dXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2'
'FnZUITChFfdHlwaW5nX2luZGljYXRvcg==');

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'messages.pb.dart'; export 'messages.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'push_notification.pb.dart'; export 'push_notification.pb.dart';

View file

@ -53,6 +53,7 @@ message EncryptedContent {
optional ResendGroupPublicKey resendGroupPublicKey = 17; optional ResendGroupPublicKey resendGroupPublicKey = 17;
optional ErrorMessages error_messages = 18; optional ErrorMessages error_messages = 18;
optional AdditionalDataMessage additional_data_message = 19; optional AdditionalDataMessage additional_data_message = 19;
optional TypingIndicator typing_indicator = 20;
message ErrorMessages { message ErrorMessages {
enum Type { enum Type {
@ -194,4 +195,9 @@ message EncryptedContent {
bool forceUpdate = 4; bool forceUpdate = 4;
} }
message TypingIndicator {
bool is_typing = 1;
int64 created_at = 2;
}
} }

View file

@ -133,8 +133,10 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
final jsonData = base64Decode(b64Data); final jsonData = base64Decode(b64Data);
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>; final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
final expiresDate = data['expiresDate'] as int; final expiresDate = data['expiresDate'] as int;
final dt = final dt = DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(expiresDate, isUtc: true); expiresDate,
isUtc: true,
);
if (dt.isBefore(DateTime.now())) { if (dt.isBefore(DateTime.now())) {
Log.warn('ExpiresDate is in the past: $dt'); Log.warn('ExpiresDate is in the past: $dt');
if (_userTriggeredBuyButton && Platform.isIOS) { if (_userTriggeredBuyButton && Platform.isIOS) {

View file

@ -26,6 +26,7 @@ import 'package:twonly/src/views/settings/data_and_storage/export_media.view.dar
import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart'; import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart';
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/views/settings/developer/developer.view.dart'; import 'package:twonly/src/views/settings/developer/developer.view.dart';
import 'package:twonly/src/views/settings/developer/reduce_flames.view.dart';
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/views/settings/help/contact_us.view.dart';
@ -84,10 +85,10 @@ final routerProvider = GoRouter(
}, },
), ),
GoRoute( GoRoute(
path: 'messages', path: 'messages/:groupId',
builder: (context, state) { builder: (context, state) {
final group = state.extra! as Group; final groupId = state.pathParameters['groupId']!;
return ChatMessagesView(group); return ChatMessagesView(groupId);
}, },
), ),
], ],
@ -280,6 +281,10 @@ final routerProvider = GoRouter(
path: 'automated_testing', path: 'automated_testing',
builder: (context, state) => const AutomatedTestingView(), builder: (context, state) => const AutomatedTestingView(),
), ),
GoRoute(
path: 'reduce_flames',
builder: (context, state) => const ReduceFlamesView(),
),
], ],
), ),
GoRoute( GoRoute(

View file

@ -92,12 +92,14 @@ class ApiService {
if (globalIsInBackgroundTask) { if (globalIsInBackgroundTask) {
await retransmitRawBytes(); await retransmitRawBytes();
await tryTransmitMessages(); await retransmitAllMessages();
await reuploadMediaFiles();
await tryDownloadAllMediaFiles(); await tryDownloadAllMediaFiles();
} else if (!globalIsAppInBackground) { } else if (!globalIsAppInBackground) {
unawaited(retransmitRawBytes()); unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages()); unawaited(retransmitAllMessages());
unawaited(tryDownloadAllMediaFiles()); unawaited(tryDownloadAllMediaFiles());
unawaited(reuploadMediaFiles());
twonlyDB.markUpdated(); twonlyDB.markUpdated();
unawaited(syncFlameCounters()); unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers()); unawaited(setupNotificationWithUsers());
@ -133,11 +135,12 @@ class ApiService {
return; return;
} }
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
Log.info('Starting reconnection timer with $_reconnectionDelay s delay');
reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async { reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async {
Log.info('Reconnection timer triggered');
reconnectionTimer = null; reconnectionTimer = null;
// only try to reconnect in case the app is in the foreground
if (!globalIsAppInBackground) {
await connect(); await connect();
}
}); });
_reconnectionDelay = 3; _reconnectionDelay = 3;
} }

View file

@ -20,8 +20,9 @@ Future<void> handleAdditionalDataMessage(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
type: Value(message.type), type: Value(message.type),
additionalMessageData: additionalMessageData: Value(
Value(Uint8List.fromList(message.additionalMessageData)), Uint8List.fromList(message.additionalMessageData),
),
createdAt: Value(fromTimestamp(message.timestamp)), createdAt: Value(fromTimestamp(message.timestamp)),
ackByServer: Value(clock.now()), ackByServer: Value(clock.now()),
), ),

View file

@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -137,8 +138,9 @@ Future<void> handleGroupUpdate(
GroupHistoriesCompanion( GroupHistoriesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
type: Value(actionType), type: Value(actionType),
newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds: Value(
Value(update.newDeleteMessagesAfterMilliseconds.toInt()), update.newDeleteMessagesAfterMilliseconds.toInt(),
),
contactId: Value(fromUserId), contactId: Value(fromUserId),
), ),
); );
@ -146,8 +148,9 @@ Future<void> handleGroupUpdate(
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
group.groupId, group.groupId,
GroupsCompanion( GroupsCompanion(
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds: Value(
Value(update.newDeleteMessagesAfterMilliseconds.toInt()), update.newDeleteMessagesAfterMilliseconds.toInt(),
),
), ),
); );
} }
@ -221,3 +224,24 @@ Future<void> handleResendGroupPublicKey(
), ),
); );
} }
Future<void> handleTypingIndicator(
int fromUserId,
String groupId,
EncryptedContent_TypingIndicator indicator,
) async {
var lastTypeIndicator = const Value<DateTime?>.absent();
if (indicator.isTyping) {
lastTypeIndicator = Value(fromTimestamp(indicator.createdAt));
}
await twonlyDB.groupsDao.updateMember(
groupId,
fromUserId,
GroupMembersCompanion(
lastChatOpened: Value(fromTimestamp(indicator.createdAt)),
lastTypeIndicator: lastTypeIndicator,
),
);
}

View file

@ -73,13 +73,39 @@ Future<void> handleMedia(
mediaType = MediaType.audio; mediaType = MediaType.audio;
} }
var mediaIdValue = const Value<String>.absent();
final messageTmp = await twonlyDB.messagesDao final messageTmp = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId) .getMessageById(media.senderMessageId)
.getSingleOrNull(); .getSingleOrNull();
if (messageTmp != null) { if (messageTmp != null) {
Log.warn('This message already exit. Message is dropped.'); if (messageTmp.senderId != fromUserId) {
Log.warn(
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
);
return; return;
} }
if (messageTmp.mediaId == null) {
Log.warn(
'This message already exit without a mediaId. Message is dropped.',
);
return;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
messageTmp.mediaId!,
);
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
Log.warn(
'This message and media file already exit and was not requested again. Dropping it.',
);
return;
}
if (mediaFile != null) {
// media file is reuploaded use the same mediaId
mediaIdValue = Value(mediaFile.mediaId);
}
}
int? displayLimitInMilliseconds; int? displayLimitInMilliseconds;
if (media.hasDisplayLimitInMilliseconds()) { if (media.hasDisplayLimitInMilliseconds()) {
@ -95,8 +121,9 @@ Future<void> handleMedia(
late Message? message; late Message? message;
await twonlyDB.transaction(() async { await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertMedia( mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion( MediaFilesCompanion(
mediaId: mediaIdValue,
downloadState: const Value(DownloadState.pending), downloadState: const Value(DownloadState.pending),
type: Value(mediaType), type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication), requiresAuthentication: Value(media.requiresAuthentication),
@ -205,23 +232,6 @@ Future<void> handleMediaUpdate(
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}'); Log.info('Got media file decryption error ${mediaFile.mediaId}');
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? []; await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
reuploadRequestedBy.add(fromUserId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.preprocessing),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();
}
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
}
} }
} }

View file

@ -54,8 +54,9 @@ Future<void> handleMessageUpdate(
} }
Future<bool> isSender(int fromUserId, String messageId) async { Future<bool> isSender(int fromUserId, String messageId) async {
final message = final message = await twonlyDB.messagesDao
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); .getMessageById(messageId)
.getSingleOrNull();
if (message == null) return false; if (message == null) return false;
if (message.senderId == fromUserId) { if (message.senderId == fromUserId) {
return true; return true;

View file

@ -14,8 +14,9 @@ Future<void> handlePushKey(
switch (pushKeys.type) { switch (pushKeys.type) {
case EncryptedContent_PushKeys_Type.REQUEST: case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('Got a pushkey request from $contactId'); Log.info('Got a pushkey request from $contactId');
if (lastPushKeyRequest if (lastPushKeyRequest.isBefore(
.isBefore(clock.now().subtract(const Duration(seconds: 60)))) { clock.now().subtract(const Duration(seconds: 60)),
)) {
lastPushKeyRequest = clock.now(); lastPushKeyRequest = clock.now();
unawaited(setupNotificationWithUsers(forceContact: contactId)); unawaited(setupNotificationWithUsers(forceContact: contactId));
} }

View file

@ -26,6 +26,148 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:workmanager/workmanager.dart' hide TaskStatus; import 'package:workmanager/workmanager.dart' hide TaskStatus;
final lockRetransmission = Mutex();
Future<void> reuploadMediaFiles() async {
return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao
.getReceiptsForMediaRetransmissions();
if (receipts.isEmpty) return;
Log.info('Reuploading ${receipts.length} media files to the server.');
final contacts = <int, Contact>{};
for (final receipt in receipts) {
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
final twentyFourHoursAgo = DateTime.now().subtract(
const Duration(hours: 24),
);
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
Log.info(
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
);
continue;
}
}
var messageId = receipt.messageId;
if (receipt.messageId == null) {
Log.info('Message not in receipt. Loading it from the content.');
try {
final content = EncryptedContent.fromBuffer(receipt.message);
if (content.hasMedia()) {
messageId = content.media.senderMessageId;
await twonlyDB.receiptsDao.updateReceipt(
receipt.receiptId,
ReceiptsCompanion(
messageId: Value(messageId),
),
);
}
} catch (e) {
Log.error(e);
}
}
if (messageId == null) {
Log.error('MessageId is empty for media file receipts');
continue;
}
if (receipt.markForRetryAfterAccepted != null) {
if (!contacts.containsKey(receipt.contactId)) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(receipt.contactId)
.getSingleOrNull();
if (contact == null) {
Log.error(
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
);
continue;
}
contacts[receipt.contactId] = contact;
}
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
Log.warn(
'Could not send message as contact has still not yet accepted.',
);
continue;
}
}
if (receipt.ackByServerAt == null) {
// media file must be reuploaded again in case the media files
// was deleted by the server, the receiver will request a new media reupload
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null || message.mediaId == null) {
Log.error(
'Message not found for reupload of the receipt (${message == null} - ${message?.mediaId}).',
);
continue;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile == null) {
Log.error(
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
);
continue;
}
await reuploadMediaFile(
receipt.contactId,
mediaFile,
message.messageId,
);
} else {
Log.info('Reuploading media file $messageId');
// the media file should be still on the server, so it should be enough
// to just resend the message containing the download token.
await tryToSendCompleteMessage(receipt: receipt);
}
}
});
}
Future<void> reuploadMediaFile(
int contactId,
MediaFile mediaFile,
String messageId,
) async {
Log.info('Reuploading media file: ${mediaFile.mediaId}');
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
contactId,
messageId,
const ReceiptsCompanion(
markForRetry: Value(null),
markForRetryAfterAccepted: Value(null),
),
);
final reuploadRequestedBy = (mediaFile.reuploadRequestedBy ?? [])
..add(contactId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.preprocessing),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();
}
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
}
}
Future<void> finishStartedPreprocessing() async { Future<void> finishStartedPreprocessing() async {
final mediaFiles = await twonlyDB.mediaFilesDao final mediaFiles = await twonlyDB.mediaFilesDao
.getAllMediaFilesPendingUpload(); .getAllMediaFilesPendingUpload();
@ -62,7 +204,7 @@ Future<void> finishStartedPreprocessing() async {
/// It can happen, that a media files is uploaded but not yet marked for been uploaded. /// It can happen, that a media files is uploaded but not yet marked for been uploaded.
/// For example because the background_downloader plugin has not yet reported the finished upload. /// For example because the background_downloader plugin has not yet reported the finished upload.
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded. /// In case the message receipts or a reaction was received, mark the media file as been uploaded.
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async { Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
final message = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessageById(messageId) .getMessageById(messageId)
@ -100,6 +242,16 @@ Future<void> markUploadAsSuccessful(MediaFile media) async {
message.messageId, message.messageId,
clock.now(), clock.now(),
); );
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
contact.contactId,
message.messageId,
ReceiptsCompanion(
ackByServerAt: Value(clock.now()),
retryCount: const Value(1),
lastRetry: Value(clock.now()),
markForRetry: const Value(null),
),
);
} }
} }
} }
@ -122,7 +274,7 @@ Future<MediaFileService?> initializeMediaUpload(
const MediaFilesCompanion(isDraftMedia: Value(false)), const MediaFilesCompanion(isDraftMedia: Value(false)),
); );
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion( MediaFilesCompanion(
uploadState: const Value(UploadState.initialized), uploadState: const Value(UploadState.initialized),
displayLimitInMilliseconds: Value(displayLimitInMilliseconds), displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
@ -147,6 +299,16 @@ Future<void> insertMediaFileInMessagesTable(
), ),
); );
for (final groupId in groupIds) { for (final groupId in groupIds) {
final groupMembers = await twonlyDB.groupsDao.getGroupContact(groupId);
if (groupMembers.length == 1) {
if (groupMembers.first.accountDeleted) {
Log.warn(
'Did not send media file to $groupId because the only account has deleted his account.',
);
continue;
}
}
final message = await twonlyDB.messagesDao.insertMessage( final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
@ -280,6 +442,14 @@ Future<void> _createUploadRequest(MediaFileService media) async {
} }
} }
final contact = await twonlyDB.contactsDao.getContactById(
groupMember.contactId,
);
if (contact == null || contact.accountDeleted) {
continue;
}
final downloadToken = getRandomUint8List(32); final downloadToken = getRandomUint8List(32);
late EncryptedContent_Media_Type type; late EncryptedContent_Media_Type type;
@ -295,7 +465,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
} }
if (media.mediaFile.reuploadRequestedBy != null) { if (media.mediaFile.reuploadRequestedBy != null) {
type = EncryptedContent_Media_Type.REUPLOAD; // not used any more... Receiver detects automatically if it is an reupload...
// type = EncryptedContent_Media_Type.REUPLOAD;
} }
final notEncryptedContent = EncryptedContent( final notEncryptedContent = EncryptedContent(
@ -322,6 +493,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final cipherText = await sendCipherText( final cipherText = await sendCipherText(
groupMember.contactId, groupMember.contactId,
notEncryptedContent, notEncryptedContent,
messageId: message.messageId,
onlyReturnEncryptedData: true, onlyReturnEncryptedData: true,
); );
@ -329,10 +501,11 @@ Future<void> _createUploadRequest(MediaFileService media) async {
Log.error( Log.error(
'Could not generate ciphertext message for ${groupMember.contactId}', 'Could not generate ciphertext message for ${groupMember.contactId}',
); );
continue;
} }
final messageOnSuccess = TextMessage() final messageOnSuccess = TextMessage()
..body = cipherText!.$1 ..body = cipherText.$1
..userId = Int64(groupMember.contactId); ..userId = Int64(groupMember.contactId);
if (cipherText.$2 != null) { if (cipherText.$2 != null) {

View file

@ -23,7 +23,7 @@ import 'package:twonly/src/utils/misc.dart';
final lockRetransmission = Mutex(); final lockRetransmission = Mutex();
Future<void> tryTransmitMessages() async { Future<void> retransmitAllMessages() async {
return lockRetransmission.protect(() async { return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission(); final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
@ -95,8 +95,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
return null; return null;
} }
Log.info('Uploading $receiptId');
final message = pb.Message.fromBuffer(receipt.message) final message = pb.Message.fromBuffer(receipt.message)
..receiptId = receiptId; ..receiptId = receiptId;
@ -110,9 +108,11 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
encryptedContent, encryptedContent,
); );
Log.info('Uploading $receiptId. (${pushNotification?.kind})');
Uint8List? pushData; Uint8List? pushData;
if (pushNotification != null && receipt.retryCount <= 3) { if (pushNotification != null && receipt.retryCount <= 1) {
/// In case the message has to be resend more than three times, do not show a notification again... // Only show the push notification the first two time.
pushData = await encryptPushNotification( pushData = await encryptPushNotification(
receipt.contactId, receipt.contactId,
pushNotification, pushNotification,
@ -300,10 +300,17 @@ Future<void> sendCipherTextToGroup(
String groupId, String groupId,
pb.EncryptedContent encryptedContent, { pb.EncryptedContent encryptedContent, {
String? messageId, String? messageId,
bool onlySendIfNoReceiptsAreOpen = false,
}) async { }) async {
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
if (messageId != null ||
encryptedContent.hasReaction() ||
encryptedContent.hasMedia() ||
encryptedContent.hasTextMessage()) {
// only update the counter in case this is a actual message
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
}
encryptedContent.groupId = groupId; encryptedContent.groupId = groupId;
@ -313,6 +320,7 @@ Future<void> sendCipherTextToGroup(
encryptedContent, encryptedContent,
messageId: messageId, messageId: messageId,
blocking: false, blocking: false,
onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen,
); );
} }
} }
@ -323,19 +331,48 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
bool blocking = true, bool blocking = true,
String? messageId, String? messageId,
bool onlySendIfNoReceiptsAreOpen = false,
}) async { }) async {
if (onlySendIfNoReceiptsAreOpen) {
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId,
);
if (openReceipts > 2) {
// this prevents that these types of messages are send in case the receiver is offline
return null;
}
}
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
final response = pb.Message() final response = pb.Message()
..type = pb.Message_Type.CIPHERTEXT ..type = pb.Message_Type.CIPHERTEXT
..encryptedContent = encryptedContent.writeToBuffer(); ..encryptedContent = encryptedContent.writeToBuffer();
var retryCounter = 0;
DateTime? lastRetry;
if (messageId != null) {
final receipts = await twonlyDB.receiptsDao
.getReceiptsByContactAndMessageId(contactId, messageId);
for (final receipt in receipts) {
if (receipt.lastRetry != null) {
lastRetry = receipt.lastRetry;
}
retryCounter += 1;
Log.info('Removing duplicated receipt for message $messageId');
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
}
}
final receipt = await twonlyDB.receiptsDao.insertReceipt( final receipt = await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion( ReceiptsCompanion(
contactId: Value(contactId), contactId: Value(contactId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
messageId: Value(messageId), messageId: Value(messageId),
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData), willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
retryCount: Value(retryCounter),
lastRetry: Value(lastRetry),
), ),
); );
@ -353,6 +390,20 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
return null; return null;
} }
Future<void> sendTypingIndication(String groupId, bool isTyping) async {
if (!gUser.typingIndicators) return;
await sendCipherTextToGroup(
groupId,
pb.EncryptedContent(
typingIndicator: pb.EncryptedContent_TypingIndicator(
isTyping: isTyping,
createdAt: Int64(clock.now().millisecondsSinceEpoch),
),
),
onlySendIfNoReceiptsAreOpen: true,
);
}
Future<void> notifyContactAboutOpeningMessage( Future<void> notifyContactAboutOpeningMessage(
int contactId, int contactId,
List<String> messageOtherIds, List<String> messageOtherIds,

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
@ -25,6 +26,7 @@ import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -79,14 +81,18 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
final message = Message.fromBuffer(body); final message = Message.fromBuffer(body);
final receiptId = message.receiptId; final receiptId = message.receiptId;
await protectReceiptCheck.protect(() async { final isDuplicated = await protectReceiptCheck.protect(() async {
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
Log.warn('Got duplicated message from the server.'); return true;
return;
} }
await twonlyDB.receiptsDao.gotReceipt(receiptId); await twonlyDB.receiptsDao.gotReceipt(receiptId);
return false;
}); });
if (isDuplicated) {
return;
}
switch (message.type) { switch (message.type) {
case Message_Type.SENDER_DELIVERY_RECEIPT: case Message_Type.SENDER_DELIVERY_RECEIPT:
Log.info('Got delivery receipt for $receiptId!'); Log.info('Got delivery receipt for $receiptId!');
@ -131,8 +137,9 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
if (message.hasEncryptedContent()) { if (message.hasEncryptedContent()) {
Value<String>? receiptIdDB; Value<String>? receiptIdDB;
final encryptedContentRaw = final encryptedContentRaw = Uint8List.fromList(
Uint8List.fromList(message.encryptedContent); message.encryptedContent,
);
Message? response; Message? response;
@ -155,8 +162,10 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
} }
if (response == null) { if (response == null) {
final (encryptedContent, plainTextContent) = final (
await handleEncryptedMessage( encryptedContent,
plainTextContent,
) = await handleEncryptedMessageRaw(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
message.type, message.type,
@ -174,6 +183,9 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
encryptedContent: encryptedContent.writeToBuffer(), encryptedContent: encryptedContent.writeToBuffer(),
); );
receiptIdDB = const Value.absent(); receiptIdDB = const Value.absent();
} else {
// Message was successful processed
//
} }
} }
@ -198,27 +210,48 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
} }
} }
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
int fromUserId, int fromUserId,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
Message_Type messageType, Message_Type messageType,
String receiptId, String receiptId,
) async { ) async {
final (content, decryptionErrorType) = await signalDecryptMessage( final (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
messageType.value, messageType.value,
); );
if (content == null) { if (encryptedContent == null) {
return ( return (
null, null,
PlaintextContent() PlaintextContent()
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() ..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
..type = decryptionErrorType!) ..type = decryptionErrorType!),
); );
} }
final (a, b) = await handleEncryptedMessage(
fromUserId,
encryptedContent,
messageType,
receiptId,
);
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);
}
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
int fromUserId,
EncryptedContent content,
Message_Type messageType,
String receiptId,
) async {
// We got a valid message fromUserId, so mark all messages which where // We got a valid message fromUserId, so mark all messages which where
// send to the user but not yet ACK for retransmission. All marked messages // send to the user but not yet ACK for retransmission. All marked messages
// will be either transmitted again after a new server connection (minimum 20 seconds). // will be either transmitted again after a new server connection (minimum 20 seconds).
@ -235,7 +268,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return ( return (
null, null,
PlaintextContent() PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage() ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
return (null, null); return (null, null);
@ -312,7 +345,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
relatedReceiptId: receiptId, relatedReceiptId: receiptId,
), ),
), ),
null null,
); );
} }
Log.info( Log.info(
@ -333,7 +366,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return ( return (
null, null,
PlaintextContent() PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage() ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
@ -365,7 +398,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return ( return (
null, null,
PlaintextContent() PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage() ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
return (null, null); return (null, null);
@ -416,5 +449,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null); return (null, null);
} }
if (content.hasTypingIndicator()) {
await handleTypingIndicator(
fromUserId,
content.groupId,
content.typingIndicator,
);
}
return (null, null); return (null, null);
} }

View file

@ -65,8 +65,9 @@ Future<void> handleMediaError(MediaFile media) async {
downloadState: Value(DownloadState.reuploadRequested), downloadState: Value(DownloadState.reuploadRequested),
), ),
); );
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); media.mediaId,
);
if (messages.length != 1) return; if (messages.length != 1) return;
final message = messages.first; final message = messages.first;
if (message.senderId == null) return; if (message.senderId == null) return;

View file

@ -49,7 +49,18 @@ void callbackDispatcher() {
}); });
} }
bool _isInitialized = false;
Future<bool> initBackgroundExecution() async { Future<bool> initBackgroundExecution() async {
if (_isInitialized) {
// Reload the users, as on Android the background isolate can
// stay alive for multiple hours between task executions
final user = await getUser();
if (user == null) return false;
gUser = user;
return true;
}
SentryWidgetsFlutterBinding.ensureInitialized(); SentryWidgetsFlutterBinding.ensureInitialized();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory = globalApplicationSupportDirectory =
@ -65,12 +76,13 @@ Future<bool> initBackgroundExecution() async {
apiService = ApiService(); apiService = ApiService();
globalIsInBackgroundTask = true; globalIsInBackgroundTask = true;
_isInitialized = true;
return true; return true;
} }
final Mutex _keyValueMutex = Mutex(); final Mutex _keyValueMutex = Mutex();
Future<void> handlePeriodicTask() async { Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
final shouldBeExecuted = await exclusiveAccess( final shouldBeExecuted = await exclusiveAccess(
lockName: 'periodic_task', lockName: 'periodic_task',
mutex: _keyValueMutex, mutex: _keyValueMutex,
@ -84,7 +96,8 @@ Future<void> handlePeriodicTask() async {
final lastExecutionDate = DateTime.fromMillisecondsSinceEpoch( final lastExecutionDate = DateTime.fromMillisecondsSinceEpoch(
lastExecutionTime, lastExecutionTime,
); );
if (DateTime.now().difference(lastExecutionDate).inMinutes < 2) { if (DateTime.now().difference(lastExecutionDate).inSeconds <
lastExecutionInSecondsLimit) {
return false; return false;
} }
} }

View file

@ -11,8 +11,10 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = final (backupId, encryptionKey) = await getMasterKey(
await getMasterKey(password, gUser.username); password,
gUser.username,
);
await updateUserdata((user) { await updateUserdata((user) {
user.twonlySafeBackup = TwonlySafeBackup( user.twonlySafeBackup = TwonlySafeBackup(

View file

@ -13,8 +13,9 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
final groups = await twonlyDB.groupsDao.getAllGroups(); final groups = await twonlyDB.groupsDao.getAllGroups();
if (groups.isEmpty) return; if (groups.isEmpty) return;
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
final bestFriend = final bestFriend = groups.firstWhere(
groups.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); (x) => x.totalMediaCounter == maxMessageCounter,
);
if (gUser.myBestFriendGroupId != bestFriend.groupId) { if (gUser.myBestFriendGroupId != bestFriend.groupId) {
await updateUserdata((user) { await updateUserdata((user) {
@ -42,8 +43,9 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
EncryptedContent( EncryptedContent(
flameSync: EncryptedContent_FlameSync( flameSync: EncryptedContent_FlameSync(
flameCounter: Int64(flameCounter), flameCounter: Int64(flameCounter),
lastFlameCounterChange: lastFlameCounterChange: Int64(
Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), group.lastFlameCounterChange!.millisecondsSinceEpoch,
),
bestFriend: group.groupId == bestFriend.groupId, bestFriend: group.groupId == bestFriend.groupId,
forceUpdate: group.groupId == forceForGroup, forceUpdate: group.groupId == forceForGroup,
), ),
@ -134,8 +136,9 @@ Future<void> incFlameCounter(
// Overwrite max flame counter either the current is bigger or the the max flame counter is older then 4 days // Overwrite max flame counter either the current is bigger or the the max flame counter is older then 4 days
if (flameCounter >= maxFlameCounter || if (flameCounter >= maxFlameCounter ||
maxFlameCounterFrom == null || maxFlameCounterFrom == null ||
maxFlameCounterFrom maxFlameCounterFrom.isBefore(
.isBefore(clock.now().subtract(const Duration(days: 5)))) { clock.now().subtract(const Duration(days: 5)),
)) {
maxFlameCounter = flameCounter; maxFlameCounter = flameCounter;
maxFlameCounterFrom = clock.now(); maxFlameCounterFrom = clock.now();
} }
@ -172,6 +175,7 @@ bool isItPossibleToRestoreFlames(Group group) {
final flameCounter = getFlameCounterFromGroup(group); final flameCounter = getFlameCounterFromGroup(group);
return group.maxFlameCounter > 2 && return group.maxFlameCounter > 2 &&
flameCounter < group.maxFlameCounter && flameCounter < group.maxFlameCounter &&
group.maxFlameCounterFrom! group.maxFlameCounterFrom!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 5))); clock.now().subtract(const Duration(days: 7)),
);
} }

View file

@ -45,8 +45,9 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
memberIds: [Int64(gUser.userId)] + memberIds, memberIds: [Int64(gUser.userId)] + memberIds,
adminIds: [Int64(gUser.userId)], adminIds: [Int64(gUser.userId)],
groupName: groupName, groupName: groupName,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds: Int64(
Int64(defaultDeleteMessagesAfterMilliseconds), defaultDeleteMessagesAfterMilliseconds,
),
padding: List<int>.generate(Random().nextInt(80), (_) => 0), padding: List<int>.generate(Random().nextInt(80), (_) => 0),
); );
@ -158,8 +159,9 @@ Future<void> fetchMissingGroupPublicKey() async {
for (final member in members) { for (final member in members) {
if (member.lastMessage == null) continue; if (member.lastMessage == null) continue;
// only request if the users has send a message in the last two days. // only request if the users has send a message in the last two days.
if (member.lastMessage! if (member.lastMessage!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 2)))) { clock.now().subtract(const Duration(days: 2)),
)) {
await sendCipherText( await sendCipherText(
member.contactId, member.contactId,
EncryptedContent( EncryptedContent(
@ -227,12 +229,15 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
final groupStateServer = GroupState.fromBuffer(response.bodyBytes); final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
final encryptedStateRaw = final encryptedStateRaw = await _decryptEnvelop(
await _decryptEnvelop(group, groupStateServer.encryptedGroupState); group,
groupStateServer.encryptedGroupState,
);
if (encryptedStateRaw == null) return null; if (encryptedStateRaw == null) return null;
final encryptedGroupState = final encryptedGroupState = EncryptedGroupState.fromBuffer(
EncryptedGroupState.fromBuffer(encryptedStateRaw); encryptedStateRaw,
);
if (group.stateVersionId >= groupStateServer.versionId.toInt()) { if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
Log.info( Log.info(
@ -266,24 +271,28 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
); );
if (encryptedStateRaw == null) continue; if (encryptedStateRaw == null) continue;
final appended = final appended = EncryptedAppendedGroupState.fromBuffer(
EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw); encryptedStateRaw,
);
if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) { if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) {
final keyPair = final keyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); group.myGroupPrivateKey!,
);
final appendedPubKey = appendedState.appendTBS.publicKey; final appendedPubKey = appendedState.appendTBS.publicKey;
final myPubKey = keyPair.getPublicKey().serialize().toList(); final myPubKey = keyPair.getPublicKey().serialize().toList();
if (listEquals(appendedPubKey, myPubKey)) { if (listEquals(appendedPubKey, myPubKey)) {
adminIds.remove(Int64(gUser.userId)); adminIds.remove(Int64(gUser.userId));
memberIds memberIds.remove(
.remove(Int64(gUser.userId)); // -> Will remove the user later... Int64(gUser.userId),
); // -> Will remove the user later...
} else { } else {
Log.info('A non admin left the group!!!'); Log.info('A non admin left the group!!!');
final member = await twonlyDB.groupsDao final member = await twonlyDB.groupsDao.getGroupMemberByPublicKey(
.getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey)); Uint8List.fromList(appendedPubKey),
);
if (member == null) { if (member == null) {
Log.error('Member is already not in this group...'); Log.error('Member is already not in this group...');
continue; continue;
@ -353,8 +362,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
), ),
); );
var currentGroupMembers = var currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); group.groupId,
);
// First find and insert NEW members // First find and insert NEW members
for (final memberId in memberIds) { for (final memberId in memberIds) {
@ -391,8 +401,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// Send the new user my public group key // Send the new user my public group key
if (group.myGroupPrivateKey != null) { if (group.myGroupPrivateKey != null) {
final keyPair = final keyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); group.myGroupPrivateKey!,
);
await sendCipherText( await sendCipherText(
memberId.toInt(), memberId.toInt(),
EncryptedContent( EncryptedContent(
@ -407,8 +418,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// check if there is a member which is not in the server list... // check if there is a member which is not in the server list...
// update the current members list // update the current members list
currentGroupMembers = currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); group.groupId,
);
for (final member in currentGroupMembers) { for (final member in currentGroupMembers) {
// Member is not any more in the members list // Member is not any more in the members list
@ -468,8 +480,9 @@ Future<bool> addNewHiddenContact(int contactId) async {
ContactsCompanion( ContactsCompanion(
username: Value(utf8.decode(userData.username)), username: Value(utf8.decode(userData.username)),
userId: Value(contactId), userId: Value(contactId),
deletedByUser: deletedByUser: const Value(
const Value(true), // this will hide the contact in the contact list true,
), // this will hide the contact in the contact list
), ),
); );
await processSignalUserData(userData); await processSignalUserData(userData);
@ -594,8 +607,9 @@ Future<bool> manageAdminState(
return false; return false;
} }
final groupActionType = final groupActionType = remove
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin; ? GroupActionType.demoteToMember
: GroupActionType.promoteToAdmin;
await sendCipherTextToGroup( await sendCipherTextToGroup(
group.groupId, group.groupId,
@ -664,8 +678,9 @@ Future<bool> updateChatDeletionTime(
if (currentState == null) return false; if (currentState == null) return false;
final (versionId, state) = currentState; final (versionId, state) = currentState;
state.deleteMessagesAfterMilliseconds = state.deleteMessagesAfterMilliseconds = Int64(
Int64(deleteMessagesAfterMilliseconds); deleteMessagesAfterMilliseconds,
);
// send new state to the server // send new state to the server
if (!await _updateGroupState(group, state)) { if (!await _updateGroupState(group, state)) {
@ -688,8 +703,9 @@ Future<bool> updateChatDeletionTime(
GroupHistoriesCompanion( GroupHistoriesCompanion(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.changeDisplayMaxTime), type: const Value(GroupActionType.changeDisplayMaxTime),
newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds: Value(
Value(deleteMessagesAfterMilliseconds), deleteMessagesAfterMilliseconds,
),
), ),
); );

View file

@ -72,13 +72,19 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
try { try {
final task = VideoRenderData( final task = VideoRenderData(
video: EditorVideo.file(media.originalPath), videoSegments: [
imageBytes: media.overlayImagePath.readAsBytesSync(), VideoSegment(video: EditorVideo.file(media.originalPath)),
],
imageLayers: [
ImageLayer(image: EditorLayerImage.file(media.overlayImagePath)),
],
enableAudio: !media.removeAudio, enableAudio: !media.removeAudio,
); );
await ProVideoEditor.instance await ProVideoEditor.instance.renderVideoToFile(
.renderVideoToFile(media.ffmpegOutputPath.path, task); media.ffmpegOutputPath.path,
task,
);
if (Platform.isIOS || if (Platform.isIOS ||
media.ffmpegOutputPath.statSync().size >= 10_000_000 || media.ffmpegOutputPath.statSync().size >= 10_000_000 ||
@ -115,8 +121,8 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024) final sizeFrom = (media.ffmpegOutputPath.statSync().size / 1024 / 1024)
.toStringAsFixed(2); .toStringAsFixed(2);
final sizeTo = final sizeTo = (media.tempPath.statSync().size / 1024 / 1024)
(media.tempPath.statSync().size / 1024 / 1024).toStringAsFixed(2); .toStringAsFixed(2);
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.', 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from $sizeFrom to $sizeTo bytes.',

View file

@ -44,8 +44,9 @@ class MediaFileService {
delete = false; delete = false;
} }
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); mediaId,
);
// in case messages in empty the file will be deleted, as delete is true by default // in case messages in empty the file will be deleted, as delete is true by default
@ -63,16 +64,18 @@ class MediaFileService {
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days // This branch will prevent to reach the next if condition, with would otherwise store the image for two days
// delete = true; // do not overwrite a previous delete = false // delete = true; // do not overwrite a previous delete = false
// this is just to make it easier to understand :) // this is just to make it easier to understand :)
} else if (message.openedAt! } else if (message.openedAt!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 2)))) { clock.now().subtract(const Duration(days: 2)),
)) {
// In case the image was opened, but send with unlimited time or no authentication. // In case the image was opened, but send with unlimited time or no authentication.
if (message.senderId == null) { if (message.senderId == null) {
delete = false; delete = false;
} else { } else {
// Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image. // Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image.
// This also allows to reopen this image for two days. // This also allows to reopen this image for two days.
final group = final group = await twonlyDB.groupsDao.getGroup(
await twonlyDB.groupsDao.getGroup(message.groupId); message.groupId,
);
if (group != null && !group.isDirectChat) { if (group != null && !group.isDirectChat) {
delete = false; delete = false;
} }
@ -93,8 +96,9 @@ class MediaFileService {
} }
Future<void> updateFromDB() async { Future<void> updateFromDB() async {
final updated = final updated = await twonlyDB.mediaFilesDao.getMediaFileById(
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); mediaFile.mediaId,
);
if (updated != null) { if (updated != null) {
mediaFile = updated; mediaFile = updated;
} }
@ -151,8 +155,9 @@ class MediaFileService {
mediaFile.mediaId, mediaFile.mediaId,
MediaFilesCompanion( MediaFilesCompanion(
requiresAuthentication: Value(requiresAuthentication), requiresAuthentication: Value(requiresAuthentication),
displayLimitInMilliseconds: displayLimitInMilliseconds: requiresAuthentication
requiresAuthentication ? const Value(12000) : const Value.absent(), ? const Value(12000)
: const Value.absent(),
), ),
); );
await updateFromDB(); await updateFromDB();
@ -208,6 +213,13 @@ class MediaFileService {
} }
} }
// Media was send with unlimited display limit time and without auth required
// and the temp media file still exists, then the media file can be reopened again...
bool get canBeOpenedAgain =>
!mediaFile.requiresAuthentication &&
mediaFile.displayLimitInMilliseconds == null &&
tempPath.existsSync();
bool get imagePreviewAvailable => bool get imagePreviewAvailable =>
thumbnailPath.existsSync() || storedPath.existsSync(); thumbnailPath.existsSync() || storedPath.existsSync();
@ -293,8 +305,10 @@ class MediaFileService {
extension = 'm4a'; extension = 'm4a';
} }
} }
final mediaBaseDir = final mediaBaseDir = buildDirectoryPath(
buildDirectoryPath(directory, globalApplicationSupportDirectory); directory,
globalApplicationSupportDirectory,
);
return File( return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
); );

View file

@ -6,10 +6,12 @@ import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/localization/generated/app_localizations_de.dart'; import 'package:twonly/src/localization/generated/app_localizations_de.dart';
import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/localization/generated/app_localizations_en.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -45,10 +47,34 @@ Future<void> customLocalPushNotification(String title, String msg) async {
); );
} }
Future<void> showPushNotificationFromServerMessages(
int fromUserId,
EncryptedContent encryptedContent,
) async {
final pushData = await getPushNotificationFromEncryptedContent(
null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user.
null,
encryptedContent,
);
if (pushData != null) {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final pushUser in pushUsers) {
if (pushUser.userId.toInt() == fromUserId) {
String? groupId;
if (encryptedContent.hasGroupId()) {
groupId = encryptedContent.groupId;
}
return showLocalPushNotification(pushUser, pushData, groupId: groupId);
}
}
}
}
Future<void> handlePushData(String pushDataB64) async { Future<void> handlePushData(String pushDataB64) async {
try { try {
final pushData = final pushData = EncryptedPushNotification.fromBuffer(
EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64)); base64.decode(pushDataB64),
);
PushNotification? pushNotification; PushNotification? pushNotification;
PushUser? foundPushUser; PushUser? foundPushUser;
@ -121,8 +147,10 @@ Future<PushNotification?> tryDecryptMessage(
mac: Mac(push.mac), mac: Mac(push.mac),
); );
final plaintext = final plaintext = await chacha20.decrypt(
await chacha20.decrypt(secretBox, secretKey: secretKeyData); secretBox,
secretKey: secretKeyData,
);
return PushNotification.fromBuffer(plaintext); return PushNotification.fromBuffer(plaintext);
} catch (e) { } catch (e) {
// this error is allowed to happen... // this error is allowed to happen...
@ -132,8 +160,9 @@ Future<PushNotification?> tryDecryptMessage(
Future<void> showLocalPushNotification( Future<void> showLocalPushNotification(
PushUser pushUser, PushUser pushUser,
PushNotification pushNotification, PushNotification pushNotification, {
) async { String? groupId,
}) async {
String? title; String? title;
String? body; String? body;
@ -174,13 +203,26 @@ Future<void> showLocalPushNotification(
iOS: darwinNotificationDetails, iOS: darwinNotificationDetails,
); );
String? payload;
if (groupId != null &&
(pushNotification.kind == PushKind.text ||
pushNotification.kind == PushKind.response ||
pushNotification.kind == PushKind.reactionToAudio ||
pushNotification.kind == PushKind.storedMediaFile ||
pushNotification.kind == PushKind.reactionToImage ||
pushNotification.kind == PushKind.reactionToText ||
pushNotification.kind == PushKind.reactionToAudio)) {
payload = Routes.chatsMessages(groupId);
}
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
pushUser.userId.toInt() % // Invalid argument (id): must fit within the size of a 32-bit integer
2147483647, // Invalid argument (id): must fit within the size of a 32-bit integer pushUser.userId.toInt() % 2147483647,
title, title,
body, body,
notificationDetails, notificationDetails,
// payload: pushNotification.kind.name, payload: payload,
); );
} }
@ -259,17 +301,22 @@ String getPushNotificationText(PushNotification pushNotification) {
PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile,
PushKind.reaction.name: lang.notificationReaction, PushKind.reaction.name: lang.notificationReaction,
PushKind.reopenedMedia.name: lang.notificationReopenedMedia, PushKind.reopenedMedia.name: lang.notificationReopenedMedia,
PushKind.reactionToVideo.name: PushKind.reactionToVideo.name: lang.notificationReactionToVideo(
lang.notificationReactionToVideo(pushNotification.additionalContent), pushNotification.additionalContent,
PushKind.reactionToAudio.name: ),
lang.notificationReactionToAudio(pushNotification.additionalContent), PushKind.reactionToAudio.name: lang.notificationReactionToAudio(
PushKind.reactionToText.name: pushNotification.additionalContent,
lang.notificationReactionToText(pushNotification.additionalContent), ),
PushKind.reactionToImage.name: PushKind.reactionToText.name: lang.notificationReactionToText(
lang.notificationReactionToImage(pushNotification.additionalContent), pushNotification.additionalContent,
),
PushKind.reactionToImage.name: lang.notificationReactionToImage(
pushNotification.additionalContent,
),
PushKind.response.name: lang.notificationResponse(inGroup), PushKind.response.name: lang.notificationResponse(inGroup),
PushKind.addedToGroup.name: PushKind.addedToGroup.name: lang.notificationAddedToGroup(
lang.notificationAddedToGroup(pushNotification.additionalContent), pushNotification.additionalContent,
),
}; };
return pushNotificationText[pushNotification.kind.name] ?? ''; return pushNotificationText[pushNotification.kind.name] ?? '';

View file

@ -3,6 +3,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -41,22 +42,31 @@ Future<void> checkForTokenUpdates() async {
Log.error('Could not get fcm token'); Log.error('Could not get fcm token');
return; return;
} }
Log.info('Loaded fcm token');
Log.info('Loaded FCM token.');
if (storedToken == null || fcmToken != storedToken) { if (storedToken == null || fcmToken != storedToken) {
Log.info('Got new FCM TOKEN.');
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
await updateUserdata((u) { await updateUserdata((u) {
u.updateFCMToken = true; u.updateFCMToken = true;
return u; return u;
}); });
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
} }
FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) async { FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
Log.info('Got new FCM TOKEN.');
await storage.write(
key: SecureStorageKeys.googleFcm,
value: fcmToken,
);
await updateUserdata((u) { await updateUserdata((u) {
u.updateFCMToken = true; u.updateFCMToken = true;
return u; return u;
}); });
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken); })
}).onError((err) { .onError((err) {
Log.error('could not listen on token refresh'); Log.error('could not listen on token refresh');
}); });
} catch (e) { } catch (e) {
@ -64,21 +74,35 @@ Future<void> checkForTokenUpdates() async {
} }
} }
Future<void> initFCMAfterAuthenticated() async { Future<void> initFCMAfterAuthenticated({bool force = false}) async {
if (gUser.updateFCMToken) { if (gUser.updateFCMToken || force) {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
if (storedToken != null) { if (storedToken != null) {
final res = await apiService.updateFCMToken(storedToken); final res = await apiService.updateFCMToken(storedToken);
if (res.isSuccess) { if (res.isSuccess) {
Log.info('Uploaded new fmt token!'); Log.info('Uploaded new FCM token!');
await updateUserdata((u) { await updateUserdata((u) {
u.updateFCMToken = false; u.updateFCMToken = false;
return u; return u;
}); });
} else {
Log.error('Could not update FCM token!');
}
} else {
Log.error('Could not send FCM update to server as token is empty.');
} }
} }
} }
Future<void> resetFCMTokens() async {
await FirebaseInstallations.instance.delete();
Log.info('Firebase Installation successfully deleted.');
await FirebaseMessaging.instance.deleteToken();
Log.info('Old FCM deleted.');
await const FlutterSecureStorage().delete(key: SecureStorageKeys.googleFcm);
await checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
} }
Future<void> initFCMService() async { Future<void> initFCMService() async {
@ -86,36 +110,24 @@ Future<void> initFCMService() async {
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
unawaited(checkForTokenUpdates()); await checkForTokenUpdates();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// You may set the permission requests to "provisional" which allows the user to choose what type
// of notifications they would like to receive once the user receives a notification.
// final notificationSettings =
// await FirebaseMessaging.instance.requestPermission(provisional: true);
await FirebaseMessaging.instance.requestPermission(); await FirebaseMessaging.instance.requestPermission();
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
// if (Platform.isIOS) {
// final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
// if (apnsToken == null) {
// return;
// }
// }
FirebaseMessaging.onMessage.listen(handleRemoteMessage); FirebaseMessaging.onMessage.listen(handleRemoteMessage);
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
initLogger(); final isInitialized = await initBackgroundExecution();
// Log.info('Handling a background message: ${message.messageId}'); Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message); await handleRemoteMessage(message);
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (await initBackgroundExecution()) { if (isInitialized) {
await handlePeriodicTask(); await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
} }
} else { } else {
// make sure every thing run... // make sure every thing run...
@ -140,7 +152,11 @@ Future<void> handleRemoteMessage(RemoteMessage message) async {
final body = final body =
message.notification?.body ?? message.data['body'] as String? ?? ''; message.notification?.body ?? message.data['body'] as String? ?? '';
await customLocalPushNotification(title, body); await customLocalPushNotification(title, body);
} else if (message.data['push_data'] != null) {
await handlePushData(message.data['push_data'] as String);
} }
// 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);
// }
} }

View file

@ -49,13 +49,15 @@ Future<void> setupNotificationWithUsers({
final contacts = await twonlyDB.contactsDao.getAllContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
final pushUser = final pushUser = pushUsers.firstWhereOrNull(
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); (x) => x.userId == contact.userId,
);
if (pushUser != null && pushUser.pushKeys.isNotEmpty) { if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
// make it harder to predict the change of the key // make it harder to predict the change of the key
final timeBefore = final timeBefore = clock.now().subtract(
clock.now().subtract(Duration(days: 10 + random.nextInt(5))); Duration(days: 10 + random.nextInt(5)),
);
final lastKey = pushUser.pushKeys.last; final lastKey = pushUser.pushKeys.last;
final createdAt = DateTime.fromMillisecondsSinceEpoch( final createdAt = DateTime.fromMillisecondsSinceEpoch(
lastKey.createdAtUnixTimestamp.toInt(), lastKey.createdAtUnixTimestamp.toInt(),
@ -197,7 +199,7 @@ Future<void> updateLastMessageId(int fromUserId, String messageId) async {
} }
Future<PushNotification?> getPushNotificationFromEncryptedContent( Future<PushNotification?> getPushNotificationFromEncryptedContent(
int toUserId, int? toUserId,
String? messageId, String? messageId,
EncryptedContent content, EncryptedContent content,
) async { ) async {
@ -210,7 +212,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageById(content.reaction.targetMessageId) .getMessageById(content.reaction.targetMessageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg == null || msg.senderId == null || msg.senderId != toUserId) { if (msg == null || msg.senderId != toUserId) {
return null; return null;
} }
if (msg.content != null) { if (msg.content != null) {
@ -285,7 +287,7 @@ Future<PushNotification?> getPushNotificationFromEncryptedContent(
.getMessageById(content.reaction.targetMessageId) .getMessageById(content.reaction.targetMessageId)
.getSingleOrNull(); .getSingleOrNull();
// These notifications should only be send to the original sender. // These notifications should only be send to the original sender.
if (msg == null || msg.senderId == null || msg.senderId != toUserId) { if (msg == null || msg.senderId != toUserId) {
return null; return null;
} }
switch (content.mediaUpdate.type) { switch (content.mediaUpdate.type) {

View file

@ -9,9 +9,11 @@ final StreamController<NotificationResponse> selectNotificationStream =
@pragma('vm:entry-point') @pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) { void notificationTapBackground(NotificationResponse notificationResponse) {
// ignore: avoid_print // ignore: avoid_print
print('notification(${notificationResponse.id}) action tapped: ' print(
'notification(${notificationResponse.id}) action tapped: '
'${notificationResponse.actionId} with' '${notificationResponse.actionId} with'
' payload: ${notificationResponse.payload}'); ' payload: ${notificationResponse.payload}',
);
if (notificationResponse.input?.isNotEmpty ?? false) { if (notificationResponse.input?.isNotEmpty ?? false) {
// ignore: avoid_print // ignore: avoid_print
print( print(
@ -26,8 +28,9 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
int id = 0; int id = 0;
Future<void> setupPushNotification() async { Future<void> setupPushNotification() async {
const initializationSettingsAndroid = const initializationSettingsAndroid = AndroidInitializationSettings(
AndroidInitializationSettings('ic_launcher_foreground'); 'ic_launcher_foreground',
);
final darwinNotificationCategories = <DarwinNotificationCategory>[]; final darwinNotificationCategories = <DarwinNotificationCategory>[];

View file

@ -22,8 +22,9 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
Future<void> signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
if (gUser.signalLastSignedPreKeyUpdated != null) { if (gUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours = final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter(
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); fortyEightHoursAgo,
);
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated. // The key does live for 48 hours then it expires and a new key is generated.
return; return;
@ -76,8 +77,9 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async { Future<SignalIdentity?> getSignalIdentity() async {
try { try {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
var signalIdentityJson = var signalIdentityJson = await storage.read(
await storage.read(key: SecureStorageKeys.signalIdentity); key: SecureStorageKeys.signalIdentity,
);
if (signalIdentityJson == null) { if (signalIdentityJson == null) {
return null; return null;
} }
@ -104,13 +106,17 @@ Future<void> createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair(); final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true); final registrationId = generateRegistrationId(true);
final signalStore = final signalStore = ConnectSignalProtocolStore(
ConnectSignalProtocolStore(identityKeyPair, registrationId); identityKeyPair,
registrationId,
);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId); final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
await signalStore.signedPreKeyStore await signalStore.signedPreKeyStore.storeSignedPreKey(
.storeSignedPreKey(signedPreKey.id, signedPreKey); signedPreKey.id,
signedPreKey,
);
final storedSignalIdentity = SignalIdentity( final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(), identityKeyPairU8List: identityKeyPair.serialize(),

View file

@ -46,8 +46,9 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
final tempIdentityKey = IdentityKey( final tempIdentityKey = IdentityKey(
Curve.decodePoint( Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(userData.publicIdentityKey)) DjbECPublicKey(
.serialize(), Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1, 1,
), ),
); );

View file

@ -11,8 +11,9 @@ Future<ConnectSignalProtocolStore?> getSignalStore() async {
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity( Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
SignalIdentity signalIdentity, SignalIdentity signalIdentity,
) async { ) async {
final identityKeyPair = final identityKeyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List); signalIdentity.identityKeyPairU8List,
);
return ConnectSignalProtocolStore( return ConnectSignalProtocolStore(
identityKeyPair, identityKeyPair,

View file

@ -0,0 +1,6 @@
import 'dart:ui';
class DefaultColors {
static const messageSelf = Color.fromARGB(255, 58, 136, 102);
static const messageOther = Color.fromARGB(233, 68, 137, 255);
}

View file

@ -8,8 +8,11 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.dart'; import 'package:twonly/src/utils/exclusive_access.dart';
bool _isInitialized = false;
void initLogger() { void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; if (_isInitialized) return;
_isInitialized = true;
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) async { Logger.root.onRecord.listen((record) async {
unawaited(_writeLogToFile(record)); unawaited(_writeLogToFile(record));
@ -126,17 +129,34 @@ Future<void> cleanLogFile() async {
return _protectFileAccess(() async { return _protectFileAccess(() async {
final logFile = File('$globalApplicationSupportDirectory/app.log'); final logFile = File('$globalApplicationSupportDirectory/app.log');
if (logFile.existsSync()) { if (!logFile.existsSync()) {
return;
}
final lines = await logFile.readAsLines(); final lines = await logFile.readAsLines();
if (lines.length <= 10000) return; final twoWeekAgo = clock.now().subtract(const Duration(days: 14));
var keepStartIndex = -1;
final removeCount = lines.length - 10000; for (var i = 0; i < lines.length; i += 100) {
final remaining = lines.sublist(removeCount, lines.length); if (lines[i].length >= 19) {
final date = DateTime.tryParse(lines[i].substring(0, 19));
if (date != null && date.isAfter(twoWeekAgo)) {
keepStartIndex = i;
break;
}
}
}
if (keepStartIndex == 0) return;
if (keepStartIndex == -1) {
await logFile.writeAsString('');
return;
}
final remaining = lines.sublist(keepStartIndex);
final sink = logFile.openWrite()..writeAll(remaining, '\n'); final sink = logFile.openWrite()..writeAll(remaining, '\n');
await sink.close(); await sink.close();
}
}); });
} }

View file

@ -224,13 +224,17 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
borderSide: borderSide: BorderSide(
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2), color: Theme.of(context).colorScheme.primary,
width: 2,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
borderSide: borderSide: BorderSide(
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2), color: Theme.of(context).colorScheme.primary,
width: 2,
),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@ -253,11 +257,13 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
final now = clock.now(); final now = clock.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()) final date = DateFormat.yMd(
.format(dateTime); Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()) final time = DateFormat.Hm(
.format(dateTime); Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
if (difference.inDays == 0) { if (difference.inDays == 0) {
return time; return time;
@ -359,18 +365,21 @@ String friendlyDateTime(
Locale? locale, Locale? locale,
}) { }) {
// Build date part // Build date part
final datePart = final datePart = DateFormat.yMd(
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
var timePart = ''; var timePart = '';
if (use24Hour) { if (use24Hour) {
timePart = timePart = DateFormat.jm(
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
} else { } else {
timePart = timePart = DateFormat.Hm(
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
} }
return '$timePart $datePart'; return '$timePart $datePart';

View file

@ -19,8 +19,9 @@ Future<Uint8List> getProfileQrCodeData() async {
final publicProfile = PublicProfile( final publicProfile = PublicProfile(
userId: Int64(gUser.userId), userId: Int64(gUser.userId),
username: gUser.username, username: gUser.username,
publicIdentityKey: publicIdentityKey: (await signalStore.getIdentityKeyPair())
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize(), .getPublicKey()
.serialize(),
registrationId: Int64(signalIdentity.registrationId), registrationId: Int64(signalIdentity.registrationId),
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(), signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
signedPrekeySignature: signedPreKey.signature, signedPrekeySignature: signedPreKey.signature,

View file

@ -22,8 +22,9 @@ Future<bool> isUserCreated() async {
Future<UserData?> getUser() async { Future<UserData?> getUser() async {
try { try {
final userJson = await const FlutterSecureStorage() final userJson = await const FlutterSecureStorage().read(
.read(key: SecureStorageKeys.userData); key: SecureStorageKeys.userData,
);
if (userJson == null) { if (userJson == null) {
return null; return null;
} }
@ -64,8 +65,10 @@ Future<UserData?> updateUserdata(
user.defaultShowTime = null; user.defaultShowTime = null;
} }
final updated = updateUser(user); final updated = updateUser(user);
await const FlutterSecureStorage() await const FlutterSecureStorage().write(
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); key: SecureStorageKeys.userData,
value: jsonEncode(updated),
);
gUser = updated; gUser = updated;
return updated; return updated;
}); });

View file

@ -34,9 +34,15 @@ class MainCameraPreview extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: mainCameraController width: mainCameraController
.cameraController!.value.previewSize!.height, .cameraController!
.value
.previewSize!
.height,
height: mainCameraController height: mainCameraController
.cameraController!.value.previewSize!.width, .cameraController!
.value
.previewSize!
.width,
child: CameraPreview( child: CameraPreview(
key: mainCameraController.cameraPreviewKey, key: mainCameraController.cameraPreviewKey,
mainCameraController.cameraController!, mainCameraController.cameraController!,
@ -67,9 +73,15 @@ class MainCameraPreview extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: mainCameraController width: mainCameraController
.cameraController!.value.previewSize!.height, .cameraController!
.value
.previewSize!
.height,
height: mainCameraController height: mainCameraController
.cameraController!.value.previewSize!.width, .cameraController!
.value
.previewSize!
.width,
child: Stack( child: Stack(
children: [ children: [
Positioned( Positioned(

View file

@ -31,7 +31,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.
import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/camera/share_image_editor.view.dart';
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -181,8 +181,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
// Maybe this is the reason? // Maybe this is the reason?
return; return;
} else { } else {
androidVolumeDownSub = androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((
FlutterAndroidVolumeKeydown.stream.listen((event) { event,
) {
if (widget.isVisible) { if (widget.isVisible) {
takePicture(); takePicture();
} else { } else {
@ -297,8 +298,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return; return;
} }
final image = await mc.screenshotController final image = await mc.screenshotController.capture(
.capture(pixelRatio: MediaQuery.of(context).devicePixelRatio); pixelRatio: MediaQuery.of(context).devicePixelRatio,
);
if (await pushMediaEditor(image, null)) { if (await pushMediaEditor(image, null)) {
return; return;
@ -314,7 +316,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
bool sharedFromGallery = false, bool sharedFromGallery = false,
MediaType? mediaType, MediaType? mediaType,
}) async { }) async {
final type = mediaType ?? final type =
mediaType ??
((videoFilePath != null) ? MediaType.video : MediaType.image); ((videoFilePath != null) ? MediaType.video : MediaType.image);
final mediaFileService = await initializeMediaUpload( final mediaFileService = await initializeMediaUpload(
type, type,
@ -340,7 +343,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await deInitVolumeControl(); await deInitVolumeControl();
if (!mounted) return true; if (!mounted) return true;
final shouldReturn = await Navigator.push( final shouldReturn =
await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
@ -352,13 +356,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mainCameraController: mc, mainCameraController: mc,
previewLink: mc.sharedLinkForPreview, previewLink: mc.sharedLinkForPreview,
), ),
transitionsBuilder: (context, animation, secondaryAnimation, child) { transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return child; return child;
}, },
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero,
), ),
) as bool?; )
as bool?;
if (mounted) { if (mounted) {
setState(() { setState(() {
mc.isSharePreviewIsShown = false; mc.isSharePreviewIsShown = false;
@ -396,13 +402,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return; return;
} }
mc.selectedCameraDetails.scaleFactor = (_baseScaleFactor + mc.selectedCameraDetails.scaleFactor =
(_baseScaleFactor +
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
(_basePanY - (details.localPosition.dy as double)) / 30) (_basePanY - (details.localPosition.dy as double)) / 30)
.clamp(1, mc.selectedCameraDetails.maxAvailableZoom); .clamp(1, mc.selectedCameraDetails.maxAvailableZoom);
await mc.cameraController! await mc.cameraController!.setZoomLevel(
.setZoomLevel(mc.selectedCameraDetails.scaleFactor); mc.selectedCameraDetails.scaleFactor,
);
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
@ -434,8 +442,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
ScreenshotImage? image; ScreenshotImage? image;
MediaType? mediaType; MediaType? mediaType;
final isImage = final isImage = imageExtensions.any(
imageExtensions.any((ext) => pickedFile.name.contains(ext)); (ext) => pickedFile.name.contains(ext),
);
if (isImage) { if (isImage) {
if (pickedFile.name.contains('.gif')) { if (pickedFile.name.contains('.gif')) {
mediaType = MediaType.gif; mediaType = MediaType.gif;
@ -497,10 +506,15 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mc.isVideoRecording = true; mc.isVideoRecording = true;
}); });
if (mc.selectedCameraDetails.isFlashOn) {
await mc.cameraController?.setFlashMode(FlashMode.torch);
}
try { try {
await mc.cameraController?.startVideoRecording(); await mc.cameraController?.startVideoRecording();
_videoRecordingTimer = _videoRecordingTimer = Timer.periodic(const Duration(milliseconds: 15), (
Timer.periodic(const Duration(milliseconds: 15), (timer) { timer,
) {
setState(() { setState(() {
_currentTime = clock.now(); _currentTime = clock.now();
}); });
@ -521,6 +535,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mc.isVideoRecording = false; mc.isVideoRecording = false;
}); });
_showCameraException(e); _showCameraException(e);
await mc.cameraController?.setFlashMode(FlashMode.off);
return; return;
} }
} }
@ -531,6 +546,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_videoRecordingTimer = null; _videoRecordingTimer = null;
} }
await mc.cameraController?.setFlashMode(FlashMode.off);
setState(() { setState(() {
_videoRecordingStarted = null; _videoRecordingStarted = null;
mc.isVideoRecording = false; mc.isVideoRecording = false;
@ -601,8 +618,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
keyTriggerButton.currentContext!.findRenderObject()! as RenderBox; keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
final localPosition = renderBox.globalToLocal(details.globalPosition); final localPosition = renderBox.globalToLocal(details.globalPosition);
final containerRect = final containerRect = Rect.fromLTWH(
Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); 0,
0,
renderBox.size.width,
renderBox.size.height,
);
if (containerRect.contains(localPosition)) { if (containerRect.contains(localPosition)) {
startVideoRecording(); startVideoRecording();
@ -676,12 +697,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
: Colors.white.withAlpha(160), : Colors.white.withAlpha(160),
onPressed: () async { onPressed: () async {
if (mc.selectedCameraDetails.isFlashOn) { if (mc.selectedCameraDetails.isFlashOn) {
await mc.cameraController await mc.cameraController?.setFlashMode(
?.setFlashMode(FlashMode.off); FlashMode.off,
);
mc.selectedCameraDetails.isFlashOn = false; mc.selectedCameraDetails.isFlashOn = false;
} else { } else {
await mc.cameraController await mc.cameraController?.setFlashMode(
?.setFlashMode(FlashMode.always); FlashMode.always,
);
mc.selectedCameraDetails.isFlashOn = true; mc.selectedCameraDetails.isFlashOn = true;
} }
setState(() {}); setState(() {});
@ -786,7 +809,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
mc.isSelectingFaceFilters mc.isSelectingFaceFilters
? mc.currentFilterType.index == ? mc.currentFilterType.index ==
FaceFilterType FaceFilterType
.values.length - .values
.length -
1 1
? FontAwesomeIcons.xmark ? FontAwesomeIcons.xmark
: FontAwesomeIcons.arrowRight : FontAwesomeIcons.arrowRight
@ -936,10 +960,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
: 'assets/animations/failed.lottie', : 'assets/animations/failed.lottie',
repeat: false, repeat: false,
onLoaded: (p0) { onLoaded: (p0) {
Future.delayed(const Duration(seconds: 4), Future.delayed(
const Duration(seconds: 4),
() { () {
widget.mainCameraController.setState(); widget.mainCameraController
}); .setState();
},
);
}, },
), ),
), ),

View file

@ -15,7 +15,8 @@ extension FaceFilterTypeExtension on FaceFilterType {
} }
FaceFilterType goLeft() { FaceFilterType goLeft() {
final prevIndex = (index - 1 + FaceFilterType.values.length) % final prevIndex =
(index - 1 + FaceFilterType.values.length) %
FaceFilterType.values.length; FaceFilterType.values.length;
return FaceFilterType.values[prevIndex]; return FaceFilterType.values[prevIndex];
} }

View file

@ -4,6 +4,7 @@ import 'package:camera/camera.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
@ -133,6 +134,11 @@ class MainCameraController {
await cameraController?.initialize(); await cameraController?.initialize();
await cameraController?.startImageStream(_processCameraImage); await cameraController?.startImageStream(_processCameraImage);
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
if (gUser.videoStabilizationEnabled && !kDebugMode) {
await cameraController?.setVideoStabilizationMode(
VideoStabilizationMode.level1,
);
}
} else { } else {
try { try {
if (!isVideoRecording) { if (!isVideoRecording) {

View file

@ -159,8 +159,12 @@ class BeardFilterPainter extends FaceFilterPainter {
..rotate(rotation) ..rotate(rotation)
..scale(scaleX, Platform.isAndroid ? -1 : 1); ..scale(scaleX, Platform.isAndroid ? -1 : 1);
final srcRect = final srcRect = Rect.fromLTWH(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); 0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final aspectRatio = image.width / image.height; final aspectRatio = image.width / image.height;
final dstWidth = width; final dstWidth = width;

View file

@ -56,8 +56,9 @@ class DogFilterPainter extends FaceFilterPainter {
final points = faceContour.points; final points = faceContour.points;
if (points.isEmpty) continue; if (points.isEmpty) continue;
final upperPoints = final upperPoints = points
points.where((p) => p.y < noseBase.position.y).toList(); .where((p) => p.y < noseBase.position.y)
.toList();
if (upperPoints.isEmpty) continue; if (upperPoints.isEmpty) continue;
@ -186,8 +187,12 @@ class DogFilterPainter extends FaceFilterPainter {
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1); canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
} }
final srcRect = final srcRect = Rect.fromLTWH(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); 0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final aspectRatio = image.width / image.height; final aspectRatio = image.width / image.height;
final dstWidth = size; final dstWidth = size;
final dstHeight = size / aspectRatio; final dstHeight = size / aspectRatio;

View file

@ -52,7 +52,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
await widget.storeImageAsOriginal!(); await widget.storeImageAsOriginal!();
} }
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia( final newMediaFile = await twonlyDB.mediaFilesDao
.insertOrUpdateMedia(
MediaFilesCompanion( MediaFilesCompanion(
type: Value(widget.mediaService.mediaFile.type), type: Value(widget.mediaService.mediaFile.type),
createdAt: Value(clock.now()), createdAt: Value(clock.now()),

View file

@ -26,7 +26,8 @@ class VideoRecordingTimer extends StatelessWidget {
children: [ children: [
Center( Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: currentTime value:
currentTime
.difference(videoRecordingStarted!) .difference(videoRecordingStarted!)
.inMilliseconds / .inMilliseconds /
(maxVideoRecordingTime * 1000), (maxVideoRecordingTime * 1000),

View file

@ -51,8 +51,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
Future<void> initAsync() async { Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
var index = var index = gCameras.indexWhere(
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide); (t) => t.lensType == CameraLensType.ultraWide,
);
if (index == -1) { if (index == -1) {
index = gCameras.indexWhere( index = gCameras.indexWhere(
(t) => t.lensType == CameraLensType.wide, (t) => t.lensType == CameraLensType.wide,
@ -62,7 +63,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
_wideCameraIndex = index; _wideCameraIndex = index;
} }
final isFront = widget.controller.description.lensDirection == final isFront =
widget.controller.description.lensDirection ==
CameraLensDirection.front; CameraLensDirection.front;
if (!showWideAngleZoom && if (!showWideAngleZoom &&
@ -94,10 +96,12 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
); );
const zoomTextStyle = TextStyle(fontSize: 13); const zoomTextStyle = TextStyle(fontSize: 13);
final isSmallerFocused = widget.scaleFactor < 1 || final isSmallerFocused =
widget.scaleFactor < 1 ||
(showWideAngleZoomIOS && (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex); widget.selectedCameraDetails.cameraId == _wideCameraIndex);
final isMiddleFocused = widget.scaleFactor >= 1 && final isMiddleFocused =
widget.scaleFactor >= 1 &&
widget.scaleFactor < 2 && widget.scaleFactor < 2 &&
!(showWideAngleZoomIOS && !(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex); widget.selectedCameraDetails.cameraId == _wideCameraIndex);
@ -107,8 +111,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
widget.scaleFactor, widget.scaleFactor,
); );
final minLevel = final minLevel = beautifulZoomScale(
beautifulZoomScale(widget.selectedCameraDetails.minAvailableZoom); widget.selectedCameraDetails.minAvailableZoom,
);
final currentLevel = beautifulZoomScale(widget.scaleFactor); final currentLevel = beautifulZoomScale(widget.scaleFactor);
return Center( return Center(
child: ClipRRect( child: ClipRRect(
@ -173,9 +178,10 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
), ),
), ),
onPressed: () async { onPressed: () async {
final level = final level = min(
min(await widget.controller.getMaxZoomLevel(), 2) await widget.controller.getMaxZoomLevel(),
.toDouble(); 2,
).toDouble();
if (showWideAngleZoomIOS && if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == widget.selectedCameraDetails.cameraId ==

View file

@ -55,8 +55,9 @@ class _ShareImageView extends State<ShareImageView> {
void initState() { void initState() {
super.initState(); super.initState();
allGroupSub = allGroupSub = twonlyDB.groupsDao.watchGroupsForShareImage().listen((
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async { allGroups,
) async {
setState(() { setState(() {
contacts = allGroups; contacts = allGroups;
}); });
@ -86,8 +87,9 @@ class _ShareImageView extends State<ShareImageView> {
groups.sort((a, b) { groups.sort((a, b) {
// First, compare by flameCounter // First, compare by flameCounter
final flameComparison = final flameComparison = getFlameCounterFromGroup(
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a)); b,
).compareTo(getFlameCounterFromGroup(a));
if (flameComparison != 0) { if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order return flameComparison; // Sort by flameCounter in descending order
} }
@ -156,8 +158,12 @@ class _ShareImageView extends State<ShareImageView> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -211,8 +217,9 @@ class _ShareImageView extends State<ShareImageView> {
return const BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
color: color: Theme.of(
Theme.of(context).colorScheme.outline, context,
).colorScheme.outline,
); );
}, },
), ),
@ -254,8 +261,10 @@ class _ShareImageView extends State<ShareImageView> {
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: context.color.primary, width: 2), color: context.color.primary,
width: 2,
),
color: context.color.primary, color: context.color.primary,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@ -336,8 +345,9 @@ class UserList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically // Step 1: Sort the users alphabetically
groups groups.sort(
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); (a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
);
return ListView.builder( return ListView.builder(
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',

View file

@ -42,8 +42,10 @@ class BestFriendsSelector extends StatelessWidget {
} }
}, },
child: Container( child: Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 7, vertical: 4), horizontal: 7,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline.withAlpha(50), color: Theme.of(context).colorScheme.outline.withAlpha(50),
boxShadow: const [ boxShadow: const [
@ -75,8 +77,9 @@ class BestFriendsSelector extends StatelessWidget {
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
key: ValueKey(groups[firstUserIndex]), key: ValueKey(groups[firstUserIndex]),
isChecked: selectedGroupIds isChecked: selectedGroupIds.contains(
.contains(groups[firstUserIndex].groupId), groups[firstUserIndex].groupId,
),
group: groups[firstUserIndex], group: groups[firstUserIndex],
onChanged: updateSelectedGroupIds, onChanged: updateSelectedGroupIds,
), ),
@ -85,8 +88,9 @@ class BestFriendsSelector extends StatelessWidget {
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
key: ValueKey(groups[secondUserIndex]), key: ValueKey(groups[secondUserIndex]),
isChecked: selectedGroupIds isChecked: selectedGroupIds.contains(
.contains(groups[secondUserIndex].groupId), groups[secondUserIndex].groupId,
),
group: groups[secondUserIndex], group: groups[secondUserIndex],
onChanged: updateSelectedGroupIds, onChanged: updateSelectedGroupIds,
), ),

View file

@ -109,13 +109,21 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
sendingOrLoadingImage = false; sendingOrLoadingImage = false;
loadingImage = false; loadingImage = false;
}); });
videoController = VideoPlayerController.file(mediaService.originalPath); videoController = VideoPlayerController.file(
mediaService.originalPath,
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: true,
),
);
videoController?.setLooping(true); videoController?.setLooping(true);
videoController?.initialize().then((_) async { videoController
?.initialize()
.then((_) async {
await videoController!.play(); await videoController!.play();
setState(() {}); setState(() {});
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler })
}).catchError(Log.error); // ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
.catchError(Log.error);
} }
} }
@ -205,8 +213,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
List<Widget> get actionsAtTheRight { List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty && if (layers.isNotEmpty &&
layers.last.isEditing && (layers.first.isEditing ||
layers.last.hasCustomActionButtons) { (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
return []; return [];
} }
return <Widget>[ return <Widget>[
@ -246,13 +254,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Icons.add_reaction_outlined, Icons.add_reaction_outlined,
tooltipText: context.lang.addEmoji, tooltipText: context.lang.addEmoji,
onPressed: () async { onPressed: () async {
final layer = await showModalBottomSheet( final layer =
await showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
builder: (context) { builder: (context) {
return const EmojiPickerBottom(); return const EmojiPickerBottom();
}, },
) as Layer?; )
as Layer?;
if (layer == null) return; if (layer == null) return;
undoLayers.clear(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
@ -277,7 +287,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: _setImageDisplayTime, onPressed: _setImageDisplayTime,
), ),
), ),
if (media.type == MediaType.video) if (media.type == MediaType.video) ...[
const SizedBox(height: 8),
ActionButton( ActionButton(
(mediaService.removeAudio) (mediaService.removeAudio)
? Icons.volume_off_rounded ? Icons.volume_off_rounded
@ -296,6 +307,29 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (mounted) setState(() {}); if (mounted) setState(() {});
}, },
), ),
],
if (media.type == MediaType.image) ...[
const SizedBox(height: 8),
ActionButton(
Icons.crop_rotate_outlined,
tooltipText: 'Crop or rotate image',
color: Colors.white,
onPressed: () async {
final first = layers.first;
if (first is BackgroundLayerData) {
first.isEditing = !first.isEditing;
}
setState(() {});
// await mediaService.toggleRemoveAudio();
// if (mediaService.removeAudio) {
// await videoController?.setVolume(0);
// } else {
// await videoController?.setVolume(100);
// }
// if (mounted) setState(() {});
},
),
],
const SizedBox(height: 8), const SizedBox(height: 8),
ActionButton( ActionButton(
FontAwesomeIcons.shieldHeart, FontAwesomeIcons.shieldHeart,
@ -348,8 +382,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
List<Widget> get actionsAtTheTop { List<Widget> get actionsAtTheTop {
if (layers.isNotEmpty && if (layers.isNotEmpty &&
layers.last.isEditing && (layers.first.isEditing ||
layers.last.hasCustomActionButtons) { (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
return []; return [];
} }
return [ return [
@ -411,7 +445,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
await videoController?.pause(); await videoController?.pause();
if (isDisposed || !mounted) return; if (isDisposed || !mounted) return;
final wasSend = await Navigator.push( final wasSend =
await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
@ -422,7 +457,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
additionalData: getAdditionalData(), additionalData: getAdditionalData(),
), ),
), ),
) as bool?; )
as bool?;
if (wasSend != null && wasSend && mounted) { if (wasSend != null && wasSend && mounted) {
widget.mainCameraController?.onImageSend(); widget.mainCameraController?.onImageSend();
Navigator.pop(context, true); Navigator.pop(context, true);
@ -471,7 +507,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mediaService.tempPath.deleteSync(); mediaService.tempPath.deleteSync();
} }
if (mediaService.originalPath.existsSync()) { if (mediaService.originalPath.existsSync()) {
if (media.type != MediaType.video) { if (media.type == MediaType.image) {
mediaService.originalPath.deleteSync(); mediaService.originalPath.deleteSync();
} }
} }
@ -480,8 +516,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (media.type == MediaType.gif) { if (media.type == MediaType.gif) {
if (bytes != null) { if (bytes != null) {
mediaService.originalPath.writeAsBytesSync(bytes.toList()); mediaService.originalPath.writeAsBytesSync(bytes.toList());
} else {
Log.error('Could not load image bytes for gif!');
} }
} else { } else {
image = await getEditedImageBytes(); image = await getEditedImageBytes();
@ -552,8 +586,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}); });
// It is important that the user can sending the image only when the image is fully loaded otherwise if the user // It is important that the user can sending the image only when the image is fully loaded otherwise if the user
// will click on send before the image is painted the screenshot will be transparent.. // will click on send before the image is painted the screenshot will be transparent..
_imageLoadingTimer = _imageLoadingTimer = Timer.periodic(const Duration(milliseconds: 10), (
Timer.periodic(const Duration(milliseconds: 10), (timer) { timer,
) {
final imageLayer = layers.first; final imageLayer = layers.first;
if (imageLayer is BackgroundLayerData) { if (imageLayer is BackgroundLayerData) {
if (imageLayer.imageLoaded) { if (imageLayer.imageLoaded) {
@ -619,8 +654,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
await askToCloseThenClose(); await askToCloseThenClose();
}, },
child: Scaffold( child: Scaffold(
backgroundColor: backgroundColor: widget.sharedFromGallery
widget.sharedFromGallery ? null : Colors.white.withAlpha(0), ? null
: Colors.white.withAlpha(0),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
fit: StackFit.expand, fit: StackFit.expand,
@ -667,8 +703,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: Theme.of(context).colorScheme.primary, iconColor: Theme.of(context).colorScheme.primary,
foregroundColor: foregroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
).colorScheme.primary,
), ),
onPressed: pushShareImageView, onPressed: pushShareImageView,
child: const FaIcon(FontAwesomeIcons.userPlus), child: const FaIcon(FontAwesomeIcons.userPlus),
@ -681,9 +718,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
width: 12, width: 12,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Theme.of(context) color: Theme.of(
.colorScheme context,
.inversePrimary, ).colorScheme.inversePrimary,
), ),
) )
: const FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),

View file

@ -1,6 +1,10 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:photo_view/photo_view.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
class BackgroundLayer extends StatefulWidget { class BackgroundLayer extends StatefulWidget {
@ -29,7 +33,17 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scImage = widget.layerData.image.image; final scImage = widget.layerData.image.image;
if (scImage == null || scImage.image == null) return Container(); if (scImage == null || scImage.image == null) return Container();
return Container( return Stack(
children: [
Positioned.fill(
child: PhotoView.customChild(
enableRotation: true,
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
backgroundDecoration: const BoxDecoration(
color: Colors.transparent,
),
child: Container(
width: widget.layerData.image.width.toDouble(), width: widget.layerData.image.width.toDouble(),
height: widget.layerData.image.height.toDouble(), height: widget.layerData.image.height.toDouble(),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -37,6 +51,29 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
child: CustomPaint( child: CustomPaint(
painter: UiImagePainter(scImage.image!), painter: UiImagePainter(scImage.image!),
), ),
),
),
),
if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
Positioned(
top: 5,
left: 5,
right: 50,
child: Row(
children: [
ActionButton(
FontAwesomeIcons.check,
tooltipText: context.lang.imageEditorDrawOk,
onPressed: () async {
widget.layerData.isEditing = false;
widget.onUpdate!();
setState(() {});
},
),
],
),
),
],
); );
} }
} }

View file

@ -91,8 +91,10 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialScale = widget.layerData.size; initialScale = widget.layerData.size;
initialRotation = widget.layerData.rotation; initialRotation = widget.layerData.rotation;
initialOffset = widget.layerData.offset; initialOffset = widget.layerData.offset;
initialFocalPoint = initialFocalPoint = Offset(
Offset(details.focalPoint.dx, details.focalPoint.dy); details.focalPoint.dx,
details.focalPoint.dy,
);
setState(() {}); setState(() {});
}, },
@ -100,8 +102,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
if (twoPointerWhereDown && details.pointerCount != 2) { if (twoPointerWhereDown && details.pointerCount != 2) {
return; return;
} }
final outlineBox = outlineKey.currentContext! final outlineBox =
.findRenderObject()! as RenderBox; outlineKey.currentContext!.findRenderObject()!
as RenderBox;
final emojiBox = final emojiBox =
emojiKey.currentContext!.findRenderObject()! as RenderBox; emojiKey.currentContext!.findRenderObject()! as RenderBox;
@ -133,9 +136,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialRotation + details.rotation; initialRotation + details.rotation;
// Update the position based on the translation // Update the position based on the translation
final dx = (initialOffset.dx) + final dx =
(initialOffset.dx) +
(details.focalPoint.dx - initialFocalPoint.dx); (details.focalPoint.dx - initialFocalPoint.dx);
final dy = (initialOffset.dy) + final dy =
(initialOffset.dy) +
(details.focalPoint.dy - initialFocalPoint.dy); (details.focalPoint.dy - initialFocalPoint.dy);
widget.layerData.offset = Offset(dx, dy); widget.layerData.offset = Offset(dx, dy);
}); });
@ -203,7 +208,8 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
Future<void> _captureEmoji() async { Future<void> _captureEmoji() async {
try { try {
final boundary = _boundaryKey.currentContext?.findRenderObject() final boundary =
_boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?; as RenderRepaintBoundary?;
if (boundary == null) return; if (boundary == null) return;

View file

@ -145,8 +145,9 @@ Future<List<Sticker>> getStickerIndex() async {
} }
} }
try { try {
final response = await http final response = await http.get(
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json')); Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
await indexFile.writeAsString(response.body); await indexFile.writeAsString(response.body);
final jsonList = json.decode(response.body) as List; final jsonList = json.decode(response.body) as List;

View file

@ -6,7 +6,7 @@ import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/c
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
class LinkPreviewLayer extends StatefulWidget { class LinkPreviewLayer extends StatefulWidget {
const LinkPreviewLayer({ const LinkPreviewLayer({
@ -32,8 +32,9 @@ class _LinkPreviewLayerState extends State<LinkPreviewLayer> {
Future<void> initAsync() async { Future<void> initAsync() async {
if (widget.layerData.metadata == null) { if (widget.layerData.metadata == null) {
widget.layerData.metadata = widget.layerData.metadata = await getMetadata(
await getMetadata(widget.layerData.link.toString()); widget.layerData.link.toString(),
);
if (widget.layerData.metadata == null) { if (widget.layerData.metadata == null) {
widget.layerData.error = true; widget.layerData.error = true;
} }

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
class MastodonPostCard extends StatelessWidget { class MastodonPostCard extends StatelessWidget {
const MastodonPostCard({required this.info, super.key}); const MastodonPostCard({required this.info, super.key});

View file

@ -6,8 +6,10 @@ class MastodonParser with BaseMetaInfo {
final Document? _document; final Document? _document;
@override @override
Vendor? get vendor => ((_document?.head?.innerHtml Vendor? get vendor =>
.contains('"repository":"mastodon/mastodon"') ?? ((_document?.head?.innerHtml.contains(
'"repository":"mastodon/mastodon"',
) ??
false) && false) &&
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false)) (_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
? Vendor.mastodonSocialMediaPosting ? Vendor.mastodonSocialMediaPosting

View file

@ -43,7 +43,8 @@ class _TextViewState extends State<TextLayer> {
if (parentBox != null) { if (parentBox != null) {
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy; final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
final screenHeight = mq.size.height; final screenHeight = mq.size.height;
localBottom = (screenHeight - globalDesiredBottom) - localBottom =
(screenHeight - globalDesiredBottom) -
parentTopGlobal - parentTopGlobal -
(parentBox.size.height); (parentBox.size.height);
} }
@ -87,7 +88,8 @@ class _TextViewState extends State<TextLayer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.layerData.isDeleted) return Container(); if (widget.layerData.isDeleted) return Container();
final bottom = MediaQuery.of(context).viewInsets.bottom + final bottom =
MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).viewPadding.bottom; MediaQuery.of(context).viewPadding.bottom;
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered. // On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
@ -181,7 +183,8 @@ class _TextViewState extends State<TextLayer> {
} }
setState(() {}); setState(() {});
}, },
onTap: (context onTap:
(context
.watch<ImageEditorProvider>() .watch<ImageEditorProvider>()
.someTextViewIsAlreadyEditing) .someTextViewIsAlreadyEditing)
? null ? null

View file

@ -23,11 +23,15 @@ class LayersViewer extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
...layers.whereType<BackgroundLayerData>().map((layerItem) { ...layers.whereType<BackgroundLayerData>().map((layerItem) {
if (!layerItem.isEditing) {
return BackgroundLayer( return BackgroundLayer(
key: layerItem.key, key: layerItem.key,
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} else {
return Container();
}
}), }),
...layers.whereType<FilterLayerData>().map((layerItem) { ...layers.whereType<FilterLayerData>().map((layerItem) {
return FilterLayer( return FilterLayer(
@ -71,6 +75,17 @@ class LayersViewer extends StatelessWidget {
} }
return Container(); return Container();
}), }),
...layers.whereType<BackgroundLayerData>().map((layerItem) {
if (layerItem.isEditing) {
return BackgroundLayer(
key: layerItem.key,
layerData: layerItem,
onUpdate: onUpdate,
);
} else {
return Container();
}
}),
], ],
); );
} }

View file

@ -4,7 +4,9 @@ import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
@ -131,7 +133,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.searchUsernameTitle), title: Text(context.lang.addFriendTitle),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
@ -140,6 +142,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
Expanded(
child: TextField( child: TextField(
onSubmitted: (_) async { onSubmitted: (_) async {
await _addNewUser(context); await _addNewUser(context);
@ -152,11 +157,25 @@ class _SearchUsernameView extends State<AddNewUserView> {
}, },
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(12), LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')), FilteringTextInputFormatter.allow(
RegExp('[a-z0-9A-Z._]'),
),
], ],
controller: searchUserName, controller: searchUserName,
decoration: decoration: getInputDecoration(
getInputDecoration(context.lang.searchUsernameInput), context.lang.searchUsernameInput,
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () =>
context.push(Routes.settingsPublicProfile),
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
),
],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -174,7 +193,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: _isLoading ? null : () async => _addNewUser(context), onPressed: _isLoading || searchUserName.text.isEmpty
? null
: () async => _addNewUser(context),
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus), : const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),

View file

@ -9,15 +9,14 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/connection_status_badge.dart';
import 'package:twonly/src/views/components/notification_badge.dart'; import 'package:twonly/src/views/components/notification_badge.dart';
class ChatListView extends StatefulWidget { class ChatListView extends StatefulWidget {
@ -45,8 +44,9 @@ class _ChatListViewState extends State<ChatListView> {
final stream = twonlyDB.groupsDao.watchGroupsForChatList(); final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_contactsSub = stream.listen((groups) { _contactsSub = stream.listen((groups) {
setState(() { setState(() {
_groupsNotPinned = _groupsNotPinned = groups
groups.where((x) => !x.pinned && !x.archived).toList(); .where((x) => !x.pinned && !x.archived)
.toList();
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
_groupsArchived = groups.where((x) => x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList();
}); });
@ -64,8 +64,10 @@ class _ChatListViewState extends State<ChatListView> {
} }
final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash = final changeLogHash = (await compute(
(await compute(Sha256().hash, changeLog.codeUnits)).bytes; Sha256().hash,
changeLog.codeUnits,
)).bytes;
if (!gUser.hideChangeLog && if (!gUser.hideChangeLog &&
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
await updateUserdata((u) { await updateUserdata((u) {
@ -93,13 +95,13 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected;
final plan = context.watch<PurchasesProvider>().plan; final plan = context.watch<PurchasesProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
children: [ children: [
GestureDetector( ConnectionStatusBadge(
child: GestureDetector(
onTap: () async { onTap: () async {
await context.push(Routes.settingsProfile); await context.push(Routes.settingsProfile);
if (!mounted) return; if (!mounted) return;
@ -111,6 +113,7 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.onSurface.withAlpha(20), color: context.color.onSurface.withAlpha(20),
), ),
), ),
),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text('twonly '), const Text('twonly '),
if (plan != SubscriptionPlan.Free) if (plan != SubscriptionPlan.Free)
@ -121,8 +124,10 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.primary, color: context.color.primary,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 5, vertical: 3), horizontal: 5,
vertical: 3,
),
child: Text( child: Text(
plan.name, plan.name,
style: TextStyle( style: TextStyle(
@ -163,22 +168,14 @@ class _ChatListViewState extends State<ChatListView> {
), ),
], ],
), ),
body: Stack( body: RefreshIndicator(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: isConnected ? Container() : const ConnectionInfo(),
),
Positioned.fill(
child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(); await apiService.connect();
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },
child: (_groupsNotPinned.isEmpty && child:
(_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty && _groupsPinned.isEmpty &&
_groupsArchived.isEmpty) _groupsArchived.isEmpty)
? Center( ? Center(
@ -194,7 +191,8 @@ class _ChatListViewState extends State<ChatListView> {
), ),
) )
: ListView.builder( : ListView.builder(
itemCount: _groupsPinned.length + itemCount:
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) + (_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length + _groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0), (_groupsArchived.isNotEmpty ? 1 : 0),
@ -242,9 +240,6 @@ class _ChatListViewState extends State<ChatListView> {
}, },
), ),
), ),
),
],
),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: Column( child: Column(

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