mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:42:54 +00:00
Merge pull request #395 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
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
This commit is contained in:
commit
1b7ec19769
48 changed files with 1563 additions and 761 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,5 +1,20 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,19 @@ 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) &
|
t.emoji.equals(emoji),
|
||||||
t.emoji.equals(emoji),
|
))
|
||||||
))
|
|
||||||
.go();
|
.go();
|
||||||
} else {
|
} else {
|
||||||
await into(reactions).insertOnConflictUpdate(
|
await into(reactions).insertOnConflictUpdate(
|
||||||
|
|
@ -63,18 +63,18 @@ 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) &
|
t.emoji.equals(emoji),
|
||||||
t.emoji.equals(emoji),
|
))
|
||||||
))
|
|
||||||
.go();
|
.go();
|
||||||
if (!remove) {
|
if (!remove) {
|
||||||
await into(reactions).insert(
|
await into(reactions).insert(
|
||||||
|
|
@ -98,20 +98,19 @@ 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,
|
messages.messageId.equalsExp(reactions.messageId),
|
||||||
messages.messageId.equalsExp(reactions.messageId),
|
useColumns: false,
|
||||||
useColumns: false,
|
),
|
||||||
),
|
],
|
||||||
],
|
)
|
||||||
)
|
..where(messages.groupId.equals(groupId))
|
||||||
..where(messages.groupId.equals(groupId))
|
..orderBy([OrderingTerm.desc(messages.createdAt)])
|
||||||
// ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
..limit(1);
|
||||||
..limit(1);
|
|
||||||
return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
|
return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 284c602b507e77addc8f21c4fc8a321f237cac1b
|
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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? ?? false
|
||||||
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
||||||
..showShowImagePreviewWhenSending =
|
..showShowImagePreviewWhenSending =
|
||||||
json['showShowImagePreviewWhenSending'] as bool? ?? false
|
json['showShowImagePreviewWhenSending'] as bool? ?? false
|
||||||
|
|
@ -105,6 +107,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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,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;
|
||||||
await connect();
|
// only try to reconnect in case the app is in the foreground
|
||||||
|
if (!globalIsAppInBackground) {
|
||||||
|
await connect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_reconnectionDelay = 3;
|
_reconnectionDelay = 3;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,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 +290,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;
|
||||||
|
|
@ -329,10 +347,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) {
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
);
|
);
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -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,19 @@ 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.');
|
Log.warn('Got duplicated message from the server.');
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
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 +138,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 +163,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 +184,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 +211,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 +269,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 +346,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
relatedReceiptId: receiptId,
|
relatedReceiptId: receiptId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
|
|
@ -333,7 +367,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
PlaintextContent()
|
PlaintextContent()
|
||||||
..retryControlError = PlaintextContent_RetryErrorMessage()
|
..retryControlError = PlaintextContent_RetryErrorMessage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,7 +399,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
PlaintextContent()
|
PlaintextContent()
|
||||||
..retryControlError = PlaintextContent_RetryErrorMessage()
|
..retryControlError = PlaintextContent_RetryErrorMessage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
|
|
||||||
|
|
@ -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] ?? '';
|
||||||
|
|
|
||||||
|
|
@ -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,81 +42,92 @@ 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
|
||||||
await updateUserdata((u) {
|
.listen((fcmToken) async {
|
||||||
u.updateFCMToken = true;
|
Log.info('Got new FCM TOKEN.');
|
||||||
return u;
|
await storage.write(
|
||||||
});
|
key: SecureStorageKeys.googleFcm,
|
||||||
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
|
value: fcmToken,
|
||||||
}).onError((err) {
|
);
|
||||||
Log.error('could not listen on token refresh');
|
await updateUserdata((u) {
|
||||||
});
|
u.updateFCMToken = true;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.onError((err) {
|
||||||
|
Log.error('could not listen on token refresh');
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('could not load fcm token: $e');
|
Log.error('could not load fcm token: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
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);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
final lines = await logFile.readAsLines();
|
return;
|
||||||
|
|
||||||
if (lines.length <= 10000) return;
|
|
||||||
|
|
||||||
final removeCount = lines.length - 10000;
|
|
||||||
final remaining = lines.sublist(removeCount, lines.length);
|
|
||||||
|
|
||||||
final sink = logFile.openWrite()..writeAll(remaining, '\n');
|
|
||||||
await sink.close();
|
|
||||||
}
|
}
|
||||||
|
final lines = await logFile.readAsLines();
|
||||||
|
|
||||||
|
final twoWeekAgo = clock.now().subtract(const Duration(days: 14));
|
||||||
|
var keepStartIndex = -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < lines.length; i += 100) {
|
||||||
|
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');
|
||||||
|
await sink.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,25 +343,28 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
await deInitVolumeControl();
|
await deInitVolumeControl();
|
||||||
if (!mounted) return true;
|
if (!mounted) return true;
|
||||||
|
|
||||||
final shouldReturn = await Navigator.push(
|
final shouldReturn =
|
||||||
context,
|
await Navigator.push(
|
||||||
PageRouteBuilder(
|
context,
|
||||||
opaque: false,
|
PageRouteBuilder(
|
||||||
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
opaque: false,
|
||||||
screenshotImage: screenshotImage,
|
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
||||||
sharedFromGallery: sharedFromGallery,
|
screenshotImage: screenshotImage,
|
||||||
sendToGroup: widget.sendToGroup,
|
sharedFromGallery: sharedFromGallery,
|
||||||
mediaFileService: mediaFileService,
|
sendToGroup: widget.sendToGroup,
|
||||||
mainCameraController: mc,
|
mediaFileService: mediaFileService,
|
||||||
previewLink: mc.sharedLinkForPreview,
|
mainCameraController: mc,
|
||||||
),
|
previewLink: mc.sharedLinkForPreview,
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
),
|
||||||
return child;
|
transitionsBuilder:
|
||||||
},
|
(context, animation, secondaryAnimation, child) {
|
||||||
transitionDuration: Duration.zero,
|
return child;
|
||||||
reverseTransitionDuration: Duration.zero,
|
},
|
||||||
),
|
transitionDuration: Duration.zero,
|
||||||
) as bool?;
|
reverseTransitionDuration: Duration.zero,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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 =
|
||||||
// ignore: avoid_dynamic_calls
|
(_baseScaleFactor +
|
||||||
(_basePanY - (details.localPosition.dy as double)) / 30)
|
// ignore: avoid_dynamic_calls
|
||||||
.clamp(1, mc.selectedCameraDetails.maxAvailableZoom);
|
(_basePanY - (details.localPosition.dy as double)) / 30)
|
||||||
|
.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(() {});
|
||||||
|
|
@ -739,8 +762,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
mc.isSelectingFaceFilters
|
mc.isSelectingFaceFilters
|
||||||
? mc.currentFilterType.index == 1
|
? mc.currentFilterType.index == 1
|
||||||
? FontAwesomeIcons.xmark
|
? FontAwesomeIcons.xmark
|
||||||
: FontAwesomeIcons.arrowLeft
|
: FontAwesomeIcons.arrowLeft
|
||||||
: FontAwesomeIcons.photoFilm,
|
: FontAwesomeIcons.photoFilm,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 25,
|
size: 25,
|
||||||
|
|
@ -785,13 +808,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
mc.isSelectingFaceFilters
|
mc.isSelectingFaceFilters
|
||||||
? mc.currentFilterType.index ==
|
? mc.currentFilterType.index ==
|
||||||
FaceFilterType
|
FaceFilterType
|
||||||
.values.length -
|
.values
|
||||||
1
|
.length -
|
||||||
? FontAwesomeIcons.xmark
|
1
|
||||||
: FontAwesomeIcons.arrowRight
|
? FontAwesomeIcons.xmark
|
||||||
|
: FontAwesomeIcons.arrowRight
|
||||||
: FontAwesomeIcons
|
: FontAwesomeIcons
|
||||||
.faceGrinTongueSquint,
|
.faceGrinTongueSquint,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 25,
|
size: 25,
|
||||||
),
|
),
|
||||||
|
|
@ -843,64 +867,64 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
children: [
|
children: [
|
||||||
...widget.mainCameraController.scannedNewProfiles.values
|
...widget.mainCameraController.scannedNewProfiles.values
|
||||||
.map(
|
.map(
|
||||||
(c) {
|
(c) {
|
||||||
if (c.isLoading) return Container();
|
if (c.isLoading) return Container();
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
c.isLoading = true;
|
c.isLoading = true;
|
||||||
widget.mainCameraController.setState();
|
widget.mainCameraController.setState();
|
||||||
if (await addNewContactFromPublicProfile(
|
if (await addNewContactFromPublicProfile(
|
||||||
c.profile,
|
c.profile,
|
||||||
) &&
|
) &&
|
||||||
context.mounted) {
|
context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
context.lang.requestedUserToastText(
|
context.lang.requestedUserToastText(
|
||||||
c.profile.username,
|
c.profile.username,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 8),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
duration: const Duration(seconds: 8),
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: context.color.surfaceContainer,
|
||||||
),
|
),
|
||||||
);
|
child: Row(
|
||||||
}
|
children: [
|
||||||
|
Text(c.profile.username),
|
||||||
|
Expanded(child: Container()),
|
||||||
|
if (c.isLoading)
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ColoredBox(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.userPlus,
|
||||||
|
color: isDarkMode(context)
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
size: 17,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
),
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: context.color.surfaceContainer,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(c.profile.username),
|
|
||||||
Expanded(child: Container()),
|
|
||||||
if (c.isLoading)
|
|
||||||
const SizedBox(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ColoredBox(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.userPlus,
|
|
||||||
color: isDarkMode(context)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
size: 17,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
...widget.mainCameraController.contactsVerified.values.map(
|
...widget.mainCameraController.contactsVerified.values.map(
|
||||||
(c) {
|
(c) {
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -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();
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
await videoController!.play();
|
?.initialize()
|
||||||
setState(() {});
|
.then((_) async {
|
||||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
await videoController!.play();
|
||||||
}).catchError(Log.error);
|
setState(() {});
|
||||||
|
})
|
||||||
|
// 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 =
|
||||||
context: context,
|
await showModalBottomSheet(
|
||||||
backgroundColor: Colors.black,
|
context: context,
|
||||||
builder: (context) {
|
backgroundColor: Colors.black,
|
||||||
return const EmojiPickerBottom();
|
builder: (context) {
|
||||||
},
|
return const EmojiPickerBottom();
|
||||||
) as Layer?;
|
},
|
||||||
|
)
|
||||||
|
as Layer?;
|
||||||
if (layer == null) return;
|
if (layer == null) return;
|
||||||
undoLayers.clear();
|
undoLayers.clear();
|
||||||
removedLayers.clear();
|
removedLayers.clear();
|
||||||
|
|
@ -265,19 +275,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
count: (media.type == MediaType.video)
|
count: (media.type == MediaType.video)
|
||||||
? '0'
|
? '0'
|
||||||
: media.displayLimitInMilliseconds == null
|
: media.displayLimitInMilliseconds == null
|
||||||
? '∞'
|
? '∞'
|
||||||
: (media.displayLimitInMilliseconds! ~/ 1000).toString(),
|
: (media.displayLimitInMilliseconds! ~/ 1000).toString(),
|
||||||
child: ActionButton(
|
child: ActionButton(
|
||||||
(media.type == MediaType.video)
|
(media.type == MediaType.video)
|
||||||
? media.displayLimitInMilliseconds == null
|
? media.displayLimitInMilliseconds == null
|
||||||
? Icons.repeat_rounded
|
? Icons.repeat_rounded
|
||||||
: Icons.repeat_one_rounded
|
: Icons.repeat_one_rounded
|
||||||
: Icons.timer_outlined,
|
: Icons.timer_outlined,
|
||||||
tooltipText: context.lang.protectAsARealTwonly,
|
tooltipText: context.lang.protectAsARealTwonly,
|
||||||
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,18 +445,20 @@ 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 =
|
||||||
context,
|
await Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (context) => ShareImageView(
|
MaterialPageRoute(
|
||||||
selectedGroupIds: selectedGroupIds,
|
builder: (context) => ShareImageView(
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
selectedGroupIds: selectedGroupIds,
|
||||||
mediaStoreFuture: mediaStoreFuture,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
mediaFileService: mediaService,
|
mediaStoreFuture: mediaStoreFuture,
|
||||||
additionalData: getAdditionalData(),
|
mediaFileService: mediaService,
|
||||||
),
|
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),
|
||||||
|
|
|
||||||
|
|
@ -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,14 +33,47 @@ 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(
|
||||||
width: widget.layerData.image.width.toDouble(),
|
children: [
|
||||||
height: widget.layerData.image.height.toDouble(),
|
Positioned.fill(
|
||||||
padding: EdgeInsets.zero,
|
child: PhotoView.customChild(
|
||||||
color: Colors.transparent,
|
enableRotation: true,
|
||||||
child: CustomPaint(
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
painter: UiImagePainter(scImage.image!),
|
minScale: PhotoViewComputedScale.contained,
|
||||||
),
|
backgroundDecoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: widget.layerData.image.width.toDouble(),
|
||||||
|
height: widget.layerData.image.height.toDouble(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: UiImagePainter(scImage.image!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.layerData.isEditing)
|
||||||
|
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(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
return BackgroundLayer(
|
if (!layerItem.isEditing) {
|
||||||
key: layerItem.key,
|
return BackgroundLayer(
|
||||||
layerData: layerItem,
|
key: layerItem.key,
|
||||||
onUpdate: onUpdate,
|
layerData: layerItem,
|
||||||
);
|
onUpdate: onUpdate,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
...layers.whereType<FilterLayerData>().map((layerItem) {
|
...layers.whereType<FilterLayerData>().map((layerItem) {
|
||||||
return FilterLayer(
|
return FilterLayer(
|
||||||
|
|
@ -37,39 +41,50 @@ class LayersViewer extends StatelessWidget {
|
||||||
}),
|
}),
|
||||||
...layers
|
...layers
|
||||||
.where(
|
.where(
|
||||||
(layerItem) =>
|
(layerItem) =>
|
||||||
layerItem is EmojiLayerData ||
|
layerItem is EmojiLayerData ||
|
||||||
layerItem is DrawLayerData ||
|
layerItem is DrawLayerData ||
|
||||||
layerItem is LinkPreviewLayerData ||
|
layerItem is LinkPreviewLayerData ||
|
||||||
layerItem is TextLayerData,
|
layerItem is TextLayerData,
|
||||||
)
|
)
|
||||||
.map((layerItem) {
|
.map((layerItem) {
|
||||||
if (layerItem is EmojiLayerData) {
|
if (layerItem is EmojiLayerData) {
|
||||||
return EmojiLayer(
|
return EmojiLayer(
|
||||||
key: layerItem.key,
|
key: layerItem.key,
|
||||||
layerData: layerItem,
|
layerData: layerItem,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
);
|
);
|
||||||
} else if (layerItem is DrawLayerData) {
|
} else if (layerItem is DrawLayerData) {
|
||||||
return DrawLayer(
|
return DrawLayer(
|
||||||
key: layerItem.key,
|
key: layerItem.key,
|
||||||
layerData: layerItem,
|
layerData: layerItem,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
);
|
);
|
||||||
} else if (layerItem is TextLayerData) {
|
} else if (layerItem is TextLayerData) {
|
||||||
return TextLayer(
|
return TextLayer(
|
||||||
key: layerItem.key,
|
key: layerItem.key,
|
||||||
layerData: layerItem,
|
layerData: layerItem,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
);
|
);
|
||||||
} else if (layerItem is LinkPreviewLayerData) {
|
} else if (layerItem is LinkPreviewLayerData) {
|
||||||
return LinkPreviewLayer(
|
return LinkPreviewLayer(
|
||||||
|
key: layerItem.key,
|
||||||
|
layerData: layerItem,
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
}),
|
||||||
|
...layers.whereType<BackgroundLayerData>().map((layerItem) {
|
||||||
|
if (layerItem.isEditing) {
|
||||||
|
return BackgroundLayer(
|
||||||
key: layerItem.key,
|
key: layerItem.key,
|
||||||
layerData: layerItem,
|
layerData: layerItem,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
}
|
}
|
||||||
return Container();
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/message_send_sta
|
||||||
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/flame.dart';
|
import 'package:twonly/src/views/components/flame.dart';
|
||||||
import 'package:twonly/src/views/components/group_context_menu.component.dart';
|
import 'package:twonly/src/views/components/group_context_menu.component.dart';
|
||||||
|
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||||
|
|
||||||
class GroupListItem extends StatefulWidget {
|
class GroupListItem extends StatefulWidget {
|
||||||
const GroupListItem({
|
const GroupListItem({
|
||||||
|
|
@ -44,6 +45,7 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
List<Message> _previewMessages = [];
|
List<Message> _previewMessages = [];
|
||||||
final List<MediaFile> _previewMediaFiles = [];
|
final List<MediaFile> _previewMediaFiles = [];
|
||||||
bool _hasNonOpenedMediaFile = false;
|
bool _hasNonOpenedMediaFile = false;
|
||||||
|
bool _receiverDeletedAccount = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -60,45 +62,51 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void initStreams() {
|
Future<void> initStreams() async {
|
||||||
_lastMessageStream = twonlyDB.messagesDao
|
_lastMessageStream = twonlyDB.messagesDao
|
||||||
.watchLastMessage(widget.group.groupId)
|
.watchLastMessage(widget.group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
protectUpdateState.protect(() async {
|
protectUpdateState.protect(() async {
|
||||||
await updateState(update, _messagesNotOpened);
|
await updateState(update, _messagesNotOpened);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_lastReactionStream = twonlyDB.reactionsDao
|
_lastReactionStream = twonlyDB.reactionsDao
|
||||||
.watchLastReactions(widget.group.groupId)
|
.watchLastReactions(widget.group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_lastReaction = update;
|
_lastReaction = update;
|
||||||
});
|
});
|
||||||
// protectUpdateState.protect(() async {
|
});
|
||||||
// await updateState(lastMessage, update, messagesNotOpened);
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
|
|
||||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
_messagesNotOpenedStream = twonlyDB.messagesDao
|
||||||
.watchMessageNotOpened(widget.group.groupId)
|
.watchMessageNotOpened(widget.group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
protectUpdateState.protect(() async {
|
protectUpdateState.protect(() async {
|
||||||
await updateState(_lastMessage, update);
|
await updateState(_lastMessage, update);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_lastMediaFilesStream =
|
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
||||||
twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
.watchNewestMediaFiles()
|
||||||
for (final mediaFile in mediaFiles) {
|
.listen((mediaFiles) {
|
||||||
final index = _previewMediaFiles
|
for (final mediaFile in mediaFiles) {
|
||||||
.indexWhere((t) => t.mediaId == mediaFile.mediaId);
|
final index = _previewMediaFiles.indexWhere(
|
||||||
if (index >= 0) {
|
(t) => t.mediaId == mediaFile.mediaId,
|
||||||
_previewMediaFiles[index] = mediaFile;
|
);
|
||||||
}
|
if (index >= 0) {
|
||||||
}
|
_previewMediaFiles[index] = mediaFile;
|
||||||
setState(() {});
|
}
|
||||||
});
|
}
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
|
||||||
|
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
|
widget.group.groupId,
|
||||||
|
);
|
||||||
|
if (groupContacts.length == 1) {
|
||||||
|
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mutex protectUpdateState = Mutex();
|
Mutex protectUpdateState = Mutex();
|
||||||
|
|
@ -113,8 +121,9 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
_previewMessages = [];
|
_previewMessages = [];
|
||||||
} else if (newMessagesNotOpened.isNotEmpty) {
|
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||||
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||||
final receivedMessages =
|
final receivedMessages = newMessagesNotOpened
|
||||||
newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
.where((x) => x.senderId != null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (receivedMessages.isNotEmpty) {
|
if (receivedMessages.isNotEmpty) {
|
||||||
_previewMessages = receivedMessages;
|
_previewMessages = receivedMessages;
|
||||||
|
|
@ -125,8 +134,17 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// there are no not opened messages show just the last message in the table
|
// there are no not opened messages show just the last message in the table
|
||||||
_currentMessage = newLastMessage;
|
// only shows the last message in case there was no newer messages which already got deleted
|
||||||
_previewMessages = [newLastMessage];
|
// This prevents, that it will show that a images got stored 10 days ago...
|
||||||
|
if (newLastMessage.createdAt.isAfter(
|
||||||
|
widget.group.lastMessageExchange.subtract(const Duration(days: 2)),
|
||||||
|
)) {
|
||||||
|
_currentMessage = newLastMessage;
|
||||||
|
_previewMessages = [newLastMessage];
|
||||||
|
} else {
|
||||||
|
_currentMessage = null;
|
||||||
|
_previewMessages = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages
|
||||||
|
|
@ -145,8 +163,9 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
for (final message in _previewMessages) {
|
for (final message in _previewMessages) {
|
||||||
if (message.mediaId != null &&
|
if (message.mediaId != null &&
|
||||||
!_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
!_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||||
final mediaFile =
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
message.mediaId!,
|
||||||
|
);
|
||||||
if (mediaFile != null) {
|
if (mediaFile != null) {
|
||||||
_previewMediaFiles.add(mediaFile);
|
_previewMediaFiles.add(mediaFile);
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +190,9 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages
|
||||||
.where((x) => x.type == MessageType.media.name)
|
.where((x) => x.type == MessageType.media.name)
|
||||||
.toList();
|
.toList();
|
||||||
final mediaFile =
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
|
msgs.first.mediaId!,
|
||||||
|
);
|
||||||
if (mediaFile?.type != MediaType.audio) {
|
if (mediaFile?.type != MediaType.audio) {
|
||||||
if (mediaFile?.downloadState == null) return;
|
if (mediaFile?.downloadState == null) return;
|
||||||
if (mediaFile!.downloadState! == DownloadState.pending) {
|
if (mediaFile!.downloadState! == DownloadState.pending) {
|
||||||
|
|
@ -190,10 +210,7 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.push(
|
await context.push(Routes.chatsMessages(widget.group.groupId));
|
||||||
Routes.chatsMessages,
|
|
||||||
extra: widget.group,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -201,23 +218,35 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
return GroupContextMenu(
|
return GroupContextMenu(
|
||||||
group: widget.group,
|
group: widget.group,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(
|
title: Row(
|
||||||
substringBy(widget.group.groupName, 30),
|
children: [
|
||||||
|
Text(
|
||||||
|
substringBy(widget.group.groupName, 30),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 3),
|
||||||
|
VerifiedShield(
|
||||||
|
group: widget.group,
|
||||||
|
showOnlyIfVerified: true,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
subtitle: (_currentMessage == null)
|
subtitle: _receiverDeletedAccount
|
||||||
|
? Text(context.lang.userDeletedAccount)
|
||||||
|
: (_currentMessage == null)
|
||||||
? (widget.group.totalMediaCounter == 0)
|
? (widget.group.totalMediaCounter == 0)
|
||||||
? Text(context.lang.chatsTapToSend)
|
? Text(context.lang.chatsTapToSend)
|
||||||
: Row(
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
LastMessageTime(
|
LastMessageTime(
|
||||||
dateTime: widget.group.lastMessageExchange,
|
dateTime: widget.group.lastMessageExchange,
|
||||||
),
|
),
|
||||||
FlameCounterWidget(
|
FlameCounterWidget(
|
||||||
groupId: widget.group.groupId,
|
groupId: widget.group.groupId,
|
||||||
prefix: true,
|
prefix: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
MessageSendStateIcon(
|
MessageSendStateIcon(
|
||||||
|
|
@ -239,8 +268,9 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (widget.group.isDirectChat) {
|
if (widget.group.isDirectChat) {
|
||||||
final contacts = await twonlyDB.groupsDao
|
final contacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
.getGroupContact(widget.group.groupId);
|
widget.group.groupId,
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
await context.push(Routes.profileContact(contacts.first.userId));
|
await context.push(Routes.profileContact(contacts.first.userId));
|
||||||
return;
|
return;
|
||||||
|
|
@ -250,15 +280,19 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
},
|
},
|
||||||
child: AvatarIcon(group: widget.group),
|
child: AvatarIcon(group: widget.group),
|
||||||
),
|
),
|
||||||
trailing: (widget.group.leftGroup)
|
trailing: (widget.group.leftGroup || _receiverDeletedAccount)
|
||||||
? null
|
? null
|
||||||
: IconButton(
|
: IconButton(
|
||||||
onPressed: () => context.push(
|
onPressed: () {
|
||||||
_hasNonOpenedMediaFile
|
if (_hasNonOpenedMediaFile) {
|
||||||
? Routes.chatsMessages
|
context.push(Routes.chatsMessages(widget.group.groupId));
|
||||||
: Routes.chatsCameraSendTo,
|
} else {
|
||||||
extra: widget.group,
|
context.push(
|
||||||
),
|
Routes.chatsCameraSendTo,
|
||||||
|
extra: widget.group,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
icon: FaIcon(
|
icon: FaIcon(
|
||||||
_hasNonOpenedMediaFile
|
_hasNonOpenedMediaFile
|
||||||
? FontAwesomeIcons.solidComments
|
? FontAwesomeIcons.solidComments
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/memory_item.model.dart';
|
import 'package:twonly/src/model/memory_item.model.dart';
|
||||||
import 'package:twonly/src/services/api/messages.dart';
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
||||||
|
|
@ -23,40 +24,11 @@ import 'package:twonly/src/views/components/blink.component.dart';
|
||||||
import 'package:twonly/src/views/components/flame.dart';
|
import 'package:twonly/src/views/components/flame.dart';
|
||||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||||
|
|
||||||
Color getMessageColor(Message message) {
|
|
||||||
return (message.senderId == null)
|
|
||||||
? const Color.fromARGB(255, 58, 136, 102)
|
|
||||||
: const Color.fromARGB(233, 68, 137, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatItem {
|
|
||||||
const ChatItem._({
|
|
||||||
this.message,
|
|
||||||
this.date,
|
|
||||||
this.groupAction,
|
|
||||||
});
|
|
||||||
factory ChatItem.date(DateTime date) {
|
|
||||||
return ChatItem._(date: date);
|
|
||||||
}
|
|
||||||
factory ChatItem.message(Message message) {
|
|
||||||
return ChatItem._(message: message);
|
|
||||||
}
|
|
||||||
factory ChatItem.groupAction(GroupHistory groupAction) {
|
|
||||||
return ChatItem._(groupAction: groupAction);
|
|
||||||
}
|
|
||||||
final GroupHistory? groupAction;
|
|
||||||
final Message? message;
|
|
||||||
final DateTime? date;
|
|
||||||
bool get isMessage => message != null;
|
|
||||||
bool get isDate => date != null;
|
|
||||||
bool get isGroupAction => groupAction != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays detailed information about a SampleItem.
|
/// Displays detailed information about a SampleItem.
|
||||||
class ChatMessagesView extends StatefulWidget {
|
class ChatMessagesView extends StatefulWidget {
|
||||||
const ChatMessagesView(this.group, {super.key});
|
const ChatMessagesView(this.groupId, {super.key});
|
||||||
|
|
||||||
final Group group;
|
final String groupId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMessagesView> createState() => _ChatMessagesViewState();
|
State<ChatMessagesView> createState() => _ChatMessagesViewState();
|
||||||
|
|
@ -64,12 +36,13 @@ class ChatMessagesView extends StatefulWidget {
|
||||||
|
|
||||||
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
||||||
late Group group;
|
|
||||||
late StreamSubscription<Group?> userSub;
|
late StreamSubscription<Group?> userSub;
|
||||||
late StreamSubscription<List<Message>> messageSub;
|
late StreamSubscription<List<Message>> messageSub;
|
||||||
StreamSubscription<List<GroupHistory>>? groupActionsSub;
|
StreamSubscription<List<GroupHistory>>? groupActionsSub;
|
||||||
StreamSubscription<List<Contact>>? contactSub;
|
StreamSubscription<List<Contact>>? contactSub;
|
||||||
|
|
||||||
|
Group? _group;
|
||||||
|
|
||||||
Map<int, Contact> userIdToContact = {};
|
Map<int, Contact> userIdToContact = {};
|
||||||
|
|
||||||
List<ChatItem> messages = [];
|
List<ChatItem> messages = [];
|
||||||
|
|
@ -81,11 +54,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
late FocusNode textFieldFocus;
|
late FocusNode textFieldFocus;
|
||||||
final ItemScrollController itemScrollController = ItemScrollController();
|
final ItemScrollController itemScrollController = ItemScrollController();
|
||||||
int? focusedScrollItem;
|
int? focusedScrollItem;
|
||||||
|
bool _receiverDeletedAccount = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
group = widget.group;
|
|
||||||
textFieldFocus = FocusNode();
|
textFieldFocus = FocusNode();
|
||||||
initStreams();
|
initStreams();
|
||||||
}
|
}
|
||||||
|
|
@ -102,36 +75,47 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
Mutex protectMessageUpdating = Mutex();
|
Mutex protectMessageUpdating = Mutex();
|
||||||
|
|
||||||
Future<void> initStreams() async {
|
Future<void> initStreams() async {
|
||||||
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
|
final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId);
|
||||||
userSub = groupStream.listen((newGroup) {
|
userSub = groupStream.listen((newGroup) {
|
||||||
if (newGroup == null) return;
|
if (newGroup == null) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
group = newGroup;
|
_group = newGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
protectMessageUpdating.protect(() async {
|
||||||
|
if (groupActionsSub == null && !newGroup.isDirectChat) {
|
||||||
|
final actionsStream = twonlyDB.groupsDao.watchGroupActions(
|
||||||
|
newGroup.groupId,
|
||||||
|
);
|
||||||
|
groupActionsSub = actionsStream.listen((update) async {
|
||||||
|
groupActions = update;
|
||||||
|
await setMessages(allMessages, update);
|
||||||
|
});
|
||||||
|
|
||||||
|
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
||||||
|
contactSub = contactsStream.listen((contacts) {
|
||||||
|
for (final contact in contacts) {
|
||||||
|
userIdToContact[contact.userId] = contact;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!widget.group.isDirectChat) {
|
final msgStream = twonlyDB.messagesDao.watchByGroupId(widget.groupId);
|
||||||
final actionsStream = twonlyDB.groupsDao.watchGroupActions(group.groupId);
|
|
||||||
groupActionsSub = actionsStream.listen((update) async {
|
|
||||||
groupActions = update;
|
|
||||||
await setMessages(allMessages, update);
|
|
||||||
});
|
|
||||||
|
|
||||||
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
|
||||||
contactSub = contactsStream.listen((contacts) {
|
|
||||||
for (final contact in contacts) {
|
|
||||||
userIdToContact[contact.userId] = contact;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
|
||||||
messageSub = msgStream.listen((update) async {
|
messageSub = msgStream.listen((update) async {
|
||||||
allMessages = update;
|
allMessages = update;
|
||||||
await protectMessageUpdating.protect(() async {
|
await protectMessageUpdating.protect(() async {
|
||||||
await setMessages(update, groupActions);
|
await setMessages(update, groupActions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
|
widget.groupId,
|
||||||
|
);
|
||||||
|
if (groupContacts.length == 1) {
|
||||||
|
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setMessages(
|
Future<void> setMessages(
|
||||||
|
|
@ -153,8 +137,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
if (groupHistoryIndex < groupActions.length) {
|
if (groupHistoryIndex < groupActions.length) {
|
||||||
for (; groupHistoryIndex < groupActions.length; groupHistoryIndex++) {
|
for (; groupHistoryIndex < groupActions.length; groupHistoryIndex++) {
|
||||||
if (msg.createdAt.isAfter(groupActions[groupHistoryIndex].actionAt)) {
|
if (msg.createdAt.isAfter(groupActions[groupHistoryIndex].actionAt)) {
|
||||||
chatItems
|
chatItems.add(
|
||||||
.add(ChatItem.groupAction(groupActions[groupHistoryIndex]));
|
ChatItem.groupAction(groupActions[groupHistoryIndex]),
|
||||||
|
);
|
||||||
// groupHistoryIndex++;
|
// groupHistoryIndex++;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|
@ -230,6 +215,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_group == null) return Container();
|
||||||
|
final group = _group!;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => FocusScope.of(context).unfocus(),
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|
@ -237,12 +224,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (group.isDirectChat) {
|
if (group.isDirectChat) {
|
||||||
final member =
|
final member = await twonlyDB.groupsDao.getAllGroupMembers(
|
||||||
await twonlyDB.groupsDao.getAllGroupMembers(group.groupId);
|
group.groupId,
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (member.isEmpty) return;
|
if (member.isEmpty) return;
|
||||||
await context
|
await context.push(
|
||||||
.push(Routes.profileContact(member.first.contactId));
|
Routes.profileContact(member.first.contactId),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await context.push(Routes.profileGroup(group.groupId));
|
await context.push(Routes.profileGroup(group.groupId));
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +343,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!group.leftGroup)
|
if (!group.leftGroup && !_receiverDeletedAccount)
|
||||||
MessageInput(
|
MessageInput(
|
||||||
group: group,
|
group: group,
|
||||||
quotesMessage: quotesMessage,
|
quotesMessage: quotesMessage,
|
||||||
|
|
@ -365,6 +354,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (_receiverDeletedAccount)
|
||||||
|
Text(context.lang.userDeletedAccount),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -372,3 +363,32 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color getMessageColor(Message message) {
|
||||||
|
return (message.senderId == null)
|
||||||
|
? const Color.fromARGB(255, 58, 136, 102)
|
||||||
|
: const Color.fromARGB(233, 68, 137, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatItem {
|
||||||
|
const ChatItem._({
|
||||||
|
this.message,
|
||||||
|
this.date,
|
||||||
|
this.groupAction,
|
||||||
|
});
|
||||||
|
factory ChatItem.date(DateTime date) {
|
||||||
|
return ChatItem._(date: date);
|
||||||
|
}
|
||||||
|
factory ChatItem.message(Message message) {
|
||||||
|
return ChatItem._(message: message);
|
||||||
|
}
|
||||||
|
factory ChatItem.groupAction(GroupHistory groupAction) {
|
||||||
|
return ChatItem._(groupAction: groupAction);
|
||||||
|
}
|
||||||
|
final GroupHistory? groupAction;
|
||||||
|
final Message? message;
|
||||||
|
final DateTime? date;
|
||||||
|
bool get isMessage => message != null;
|
||||||
|
bool get isDate => date != null;
|
||||||
|
bool get isGroupAction => groupAction != null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> asyncLoadNextMedia(bool firstRun) async {
|
Future<void> asyncLoadNextMedia(bool firstRun) async {
|
||||||
final messages =
|
final messages = twonlyDB.messagesDao.watchMediaNotOpened(
|
||||||
twonlyDB.messagesDao.watchMediaNotOpened(widget.group.groupId);
|
widget.group.groupId,
|
||||||
|
);
|
||||||
|
|
||||||
_subscription = messages.listen((messages) async {
|
_subscription = messages.listen((messages) async {
|
||||||
for (final msg in messages) {
|
for (final msg in messages) {
|
||||||
|
|
@ -121,8 +122,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
|
|
||||||
/// If the messages was already there just replace it and go to the next...
|
/// If the messages was already there just replace it and go to the next...
|
||||||
|
|
||||||
final index =
|
final index = allMediaFiles.indexWhere(
|
||||||
allMediaFiles.indexWhere((m) => m.messageId == msg.messageId);
|
(m) => m.messageId == msg.messageId,
|
||||||
|
);
|
||||||
|
|
||||||
if (index >= 1) {
|
if (index >= 1) {
|
||||||
allMediaFiles[index] = msg;
|
allMediaFiles[index] = msg;
|
||||||
|
|
@ -160,7 +162,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
if (group != null &&
|
if (group != null &&
|
||||||
group.draftMessage != null &&
|
group.draftMessage != null &&
|
||||||
group.draftMessage != '') {
|
group.draftMessage != '') {
|
||||||
context.replace(Routes.chatsMessages, extra: group);
|
context.replace(Routes.chatsMessages(group.groupId));
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
@ -190,8 +192,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
|
|
||||||
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
unawaited(flutterLocalNotificationsPlugin.cancelAll());
|
||||||
|
|
||||||
final stream =
|
final stream = twonlyDB.mediaFilesDao.watchMedia(
|
||||||
twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!);
|
allMediaFiles.first.mediaId!,
|
||||||
|
);
|
||||||
|
|
||||||
var downloadTriggered = false;
|
var downloadTriggered = false;
|
||||||
|
|
||||||
|
|
@ -204,8 +207,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
});
|
});
|
||||||
if (!downloadTriggered) {
|
if (!downloadTriggered) {
|
||||||
downloadTriggered = true;
|
downloadTriggered = true;
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
.getMediaFileById(allMediaFiles.first.mediaId!);
|
allMediaFiles.first.mediaId!,
|
||||||
|
);
|
||||||
if (mediaFile == null) return;
|
if (mediaFile == null) return;
|
||||||
await startDownloadMedia(mediaFile, true);
|
await startDownloadMedia(mediaFile, true);
|
||||||
unawaited(tryDownloadAllMediaFiles(force: true));
|
unawaited(tryDownloadAllMediaFiles(force: true));
|
||||||
|
|
@ -226,8 +230,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showDownloadingLoader = false;
|
_showDownloadingLoader = false;
|
||||||
});
|
});
|
||||||
final currentMediaLocal =
|
final currentMediaLocal = await MediaFileService.fromMediaId(
|
||||||
await MediaFileService.fromMediaId(allMediaFiles.first.mediaId!);
|
allMediaFiles.first.mediaId!,
|
||||||
|
);
|
||||||
if (currentMediaLocal == null || !mounted) return;
|
if (currentMediaLocal == null || !mounted) return;
|
||||||
|
|
||||||
if (currentMediaLocal.mediaFile.requiresAuthentication) {
|
if (currentMediaLocal.mediaFile.requiresAuthentication) {
|
||||||
|
|
@ -259,8 +264,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!widget.group.isDirectChat) {
|
if (!widget.group.isDirectChat) {
|
||||||
final sender =
|
final sender = await twonlyDB.contactsDao.getContactById(
|
||||||
await twonlyDB.contactsDao.getContactById(currentMessage!.senderId!);
|
currentMessage!.senderId!,
|
||||||
|
);
|
||||||
if (sender != null) {
|
if (sender != null) {
|
||||||
_currentMediaSender =
|
_currentMediaSender =
|
||||||
'${getContactDisplayName(sender)} (${widget.group.groupName})';
|
'${getContactDisplayName(sender)} (${widget.group.groupName})';
|
||||||
|
|
@ -281,36 +287,49 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
var timerRequired = false;
|
var timerRequired = false;
|
||||||
|
|
||||||
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
||||||
videoController = VideoPlayerController.file(currentMediaLocal.tempPath);
|
videoController = VideoPlayerController.file(
|
||||||
|
currentMediaLocal.tempPath,
|
||||||
|
videoPlayerOptions: VideoPlayerOptions(
|
||||||
|
// only mix in case the video can be played multiple times,
|
||||||
|
// otherwise stop the background music in case the video contains audio
|
||||||
|
mixWithOthers:
|
||||||
|
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||||
|
),
|
||||||
|
);
|
||||||
await videoController?.setLooping(
|
await videoController?.setLooping(
|
||||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||||
);
|
);
|
||||||
await videoController?.initialize().then((_) {
|
await videoController
|
||||||
if (videoController == null) return;
|
?.initialize()
|
||||||
videoController?.play();
|
.then((_) {
|
||||||
videoController?.addListener(() {
|
if (videoController == null) return;
|
||||||
setState(() {
|
videoController?.play();
|
||||||
progress = 1 -
|
videoController?.addListener(() {
|
||||||
videoController!.value.position.inSeconds /
|
setState(() {
|
||||||
videoController!.value.duration.inSeconds;
|
progress =
|
||||||
});
|
1 -
|
||||||
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
|
videoController!.value.position.inSeconds /
|
||||||
if (videoController?.value.position ==
|
videoController!.value.duration.inSeconds;
|
||||||
videoController?.value.duration) {
|
});
|
||||||
nextMediaOrExit();
|
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds !=
|
||||||
}
|
null) {
|
||||||
}
|
if (videoController?.value.position ==
|
||||||
});
|
videoController?.value.duration) {
|
||||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
nextMediaOrExit();
|
||||||
}).catchError(Log.error);
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
|
||||||
|
.catchError(Log.error);
|
||||||
} else {
|
} else {
|
||||||
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
|
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
|
||||||
canBeSeenUntil = clock.now().add(
|
canBeSeenUntil = clock.now().add(
|
||||||
Duration(
|
Duration(
|
||||||
milliseconds:
|
milliseconds:
|
||||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
|
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
timerRequired = true;
|
timerRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -434,8 +453,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
height: 8,
|
height: 8,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: EmojiAnimation(
|
child: EmojiAnimation(
|
||||||
emoji:
|
emoji: EmojiAnimation.animatedIcons.keys
|
||||||
EmojiAnimation.animatedIcons.keys.toList()[index],
|
.toList()[index],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,9 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
contactSub =
|
contactSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((
|
||||||
twonlyDB.contactsDao.watchAllAcceptedContacts().listen((update) async {
|
update,
|
||||||
|
) async {
|
||||||
update.sort(
|
update.sort(
|
||||||
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
|
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
|
||||||
);
|
);
|
||||||
|
|
@ -46,13 +47,14 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
await filterUsers();
|
await filterUsers();
|
||||||
});
|
});
|
||||||
|
|
||||||
allNonDirectGroupsSub =
|
allNonDirectGroupsSub = twonlyDB.groupsDao
|
||||||
twonlyDB.groupsDao.watchGroupsForStartNewChat().listen((update) async {
|
.watchGroupsForStartNewChat()
|
||||||
setState(() {
|
.listen((update) async {
|
||||||
allNonDirectGroups = update;
|
setState(() {
|
||||||
});
|
allNonDirectGroups = update;
|
||||||
await filterUsers();
|
});
|
||||||
});
|
await filterUsers();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -72,16 +74,16 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
}
|
}
|
||||||
final usersFiltered = allContacts
|
final usersFiltered = allContacts
|
||||||
.where(
|
.where(
|
||||||
(user) => getContactDisplayName(user)
|
(user) => getContactDisplayName(
|
||||||
.toLowerCase()
|
user,
|
||||||
.contains(searchUserName.value.text.toLowerCase()),
|
).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final groupsFiltered = allNonDirectGroups
|
final groupsFiltered = allNonDirectGroups
|
||||||
.where(
|
.where(
|
||||||
(g) => g.groupName
|
(g) => g.groupName.toLowerCase().contains(
|
||||||
.toLowerCase()
|
searchUserName.value.text.toLowerCase(),
|
||||||
.contains(searchUserName.value.text.toLowerCase()),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -108,7 +110,7 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ChatMessagesView(directChat!);
|
return ChatMessagesView(directChat!.groupId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -119,7 +121,7 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ChatMessagesView(group);
|
return ChatMessagesView(group.groupId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -133,8 +135,12 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
),
|
),
|
||||||
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(
|
||||||
|
|
@ -167,8 +173,9 @@ class _StartNewChatView extends State<StartNewChatView> {
|
||||||
size: 13,
|
size: 13,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => context
|
onTap: () => context.push(
|
||||||
.push(Routes.groupCreateSelectMember(null)),
|
Routes.groupCreateSelectMember(null),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (i == 1) {
|
if (i == 1) {
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,7 @@ class GroupContextMenu extends StatelessWidget {
|
||||||
),
|
),
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.contextMenuOpenChat,
|
title: context.lang.contextMenuOpenChat,
|
||||||
onTap: () => context.push(
|
onTap: () => context.push(Routes.chatsMessages(group.groupId)),
|
||||||
Routes.chatsMessages,
|
|
||||||
extra: group,
|
|
||||||
),
|
|
||||||
icon: FontAwesomeIcons.comments,
|
icon: FontAwesomeIcons.comments,
|
||||||
),
|
),
|
||||||
if (!group.archived)
|
if (!group.archived)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,14 @@ class VerifiedShield extends StatefulWidget {
|
||||||
this.group,
|
this.group,
|
||||||
super.key,
|
super.key,
|
||||||
this.size = 15,
|
this.size = 15,
|
||||||
|
this.showOnlyIfVerified = false,
|
||||||
});
|
});
|
||||||
final Group? group;
|
final Group? group;
|
||||||
final Contact? contact;
|
final Contact? contact;
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
|
final bool showOnlyIfVerified;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VerifiedShield> createState() => _VerifiedShieldState();
|
State<VerifiedShield> createState() => _VerifiedShieldState();
|
||||||
}
|
}
|
||||||
|
|
@ -33,13 +36,13 @@ class _VerifiedShieldState extends State<VerifiedShield> {
|
||||||
stream = twonlyDB.groupsDao
|
stream = twonlyDB.groupsDao
|
||||||
.watchGroupContact(widget.group!.groupId)
|
.watchGroupContact(widget.group!.groupId)
|
||||||
.listen((contacts) {
|
.listen((contacts) {
|
||||||
if (contacts.length == 1) {
|
if (contacts.length == 1) {
|
||||||
contact = contacts.first;
|
contact = contacts.first;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
isVerified = contacts.every((t) => t.verified);
|
isVerified = contacts.every((t) => t.verified);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (widget.contact != null) {
|
} else if (widget.contact != null) {
|
||||||
isVerified = widget.contact!.verified;
|
isVerified = widget.contact!.verified;
|
||||||
contact = widget.contact;
|
contact = widget.contact;
|
||||||
|
|
@ -56,19 +59,24 @@ class _VerifiedShieldState extends State<VerifiedShield> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!isVerified && widget.showOnlyIfVerified) return Container();
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: (contact == null)
|
onTap: (contact == null)
|
||||||
? null
|
? null
|
||||||
: () => context.push(Routes.settingsPublicProfile),
|
: () => context.push(Routes.settingsHelpFaqVerifyBadge),
|
||||||
child: Tooltip(
|
child: ColoredBox(
|
||||||
message: isVerified
|
color: Colors.transparent,
|
||||||
? 'You verified this contact'
|
|
||||||
: 'You have not verifies this contact.',
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsetsGeometry.only(top: 2),
|
padding: const EdgeInsetsGeometry.only(
|
||||||
|
top: 4,
|
||||||
|
left: 3,
|
||||||
|
right: 3,
|
||||||
|
bottom: 3,
|
||||||
|
),
|
||||||
child: SvgIcon(
|
child: SvgIcon(
|
||||||
assetPath:
|
assetPath: isVerified
|
||||||
isVerified ? SvgIcons.verifiedGreen : SvgIcons.verifiedRed,
|
? SvgIcons.verifiedGreen
|
||||||
|
: SvgIcons.verifiedRed,
|
||||||
size: widget.size,
|
size: widget.size,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,12 @@ class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = VideoPlayerController.file(widget.videoPath);
|
_controller = VideoPlayerController.file(
|
||||||
|
widget.videoPath,
|
||||||
|
videoPlayerOptions: VideoPlayerOptions(
|
||||||
|
mixWithOthers: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
_controller.initialize().then((_) async {
|
_controller.initialize().then((_) async {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import 'package:twonly/src/views/components/better_list_title.dart';
|
||||||
import 'package:twonly/src/views/components/flame.dart';
|
import 'package:twonly/src/views/components/flame.dart';
|
||||||
import 'package:twonly/src/views/components/max_flame_list_title.dart';
|
import 'package:twonly/src/views/components/max_flame_list_title.dart';
|
||||||
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
|
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
|
||||||
import 'package:twonly/src/views/components/svg_icon.dart';
|
|
||||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||||
import 'package:twonly/src/views/groups/group.view.dart';
|
import 'package:twonly/src/views/groups/group.view.dart';
|
||||||
|
|
||||||
|
|
@ -36,8 +35,9 @@ class _ContactViewState extends State<ContactView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_contactSub =
|
_contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
|
||||||
twonlyDB.contactsDao.watchContact(widget.userId).listen((update) {
|
update,
|
||||||
|
) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_contact = update;
|
_contact = update;
|
||||||
});
|
});
|
||||||
|
|
@ -45,8 +45,8 @@ class _ContactViewState extends State<ContactView> {
|
||||||
_groupMemberSub = twonlyDB.groupsDao
|
_groupMemberSub = twonlyDB.groupsDao
|
||||||
.watchContactGroupMember(widget.userId)
|
.watchContactGroupMember(widget.userId)
|
||||||
.listen((groups) async {
|
.listen((groups) async {
|
||||||
_memberOfGroups = groups;
|
_memberOfGroups = groups;
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,8 +81,9 @@ class _ContactViewState extends State<ContactView> {
|
||||||
|
|
||||||
final remove = await showAlertDialog(
|
final remove = await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
context.lang
|
context.lang.contactRemoveTitle(
|
||||||
.contactRemoveTitle(getContactDisplayName(contact, maxLength: 20)),
|
getContactDisplayName(contact, maxLength: 20),
|
||||||
|
),
|
||||||
context.lang.contactRemoveBody,
|
context.lang.contactRemoveBody,
|
||||||
);
|
);
|
||||||
if (remove) {
|
if (remove) {
|
||||||
|
|
@ -177,13 +178,11 @@ class _ContactViewState extends State<ContactView> {
|
||||||
icon: FontAwesomeIcons.solidComments,
|
icon: FontAwesomeIcons.solidComments,
|
||||||
text: context.lang.contactViewMessage,
|
text: context.lang.contactViewMessage,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final group =
|
final group = await twonlyDB.groupsDao.getDirectChat(
|
||||||
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
contact.userId,
|
||||||
|
);
|
||||||
if (group != null && context.mounted) {
|
if (group != null && context.mounted) {
|
||||||
await context.push(
|
await context.push(Routes.chatsMessages(group.groupId));
|
||||||
Routes.chatsMessages,
|
|
||||||
extra: group,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -196,8 +195,10 @@ class _ContactViewState extends State<ContactView> {
|
||||||
|
|
||||||
if (context.mounted && nickName != null && nickName != '') {
|
if (context.mounted && nickName != null && nickName != '') {
|
||||||
final update = ContactsCompanion(nickName: Value(nickName));
|
final update = ContactsCompanion(nickName: Value(nickName));
|
||||||
await twonlyDB.contactsDao
|
await twonlyDB.contactsDao.updateContact(
|
||||||
.updateContact(contact.userId, update);
|
contact.userId,
|
||||||
|
update,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -208,18 +209,18 @@ class _ContactViewState extends State<ContactView> {
|
||||||
MaxFlameListTitle(
|
MaxFlameListTitle(
|
||||||
contactId: widget.userId,
|
contactId: widget.userId,
|
||||||
),
|
),
|
||||||
BetterListTile(
|
if (!contact.verified)
|
||||||
leading: SvgIcon(
|
BetterListTile(
|
||||||
assetPath: SvgIcons.verifiedGreen,
|
leading: VerifiedShield(
|
||||||
size: 20,
|
contact: contact,
|
||||||
color: IconTheme.of(context).color,
|
size: 20,
|
||||||
|
),
|
||||||
|
text: context.lang.contactVerifyNumberTitle,
|
||||||
|
onTap: () async {
|
||||||
|
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
text: context.lang.contactVerifyNumberTitle,
|
|
||||||
onTap: () async {
|
|
||||||
await context.push(Routes.settingsPublicProfile);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.flag,
|
icon: FontAwesomeIcons.flag,
|
||||||
text: context.lang.reportUser,
|
text: context.lang.reportUser,
|
||||||
|
|
@ -247,8 +248,9 @@ Future<String?> showNicknameChangeDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Contact contact,
|
Contact contact,
|
||||||
) {
|
) {
|
||||||
final controller =
|
final controller = TextEditingController(
|
||||||
TextEditingController(text: getContactDisplayName(contact));
|
text: getContactDisplayName(contact),
|
||||||
|
);
|
||||||
|
|
||||||
return showDialog<String>(
|
return showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -258,8 +260,9 @@ Future<String?> showNicknameChangeDialog(
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration:
|
decoration: InputDecoration(
|
||||||
InputDecoration(hintText: context.lang.contactNicknameNew),
|
hintText: context.lang.contactNicknameNew,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -271,8 +274,9 @@ Future<String?> showNicknameChangeDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(context.lang.ok),
|
child: Text(context.lang.ok),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context)
|
Navigator.of(
|
||||||
.pop(controller.text); // Return the input text
|
context,
|
||||||
|
).pop(controller.text); // Return the input text
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -291,8 +295,9 @@ Future<String?> showReportDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title:
|
title: Text(
|
||||||
Text(context.lang.reportUserTitle(getContactDisplayName(contact))),
|
context.lang.reportUserTitle(getContactDisplayName(contact)),
|
||||||
|
),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
|
||||||
|
|
@ -124,14 +124,15 @@ class GroupMemberContextMenu extends StatelessWidget {
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
title: context.lang.contextMenuOpenChat,
|
title: context.lang.contextMenuOpenChat,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final directChat =
|
final directChat = await twonlyDB.groupsDao.getDirectChat(
|
||||||
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
contact.userId,
|
||||||
|
);
|
||||||
if (directChat == null) {
|
if (directChat == null) {
|
||||||
// create
|
// create
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
await context.push(Routes.chatsMessages, extra: directChat);
|
await context.push(Routes.chatsMessages(directChat.groupId));
|
||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.message,
|
icon: FontAwesomeIcons.message,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/intent/links.intent.dart';
|
import 'package:twonly/src/services/intent/links.intent.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
|
|
@ -106,7 +108,12 @@ class HomeViewState extends State<HomeView> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
selectNotificationStream.stream.listen((response) async {
|
selectNotificationStream.stream.listen((response) async {
|
||||||
globalUpdateOfHomeViewPageIndex(0);
|
if (response.payload != null &&
|
||||||
|
response.payload!.startsWith(Routes.chats)) {
|
||||||
|
await routerProvider.push(response.payload!);
|
||||||
|
} else {
|
||||||
|
globalUpdateOfHomeViewPageIndex(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
unawaited(_mainCameraController.selectCamera(0, true));
|
unawaited(_mainCameraController.selectCamera(0, true));
|
||||||
unawaited(initAsync());
|
unawaited(initAsync());
|
||||||
|
|
@ -140,13 +147,26 @@ class HomeViewState extends State<HomeView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
final notificationAppLaunchDetails =
|
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
||||||
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
.getNotificationAppLaunchDetails();
|
||||||
|
|
||||||
if (widget.initialPage == 0 ||
|
if (widget.initialPage == 0 ||
|
||||||
(notificationAppLaunchDetails != null &&
|
(notificationAppLaunchDetails != null &&
|
||||||
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||||
globalUpdateOfHomeViewPageIndex(0);
|
var pushed = false;
|
||||||
|
|
||||||
|
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
|
final payload =
|
||||||
|
notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||||
|
if (payload != null && payload.startsWith(Routes.chats)) {
|
||||||
|
await routerProvider.push(payload);
|
||||||
|
pushed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pushed) {
|
||||||
|
globalUpdateOfHomeViewPageIndex(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
|
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
|
||||||
|
|
@ -168,8 +188,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
onDoubleTap:
|
onDoubleTap: offsetRatio == 0
|
||||||
offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
|
? _mainCameraController.onDoubleTap
|
||||||
|
: null,
|
||||||
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:restart_app/restart_app.dart';
|
import 'package:restart_app/restart_app.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -32,6 +33,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> toggleVideoStabilization() async {
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.videoStabilizationEnabled = !u.videoStabilizationEnabled;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -53,6 +62,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
context.push(Routes.settingsDeveloperRetransmissionDatabase),
|
context.push(Routes.settingsDeveloperRetransmissionDatabase),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Toggle Video Stabilization'),
|
||||||
|
onTap: toggleVideoStabilization,
|
||||||
|
trailing: Switch(
|
||||||
|
value: gUser.videoStabilizationEnabled,
|
||||||
|
onChanged: (a) => toggleVideoStabilization(),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Delete all (!) app data'),
|
title: const Text('Delete all (!) app data'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
|
@ -71,6 +88,10 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Reduce flames'),
|
||||||
|
onTap: () => context.push(Routes.settingsDeveloperReduceFlames),
|
||||||
|
),
|
||||||
if (!kReleaseMode)
|
if (!kReleaseMode)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Make it possible to reset flames'),
|
title: const Text('Make it possible to reset flames'),
|
||||||
|
|
@ -84,9 +105,13 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
flameCounter: const Value(0),
|
flameCounter: const Value(0),
|
||||||
maxFlameCounter: const Value(365),
|
maxFlameCounter: const Value(365),
|
||||||
lastFlameCounterChange: Value(clock.now()),
|
lastFlameCounterChange: Value(clock.now()),
|
||||||
|
maxFlameCounterFrom: Value(
|
||||||
|
clock.now().subtract(const Duration(days: 1)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await HapticFeedback.heavyImpact();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!kReleaseMode)
|
if (!kReleaseMode)
|
||||||
|
|
|
||||||
130
lib/src/views/settings/developer/reduce_flames.view.dart
Normal file
130
lib/src/views/settings/developer/reduce_flames.view.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||||
|
import 'package:twonly/src/views/components/flame.dart';
|
||||||
|
|
||||||
|
class ReduceFlamesView extends StatefulWidget {
|
||||||
|
const ReduceFlamesView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReduceFlamesView> createState() => _ReduceFlamesViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReduceFlamesViewState extends State<ReduceFlamesView> {
|
||||||
|
List<Group> allGroups = [];
|
||||||
|
List<Group> backupFlames = [];
|
||||||
|
HashSet<String> changedGroupIds = HashSet();
|
||||||
|
late StreamSubscription<List<Group>> groupSub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
|
||||||
|
|
||||||
|
groupSub = stream.listen((update) async {
|
||||||
|
if (backupFlames.isEmpty) {
|
||||||
|
backupFlames = update;
|
||||||
|
}
|
||||||
|
update.sort(
|
||||||
|
(a, b) => a.flameCounter.compareTo(b.flameCounter),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
allGroups = update.where((g) => g.flameCounter > 1).toList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
unawaited(groupSub.cancel());
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Reduce Flames'),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: 40,
|
||||||
|
left: 10,
|
||||||
|
top: 20,
|
||||||
|
right: 10,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'There was a bug that caused the flames in direct messages to be replaced by group flames. Here, you can reduce the flames again. If you reduce the flames, the other person MUST do the same; otherwise, they will be resynchronized to the higher value.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
for (final backupGroup in backupFlames) {
|
||||||
|
if (changedGroupIds.contains(backupGroup.groupId)) {
|
||||||
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
|
backupGroup.groupId,
|
||||||
|
GroupsCompanion(
|
||||||
|
flameCounter: Value(backupGroup.flameCounter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Undo changes'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
restorationId: 'new_message_users_list',
|
||||||
|
itemCount: allGroups.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final group = allGroups[i];
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(group.groupName),
|
||||||
|
FlameCounterWidget(
|
||||||
|
groupId: group.groupId,
|
||||||
|
prefix: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: AvatarIcon(
|
||||||
|
group: group,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
trailing: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
changedGroupIds.add(group.groupId);
|
||||||
|
twonlyDB.groupsDao.updateGroup(
|
||||||
|
group.groupId,
|
||||||
|
GroupsCompanion(
|
||||||
|
flameCounter: Value(group.flameCounter - 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('-1'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/views/components/better_list_title.dart';
|
||||||
import 'package:twonly/src/views/components/svg_icon.dart';
|
import 'package:twonly/src/views/components/svg_icon.dart';
|
||||||
|
|
||||||
|
const colorVerificationBadgeYellow = Color(0xffffa500);
|
||||||
|
|
||||||
class VerificationBadeFaqView extends StatefulWidget {
|
class VerificationBadeFaqView extends StatefulWidget {
|
||||||
const VerificationBadeFaqView({super.key});
|
const VerificationBadeFaqView({super.key});
|
||||||
|
|
||||||
|
|
@ -33,7 +39,7 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||||
icon: const SvgIcon(
|
icon: const SvgIcon(
|
||||||
assetPath: SvgIcons.verifiedGreen,
|
assetPath: SvgIcons.verifiedGreen,
|
||||||
size: 40,
|
size: 40,
|
||||||
color: Color.fromARGB(255, 227, 227, 3),
|
color: colorVerificationBadgeYellow,
|
||||||
),
|
),
|
||||||
description: context.lang.verificationBadgeYellowDesc,
|
description: context.lang.verificationBadgeYellowDesc,
|
||||||
),
|
),
|
||||||
|
|
@ -41,6 +47,18 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
||||||
description: context.lang.verificationBadgeRedDesc,
|
description: context.lang.verificationBadgeRedDesc,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
BetterListTile(
|
||||||
|
leading: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
text: context.lang.scanOtherProfile,
|
||||||
|
onTap: () => context.push(Routes.cameraQRScanner),
|
||||||
|
),
|
||||||
|
BetterListTile(
|
||||||
|
leading: const FaIcon(FontAwesomeIcons.qrcode),
|
||||||
|
text: context.lang.openYourOwnQRcode,
|
||||||
|
onTap: () => context.push(Routes.settingsPublicProfile),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,87 @@ 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/components/alert_dialog.dart';
|
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||||
|
|
||||||
class NotificationView extends StatelessWidget {
|
class NotificationView extends StatefulWidget {
|
||||||
const NotificationView({super.key});
|
const NotificationView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotificationView> createState() => _NotificationViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationViewState extends State<NotificationView> {
|
||||||
|
bool _isLoadingTroubleshooting = false;
|
||||||
|
bool _isLoadingReset = false;
|
||||||
|
bool _troubleshootingDidRun = false;
|
||||||
|
|
||||||
|
Future<void> _troubleshooting() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingTroubleshooting = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await initFCMAfterAuthenticated(force: true);
|
||||||
|
|
||||||
|
final storedToken = await (const FlutterSecureStorage().read(
|
||||||
|
key: SecureStorageKeys.googleFcm,
|
||||||
|
));
|
||||||
|
|
||||||
|
await setupNotificationWithUsers(force: true);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (storedToken == null) {
|
||||||
|
final platform = Platform.isAndroid ? "Google's" : "Apple's";
|
||||||
|
await showAlertDialog(
|
||||||
|
context,
|
||||||
|
'Problem detected',
|
||||||
|
'twonly is not able to register your app to $platform push server infrastructure. For Android that can happen when you do not have the Google Play Services installed. If you theses installed and want to help us to fix the issue please send us your debug log in Settings > Help > Debug log.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final run = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.settingsNotifyTroubleshootingNoProblem,
|
||||||
|
context.lang.settingsNotifyTroubleshootingNoProblemDesc,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (run) {
|
||||||
|
final user = await getUser();
|
||||||
|
if (user != null) {
|
||||||
|
final pushData = await encryptPushNotification(
|
||||||
|
user.userId,
|
||||||
|
PushNotification(
|
||||||
|
messageId: uuid.v4(),
|
||||||
|
kind: PushKind.testNotification,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await apiService.sendTextMessage(
|
||||||
|
user.userId,
|
||||||
|
Uint8List(0),
|
||||||
|
pushData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_troubleshootingDidRun = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoadingTroubleshooting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetTokens() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingReset = true;
|
||||||
|
});
|
||||||
|
await resetFCMTokens();
|
||||||
|
if (!mounted) return;
|
||||||
|
await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.settingsNotifyResetTitleReset,
|
||||||
|
context.lang.settingsNotifyResetTitleResetDesc,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isLoadingReset = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -27,47 +105,32 @@ class NotificationView extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.settingsNotifyTroubleshooting),
|
title: Text(context.lang.settingsNotifyTroubleshooting),
|
||||||
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
||||||
onTap: () async {
|
trailing: _isLoadingTroubleshooting
|
||||||
await initFCMAfterAuthenticated();
|
? const SizedBox(
|
||||||
final storedToken = await (const FlutterSecureStorage()
|
width: 16,
|
||||||
.read(key: SecureStorageKeys.googleFcm));
|
height: 16,
|
||||||
await setupNotificationWithUsers(force: true);
|
child: CircularProgressIndicator(
|
||||||
if (!context.mounted) return;
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
if (storedToken == null) {
|
)
|
||||||
final platform = Platform.isAndroid ? "Google's" : "Apple's";
|
: null,
|
||||||
await showAlertDialog(
|
onTap: _isLoadingTroubleshooting ? null : _troubleshooting,
|
||||||
context,
|
|
||||||
'Problem detected',
|
|
||||||
'twonly is not able to register your app to $platform push server infrastructure. For Android that can happen when you do not have the Google Play Services installed. If you theses installed and want to help us to fix the issue please send us your debug log in Settings > Help > Debug log.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final run = await showAlertDialog(
|
|
||||||
context,
|
|
||||||
context.lang.settingsNotifyTroubleshootingNoProblem,
|
|
||||||
context.lang.settingsNotifyTroubleshootingNoProblemDesc,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (run) {
|
|
||||||
final user = await getUser();
|
|
||||||
if (user != null) {
|
|
||||||
final pushData = await encryptPushNotification(
|
|
||||||
user.userId,
|
|
||||||
PushNotification(
|
|
||||||
messageId: uuid.v4(),
|
|
||||||
kind: PushKind.testNotification,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await apiService.sendTextMessage(
|
|
||||||
user.userId,
|
|
||||||
Uint8List(0),
|
|
||||||
pushData,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
if (_troubleshootingDidRun)
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.settingsNotifyResetTitle),
|
||||||
|
subtitle: Text(context.lang.settingsNotifyResetTitleSubtitle),
|
||||||
|
trailing: _isLoadingReset
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: _isLoadingReset ? null : resetTokens,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,12 @@ class _SettingsMainViewState extends State<SettingsMainView> {
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.circleQuestion,
|
icon: FontAwesomeIcons.circleQuestion,
|
||||||
text: context.lang.settingsHelp,
|
text: context.lang.settingsHelp,
|
||||||
onTap: () => context.push(Routes.settingsHelp),
|
onTap: () async {
|
||||||
|
await context.push(Routes.settingsHelp);
|
||||||
|
setState(() {
|
||||||
|
// gUser could have been changed
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
if (gUser.isDeveloper)
|
if (gUser.isDeveloper)
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
|
|
|
||||||
214
pubspec.lock
214
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a
|
sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.67"
|
version: "1.3.68"
|
||||||
adaptive_number:
|
adaptive_number:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
|
|
@ -84,10 +84,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
audio_waveforms:
|
audio_waveforms:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -124,18 +124,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3"
|
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.5"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_config
|
name: build_config
|
||||||
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
|
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
build_daemon:
|
build_daemon:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -148,10 +148,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734"
|
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.1"
|
version: "2.13.1"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -164,10 +164,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9"
|
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.4"
|
version: "8.12.5"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -196,27 +196,27 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: camera
|
name: camera
|
||||||
sha256: "4142a19a38e388d3bab444227636610ba88982e36dff4552d5191a86f65dc437"
|
sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.4"
|
version: "0.12.0+1"
|
||||||
camera_android_camerax:
|
camera_android_camerax:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: "packages/camera/camera_android_camerax"
|
path: "packages/camera/camera_android_camerax"
|
||||||
ref: "43b87faec960306f98d767253b9bf2cee61be630"
|
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
resolved-ref: "43b87faec960306f98d767253b9bf2cee61be630"
|
resolved-ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
url: "https://github.com/otsmr/flutter-packages.git"
|
url: "https://github.com/otsmr/flutter-packages.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.6.25+1"
|
version: "0.7.1+2"
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_avfoundation
|
name: camera_avfoundation
|
||||||
sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b"
|
sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.23+2"
|
version: "0.10.1"
|
||||||
camera_platform_interface:
|
camera_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -301,18 +301,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus
|
name: connectivity_plus
|
||||||
sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c"
|
sha256: b8fe52979ff12432ecf8f0abf6ff70410b1bb734be1c9e4f2f86807ad7166c79
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.1.0"
|
||||||
connectivity_plus_platform_interface:
|
connectivity_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: connectivity_plus_platform_interface
|
name: connectivity_plus_platform_interface
|
||||||
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.1.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -365,10 +365,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "6f6b30cba0301e7b38f32bdc9a6bdae6f5921a55f0a1eb9450e1e6515645dbb2"
|
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.7"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -381,10 +381,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
|
sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.3.0"
|
version: "12.4.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -504,54 +504,78 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
version: "0.9.3+5"
|
||||||
|
firebase_app_installations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_app_installations
|
||||||
|
sha256: "9103cac19ec40561b49a023e8e583f007f77a499c2058f5a1d82ba480c2367d7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
|
firebase_app_installations_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_app_installations_platform_interface
|
||||||
|
sha256: "0811d37b91c992cc0c98200cca79652d0375a16c58b364a4db5601571c198ee5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4+67"
|
||||||
|
firebase_app_installations_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_app_installations_web
|
||||||
|
sha256: "02f2a96e85581bd1f78319b503dc92c80afa1527cbc299c7c921995b75595bbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.7+4"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb
|
sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.0"
|
version: "4.6.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
version: "6.0.3"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6"
|
sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.5.0"
|
version: "3.5.1"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
sha256: bd17823b70e629877904d384841cda72ed2cc197517404c0c90da5c0ba786a8c
|
sha256: "8dc372085b1647f05e3ec1b8bc1dada87c0062f93b2a6976f620eb85edc44f97"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.1.2"
|
version: "16.1.3"
|
||||||
firebase_messaging_platform_interface:
|
firebase_messaging_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_platform_interface
|
name: firebase_messaging_platform_interface
|
||||||
sha256: "550435235cc7d53683f32bf0762c28ef8cfc20a8d36318a033676ae09526d7fb"
|
sha256: "6ea10f7df747542b17679d5939213c09163aab9c301b2f9b858cb55f38efdb54"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.7"
|
version: "4.7.8"
|
||||||
firebase_messaging_web:
|
firebase_messaging_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_web
|
name: firebase_messaging_web
|
||||||
sha256: "6b1b93ed90309fbce91c219e3cd32aa831e8eccaf4a61f3afaea1625479275d2"
|
sha256: "1f9798c8021ccf22b7e43e7fba81becd42252cb168228379fcabb7c2ef7dd638"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.3"
|
version: "4.1.4"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -727,10 +751,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.33"
|
version: "2.0.34"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -790,10 +814,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
|
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.4"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -848,10 +872,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896"
|
sha256: "48fb2f42ad057476fa4b733cb95e9f9ea7b0b010bb349ea491dca7dbdb18ffc4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.1.0"
|
version: "17.2.0"
|
||||||
google_mlkit_barcode_scanning:
|
google_mlkit_barcode_scanning:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -917,10 +941,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
|
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -973,10 +997,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156
|
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+14"
|
version: "0.8.13+16"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1037,10 +1061,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: in_app_purchase_android
|
name: in_app_purchase_android
|
||||||
sha256: abb254ae159a5a9d4f867795ecb076864faeba59ce015ab81d4cca380f23df45
|
sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.0+8"
|
version: "0.4.0+10"
|
||||||
in_app_purchase_platform_interface:
|
in_app_purchase_platform_interface:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1053,10 +1077,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: in_app_purchase_storekit
|
name: in_app_purchase_storekit
|
||||||
sha256: "2f1a1db44798158076ced07d401b349880dd24a29c7c50a1b1a0de230b7f2f62"
|
sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.8"
|
version: "0.4.8+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1100,10 +1124,10 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: json_serializable
|
name: json_serializable
|
||||||
sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0"
|
sha256: fbcf404b03520e6e795f6b9b39badb2b788407dfc0a50cf39158a6ae1ca78925
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.13.0"
|
version: "6.13.1"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1155,10 +1179,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: dc9663a7bc8ac33d7d988e63901974f63d527ebef260eabd19c479447cc9c911
|
sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.7"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1241,10 +1265,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: native_toolchain_c
|
name: native_toolchain_c
|
||||||
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.4"
|
version: "0.17.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1303,10 +1327,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.0"
|
version: "9.0.1"
|
||||||
package_info_plus_platform_interface:
|
package_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1343,10 +1367,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.22"
|
version: "2.2.23"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1485,10 +1509,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: pro_video_editor
|
name: pro_video_editor
|
||||||
sha256: "5aa37aed1399333a3ac4b78ce00c7dcba77c5e407b6420960bba43751895fa22"
|
sha256: cfed1424b3ca3d5981cc81efdd20b844c995c0ad2818e185eb5bc06a8674f728
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.2"
|
version: "1.14.2"
|
||||||
protobuf:
|
protobuf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1570,26 +1594,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sentry
|
name: sentry
|
||||||
sha256: "605ad1f6f1ae5b72018cbe8fc20f490fa3bd53e58882e5579566776030d8c8c1"
|
sha256: "288aee3d35f252ac0dc3a4b0accbbe7212fa2867604027f2cc5bc65334afd743"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.14.0"
|
version: "9.16.0"
|
||||||
sentry_flutter:
|
sentry_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sentry_flutter
|
name: sentry_flutter
|
||||||
sha256: "7fd0fb80050c1f6a77ae185bda997a76d384326d6777cf5137a6c38952c4ac7d"
|
sha256: f9e87d5895cc437902aa2b081727ee7e46524fe7cc2e1910f535480a3eeb8bed
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.14.0"
|
version: "9.16.0"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: share_plus
|
name: share_plus
|
||||||
sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840"
|
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.1"
|
version: "12.0.2"
|
||||||
share_plus_platform_interface:
|
share_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1602,18 +1626,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.5"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41"
|
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.21"
|
version: "2.4.23"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1634,10 +1658,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1679,18 +1703,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17"
|
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.2"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae"
|
sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.10"
|
version: "1.3.11"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1711,10 +1735,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_android
|
name: sqflite_android
|
||||||
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2+2"
|
version: "2.4.2+3"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1751,10 +1775,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "1e800ebe7f85a80a66adacaa6febe4d5f4d8b75f244e9838a27cb2ffc7aec08d"
|
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.41"
|
version: "0.5.42"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1847,10 +1871,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.28"
|
version: "6.3.29"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1911,10 +1935,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: vector_graphics
|
name: vector_graphics
|
||||||
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
|
sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.19"
|
version: "1.1.21"
|
||||||
vector_graphics_codec:
|
vector_graphics_codec:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1951,26 +1975,26 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
sha256: "08bfba72e311d48219acad4e191b1f9c27ff8cf928f2c7234874592d9c9d7341"
|
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.11.1"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_android
|
name: video_player_android
|
||||||
sha256: "9862c67c4661c98f30fe707bc1a4f97d6a0faa76784f485d282668e4651a7ac3"
|
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.4"
|
version: "2.9.5"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
sha256: f93b93a3baa12ca0ff7d00ca8bc60c1ecd96865568a01ff0c18a99853ee201a5
|
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.3"
|
version: "2.9.4"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.1.1+101
|
version: 0.1.3+103
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
@ -30,7 +30,7 @@ dependencies:
|
||||||
|
|
||||||
|
|
||||||
# Trusted publisher flutter.dev
|
# Trusted publisher flutter.dev
|
||||||
camera: ^0.11.2
|
camera: ^0.12.0+1
|
||||||
flutter_svg: ^2.0.17
|
flutter_svg: ^2.0.17
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
local_auth: ^3.0.0
|
local_auth: ^3.0.0
|
||||||
|
|
@ -54,6 +54,7 @@ dependencies:
|
||||||
# Trustworthy publishers
|
# Trustworthy publishers
|
||||||
firebase_core: ^4.3.0 # firebase.google.com
|
firebase_core: ^4.3.0 # firebase.google.com
|
||||||
firebase_messaging: ^16.1.0 # firebase.google.com
|
firebase_messaging: ^16.1.0 # firebase.google.com
|
||||||
|
firebase_app_installations: ^0.4.1 # firebase.google.com
|
||||||
json_annotation: ^4.9.0 # google.dev
|
json_annotation: ^4.9.0 # google.dev
|
||||||
protobuf: ^4.0.0 # google.dev
|
protobuf: ^4.0.0 # google.dev
|
||||||
scrollable_positioned_list: ^0.3.8 # google.dev
|
scrollable_positioned_list: ^0.3.8 # google.dev
|
||||||
|
|
@ -152,7 +153,7 @@ dependency_overrides:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/otsmr/flutter-packages.git
|
url: https://github.com/otsmr/flutter-packages.git
|
||||||
path: packages/camera/camera_android_camerax
|
path: packages/camera/camera_android_camerax
|
||||||
ref: 43b87faec960306f98d767253b9bf2cee61be630
|
ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69
|
||||||
emoji_picker_flutter:
|
emoji_picker_flutter:
|
||||||
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
|
||||||
# Using override until this gets merged.
|
# Using override until this gets merged.
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,20 @@ void main() {
|
||||||
);
|
);
|
||||||
await withClock(
|
await withClock(
|
||||||
Clock.fixed(DateTime(2026, 3, 25, 19)),
|
Clock.fixed(DateTime(2026, 3, 25, 19)),
|
||||||
|
() async {
|
||||||
|
final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!;
|
||||||
|
expect(isItPossibleToRestoreFlames(group2), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await withClock(
|
||||||
|
Clock.fixed(DateTime(2026, 3, 26, 19)),
|
||||||
|
() async {
|
||||||
|
final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!;
|
||||||
|
expect(isItPossibleToRestoreFlames(group2), true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await withClock(
|
||||||
|
Clock.fixed(DateTime(2026, 3, 27, 19)),
|
||||||
() async {
|
() async {
|
||||||
final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!;
|
final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!;
|
||||||
expect(isItPossibleToRestoreFlames(group2), false);
|
expect(isItPossibleToRestoreFlames(group2), false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue