mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 06:42:12 +00:00
Merge pull request #414 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- New: Adds an "Ask a Friend" button to new contact suggestions. - New: Adds security profiles. - Improved: Onboarding flow for new users. - Improved: Flame restore experience. - Improved: The blue verification checkmark now displays the total number of verifications. - Fix: Issue with receiving messages when user closed app while decrypting - Fix: Background message fetching reliability. - Fix: Issue with focus changing when taking a picture - Fix: Issues with the camera initialization
This commit is contained in:
commit
874cf5fecc
129 changed files with 20095 additions and 2090 deletions
4
.github/workflows/dev_github.yml
vendored
4
.github/workflows/dev_github.yml
vendored
|
|
@ -31,5 +31,5 @@ jobs:
|
||||||
- name: flutter analyze
|
- name: flutter analyze
|
||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|
||||||
- name: flutter test
|
# - name: flutter test
|
||||||
run: flutter test
|
# run: flutter test
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -15,6 +15,9 @@
|
||||||
*.sqlite-wal
|
*.sqlite-wal
|
||||||
migrate_working_dir/
|
migrate_working_dir/
|
||||||
|
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/README.md
|
||||||
|
|
||||||
# IntelliJ related
|
# IntelliJ related
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|
|
||||||
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,5 +1,21 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.20
|
||||||
|
|
||||||
|
- New: Adds an "Ask a Friend" button to new contact suggestions.
|
||||||
|
- New: Adds security profiles.
|
||||||
|
- Improved: Onboarding flow for new users.
|
||||||
|
- Improved: Flame restore experience.
|
||||||
|
- Improved: The blue verification checkmark now displays the total number of verifications.
|
||||||
|
- Fix: Issue with receiving messages when user closed app while decrypting
|
||||||
|
- Fix: Background message fetching reliability.
|
||||||
|
- Fix: Issue with focus changing when taking a picture
|
||||||
|
- Fix: Issues with the camera initialization
|
||||||
|
|
||||||
|
## 0.2.16
|
||||||
|
|
||||||
|
- Fix: Images not shown after opening due to cleanup
|
||||||
|
|
||||||
## 0.2.15
|
## 0.2.15
|
||||||
|
|
||||||
- Fix: Issue with opening directly in chats
|
- Fix: Issue with opening directly in chats
|
||||||
|
|
@ -10,7 +26,7 @@
|
||||||
- New: Tutorial on how to use zoom.
|
- New: Tutorial on how to use zoom.
|
||||||
- New: Manage storage view.
|
- New: Manage storage view.
|
||||||
- Improved: Media thumbnails for faster loading.
|
- Improved: Media thumbnails for faster loading.
|
||||||
- Fix: Some message where not marked as opened.
|
- Fix: Some messages were not marked as opened.
|
||||||
|
|
||||||
## 0.2.12
|
## 0.2.12
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM8.107 4.000L9.339 4.000L9.339 12.000L7.832 12.000L7.832 6.246L7.817 6.232L7.339 6.638L6.948 6.899L6.455 7.159L5.861 7.391L5.861 6.014L6.165 5.899L6.499 5.725L6.861 5.493L7.296 5.159L7.643 4.812L7.832 4.565L8.049 4.174L8.107 4.000Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 732 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.899 4.000L8.391 4.000L8.768 4.043L9.029 4.101L9.362 4.217L9.623 4.348L9.942 4.580L10.145 4.783L10.304 4.986L10.406 5.145L10.551 5.464L10.652 5.899L10.667 6.406L10.594 6.884L10.478 7.246L10.290 7.638L10.043 8.029L9.812 8.333L9.507 8.667L8.406 9.696L7.928 10.174L7.754 10.391L7.638 10.580L10.667 10.594L10.667 12.000L5.333 12.000L5.391 11.609L5.522 11.159L5.710 10.725L6.000 10.246L6.232 9.942L6.638 9.478L7.464 8.652L8.072 8.087L8.594 7.551L8.870 7.203L9.029 6.899L9.087 6.739L9.145 6.449L9.145 6.174L9.101 5.928L8.986 5.667L8.768 5.435L8.652 5.362L8.522 5.304L8.246 5.246L7.971 5.246L7.783 5.275L7.652 5.319L7.406 5.464L7.232 5.652L7.101 5.913L7.029 6.203L7.000 6.493L5.493 6.348L5.565 5.870L5.725 5.362L5.957 4.942L6.275 4.594L6.638 4.348L6.942 4.203L7.377 4.072L7.899 4.000Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.730 4.000L8.142 4.000L8.555 4.057L8.854 4.142L9.224 4.313L9.523 4.527L9.794 4.797L10.064 5.196L10.206 5.580L10.249 5.851L10.235 6.263L10.149 6.591L10.007 6.875L9.836 7.103L9.566 7.359L9.125 7.644L9.409 7.730L9.708 7.872L9.950 8.043L10.164 8.256L10.335 8.498L10.477 8.797L10.548 9.039L10.591 9.338L10.577 9.836L10.448 10.377L10.249 10.776L10.093 11.004L9.893 11.231L9.523 11.544L9.125 11.772L8.911 11.858L8.598 11.943L8.171 12.000L7.587 11.986L7.146 11.900L6.733 11.744L6.320 11.488L5.950 11.132L5.680 10.733L5.509 10.320L5.409 9.865L5.409 9.808L6.847 9.637L6.861 9.765L6.947 10.064L7.004 10.192L7.160 10.420L7.416 10.633L7.630 10.733L7.843 10.776L8.171 10.762L8.327 10.719L8.498 10.633L8.797 10.363L8.968 10.064L9.039 9.822L9.068 9.609L9.053 9.167L8.954 8.826L8.769 8.541L8.512 8.327L8.214 8.214L7.872 8.199L7.331 8.299L7.488 7.132L7.929 7.089L8.256 6.975L8.384 6.890L8.569 6.705L8.683 6.505L8.754 6.221L8.754 5.979L8.698 5.737L8.598 5.552L8.427 5.381L8.242 5.281L8.000 5.224L7.772 5.224L7.445 5.324L7.317 5.409L7.132 5.594L7.032 5.751L6.947 5.964L6.890 6.278L5.523 6.036L5.623 5.623L5.794 5.181L5.950 4.911L6.235 4.584L6.591 4.327L6.961 4.157L7.302 4.057L7.730 4.000Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
2
fastlane/Appfile
Normal file
2
fastlane/Appfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json")
|
||||||
|
package_name("eu.twonly") # Your application ID
|
||||||
15
fastlane/Fastfile
Normal file
15
fastlane/Fastfile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
desc "Submit a new App Bundle to the Google Play Internal Track"
|
||||||
|
lane :internal do
|
||||||
|
# This lane assumes that `flutter build appbundle` has already been run from the flutter root.
|
||||||
|
upload_to_play_store(
|
||||||
|
track: 'internal',
|
||||||
|
aab: 'build/app/outputs/bundle/release/app-release.aab',
|
||||||
|
skip_upload_metadata: true,
|
||||||
|
skip_upload_images: true,
|
||||||
|
skip_upload_screenshots: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
13
lib/app.dart
13
lib/app.dart
|
|
@ -137,12 +137,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
bool _isTwonlyLocked = true;
|
bool _isTwonlyLocked = true;
|
||||||
bool _wasLogged = true;
|
bool _wasLogged = true;
|
||||||
|
late int _initialPage;
|
||||||
|
|
||||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initialPage = widget.initialPage;
|
||||||
Log.info('AppWidgetState: initState started');
|
Log.info('AppWidgetState: initState started');
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +152,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
Log.info('AppWidgetState: initAsync started');
|
Log.info('AppWidgetState: initAsync started');
|
||||||
if (userService.isUserCreated) {
|
if (userService.isUserCreated) {
|
||||||
|
if (_initialPage != 0) {
|
||||||
|
final count = await twonlyDB.contactsDao.getContactsCount();
|
||||||
|
if (count == 0) {
|
||||||
|
_initialPage = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
unawaited(FirebaseMessaging.instance.requestPermission());
|
unawaited(FirebaseMessaging.instance.requestPermission());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -200,8 +208,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
_isTwonlyLocked = false;
|
_isTwonlyLocked = false;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (!userService.currentUser.skipSetupPages &&
|
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
|
||||||
userService.currentUser.currentSetupPage != null) {
|
|
||||||
// This will only be shown in case the user have not skipped
|
// This will only be shown in case the user have not skipped
|
||||||
child = SetupView(
|
child = SetupView(
|
||||||
onUpdate: () => setState(() {
|
onUpdate: () => setState(() {
|
||||||
|
|
@ -210,7 +217,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = HomeView(
|
child = HomeView(
|
||||||
initialPage: widget.initialPage,
|
initialPage: _initialPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (_showOnboarding) {
|
} else if (_showOnboarding) {
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,5 @@ class AppState {
|
||||||
static bool allowErrorTrackingViaSentry = false;
|
static bool allowErrorTrackingViaSentry = false;
|
||||||
static bool gotMessageFromServer = false;
|
static bool gotMessageFromServer = false;
|
||||||
static int latestAppVersionId = 116;
|
static int latestAppVersionId = 116;
|
||||||
|
static bool hasCameraPermissions = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -15,7 +16,8 @@ class LoggingCallbacks {
|
||||||
Log.info(log.split('INFO ')[1]);
|
Log.info(log.split('INFO ')[1]);
|
||||||
} else if (log.contains('DEBUG ')) {
|
} else if (log.contains('DEBUG ')) {
|
||||||
Log.info(log.split('DEBUG ')[1]);
|
Log.info(log.split('DEBUG ')[1]);
|
||||||
} else if (kDebugMode) {
|
} else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||||
|
// ignore: avoid_print
|
||||||
print(log);
|
print(log);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ class Routes {
|
||||||
'/settings/privacy/block_users';
|
'/settings/privacy/block_users';
|
||||||
static const String settingsPrivacyUserDiscovery =
|
static const String settingsPrivacyUserDiscovery =
|
||||||
'/settings/privacy/user_discovery';
|
'/settings/privacy/user_discovery';
|
||||||
|
static const String settingsPrivacyProfileSelection =
|
||||||
|
'/settings/privacy/profile_selection';
|
||||||
static const String settingsNotification = '/settings/notification';
|
static const String settingsNotification = '/settings/notification';
|
||||||
static const String settingsStorage = '/settings/storage_data';
|
static const String settingsStorage = '/settings/storage_data';
|
||||||
static const String settingsStorageManage = '/settings/storage_data/manage';
|
static const String settingsStorageManage = '/settings/storage_data/manage';
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
return select(contacts).get();
|
return select(contacts).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getContactsCount() async {
|
||||||
|
final count = contacts.userId.count();
|
||||||
|
final query = selectOnly(contacts)..addColumns([count]);
|
||||||
|
final result = await query.map((row) => row.read(count)).getSingle();
|
||||||
|
return result ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
Stream<int?> watchContactsBlocked() {
|
Stream<int?> watchContactsBlocked() {
|
||||||
final count = contacts.userId.count();
|
final count = contacts.userId.count();
|
||||||
final query = selectOnly(contacts)
|
final query = selectOnly(contacts)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
import 'package:twonly/src/services/flame.service.dart';
|
||||||
|
|
@ -292,6 +293,27 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
return query.map((row) => row.readTable(groups)).getSingleOrNull();
|
return query.map((row) => row.readTable(groups)).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Group?> createOrGetDirectChat(int contactId) async {
|
||||||
|
var directChat = await getDirectChat(contactId);
|
||||||
|
if (directChat == null) {
|
||||||
|
final contact = await attachedDatabase.contactsDao.getContactById(
|
||||||
|
contactId,
|
||||||
|
);
|
||||||
|
if (contact == null) {
|
||||||
|
Log.error('Contact $contactId not found, cannot create direct chat');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await createNewDirectChat(
|
||||||
|
contactId,
|
||||||
|
GroupsCompanion(
|
||||||
|
groupName: Value(getContactDisplayName(contact)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
directChat = await getDirectChat(contactId);
|
||||||
|
}
|
||||||
|
return directChat;
|
||||||
|
}
|
||||||
|
|
||||||
Stream<int> watchSumTotalMediaCounter() {
|
Stream<int> watchSumTotalMediaCounter() {
|
||||||
final query = selectOnly(groups)
|
final query = selectOnly(groups)
|
||||||
..addColumns([groups.totalMediaCounter.sum()]);
|
..addColumns([groups.totalMediaCounter.sum()]);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
KeyVerificationDao(super.db);
|
KeyVerificationDao(super.db);
|
||||||
|
|
||||||
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
||||||
final cutoff = DateTime.now().subtract(const Duration(hours: 24));
|
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
|
||||||
|
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
|
||||||
return (select(
|
return (select(
|
||||||
verificationTokens,
|
verificationTokens,
|
||||||
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
||||||
|
|
@ -223,4 +224,38 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteKeyVerification(int contactId) async {
|
||||||
|
try {
|
||||||
|
await (delete(
|
||||||
|
keyVerifications,
|
||||||
|
)..where((kv) => kv.contactId.equals(contactId))).go();
|
||||||
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||||
|
contactId: contactId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteKeyVerificationById(
|
||||||
|
int verificationId,
|
||||||
|
int contactId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await (delete(
|
||||||
|
keyVerifications,
|
||||||
|
)..where((kv) => kv.verificationId.equals(verificationId))).go();
|
||||||
|
final remaining = await getContactVerification(contactId);
|
||||||
|
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
||||||
|
contactId: contactId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -278,14 +278,12 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
messageId,
|
messageId,
|
||||||
MessageActionType.openedAt,
|
MessageActionType.openedAt,
|
||||||
);
|
);
|
||||||
final now = clock.now();
|
|
||||||
|
|
||||||
await (update(
|
await (update(
|
||||||
messages,
|
messages,
|
||||||
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
)..where((tbl) => tbl.messageId.equals(messageId))).write(
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
openedAt: Value(now),
|
openedAt: Value(timestamp),
|
||||||
openedByAll: Value(isOpenedByAll ? now : null),
|
openedByAll: Value(isOpenedByAll ? timestamp : null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -309,7 +307,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
);
|
);
|
||||||
await twonlyDB.messagesDao.updateMessageId(
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
messageId,
|
messageId,
|
||||||
MessagesCompanion(ackByServer: Value(clock.now())),
|
MessagesCompanion(ackByServer: Value(timestamp)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,12 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UserDiscoveryAnnouncedUser?> getAnnouncedUserById(int id) async {
|
||||||
|
return (select(
|
||||||
|
userDiscoveryAnnouncedUsers,
|
||||||
|
)..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
|
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
|
||||||
select(userDiscoveryAnnouncedUsers).watch();
|
select(userDiscoveryAnnouncedUsers).watch();
|
||||||
|
|
||||||
|
|
|
||||||
3033
lib/src/database/schemas/twonly_db/drift_schema_v17.json
Normal file
3033
lib/src/database/schemas/twonly_db/drift_schema_v17.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
|
||||||
enum MessageType { media, text, contacts, restoreFlameCounter }
|
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
|
||||||
|
|
||||||
@DataClassName('Message')
|
@DataClassName('Message')
|
||||||
class Messages extends Table {
|
class Messages extends Table {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ class UserDiscoveryAnnouncedUsers extends Table {
|
||||||
BoolColumn get wasShownToTheUser =>
|
BoolColumn get wasShownToTheUser =>
|
||||||
boolean().withDefault(const Constant(false))();
|
boolean().withDefault(const Constant(false))();
|
||||||
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
|
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
|
||||||
|
BoolColumn get wasAskedFriends =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {announcedUserId};
|
Set<Column> get primaryKey => {announcedUserId};
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 16;
|
int get schemaVersion => 17;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
|
|
@ -218,6 +218,12 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
);
|
);
|
||||||
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
|
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
|
||||||
},
|
},
|
||||||
|
from16To17: (m, schema) async {
|
||||||
|
await m.addColumn(
|
||||||
|
schema.userDiscoveryAnnouncedUsers,
|
||||||
|
schema.userDiscoveryAnnouncedUsers.wasAskedFriends,
|
||||||
|
);
|
||||||
|
},
|
||||||
)(m, from, to);
|
)(m, from, to);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10318,6 +10318,21 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
||||||
),
|
),
|
||||||
defaultValue: const Constant(false),
|
defaultValue: const Constant(false),
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _wasAskedFriendsMeta = const VerificationMeta(
|
||||||
|
'wasAskedFriends',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> wasAskedFriends = GeneratedColumn<bool>(
|
||||||
|
'was_asked_friends',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("was_asked_friends" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
announcedUserId,
|
announcedUserId,
|
||||||
|
|
@ -10326,6 +10341,7 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
||||||
username,
|
username,
|
||||||
wasShownToTheUser,
|
wasShownToTheUser,
|
||||||
isHidden,
|
isHidden,
|
||||||
|
wasAskedFriends,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
|
@ -10388,6 +10404,15 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
||||||
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta),
|
isHidden.isAcceptableOrUnknown(data['is_hidden']!, _isHiddenMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('was_asked_friends')) {
|
||||||
|
context.handle(
|
||||||
|
_wasAskedFriendsMeta,
|
||||||
|
wasAskedFriends.isAcceptableOrUnknown(
|
||||||
|
data['was_asked_friends']!,
|
||||||
|
_wasAskedFriendsMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10424,6 +10449,10 @@ class $UserDiscoveryAnnouncedUsersTable extends UserDiscoveryAnnouncedUsers
|
||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
data['${effectivePrefix}is_hidden'],
|
data['${effectivePrefix}is_hidden'],
|
||||||
)!,
|
)!,
|
||||||
|
wasAskedFriends: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}was_asked_friends'],
|
||||||
|
)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10441,6 +10470,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
final String? username;
|
final String? username;
|
||||||
final bool wasShownToTheUser;
|
final bool wasShownToTheUser;
|
||||||
final bool isHidden;
|
final bool isHidden;
|
||||||
|
final bool wasAskedFriends;
|
||||||
const UserDiscoveryAnnouncedUser({
|
const UserDiscoveryAnnouncedUser({
|
||||||
required this.announcedUserId,
|
required this.announcedUserId,
|
||||||
required this.announcedPublicKey,
|
required this.announcedPublicKey,
|
||||||
|
|
@ -10448,6 +10478,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
this.username,
|
this.username,
|
||||||
required this.wasShownToTheUser,
|
required this.wasShownToTheUser,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
|
required this.wasAskedFriends,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
|
@ -10460,6 +10491,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
}
|
}
|
||||||
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser);
|
map['was_shown_to_the_user'] = Variable<bool>(wasShownToTheUser);
|
||||||
map['is_hidden'] = Variable<bool>(isHidden);
|
map['is_hidden'] = Variable<bool>(isHidden);
|
||||||
|
map['was_asked_friends'] = Variable<bool>(wasAskedFriends);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10473,6 +10505,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
: Value(username),
|
: Value(username),
|
||||||
wasShownToTheUser: Value(wasShownToTheUser),
|
wasShownToTheUser: Value(wasShownToTheUser),
|
||||||
isHidden: Value(isHidden),
|
isHidden: Value(isHidden),
|
||||||
|
wasAskedFriends: Value(wasAskedFriends),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10490,6 +10523,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
username: serializer.fromJson<String?>(json['username']),
|
username: serializer.fromJson<String?>(json['username']),
|
||||||
wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']),
|
wasShownToTheUser: serializer.fromJson<bool>(json['wasShownToTheUser']),
|
||||||
isHidden: serializer.fromJson<bool>(json['isHidden']),
|
isHidden: serializer.fromJson<bool>(json['isHidden']),
|
||||||
|
wasAskedFriends: serializer.fromJson<bool>(json['wasAskedFriends']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
|
@ -10502,6 +10536,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
'username': serializer.toJson<String?>(username),
|
'username': serializer.toJson<String?>(username),
|
||||||
'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser),
|
'wasShownToTheUser': serializer.toJson<bool>(wasShownToTheUser),
|
||||||
'isHidden': serializer.toJson<bool>(isHidden),
|
'isHidden': serializer.toJson<bool>(isHidden),
|
||||||
|
'wasAskedFriends': serializer.toJson<bool>(wasAskedFriends),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10512,6 +10547,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
Value<String?> username = const Value.absent(),
|
Value<String?> username = const Value.absent(),
|
||||||
bool? wasShownToTheUser,
|
bool? wasShownToTheUser,
|
||||||
bool? isHidden,
|
bool? isHidden,
|
||||||
|
bool? wasAskedFriends,
|
||||||
}) => UserDiscoveryAnnouncedUser(
|
}) => UserDiscoveryAnnouncedUser(
|
||||||
announcedUserId: announcedUserId ?? this.announcedUserId,
|
announcedUserId: announcedUserId ?? this.announcedUserId,
|
||||||
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
|
announcedPublicKey: announcedPublicKey ?? this.announcedPublicKey,
|
||||||
|
|
@ -10519,6 +10555,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
username: username.present ? username.value : this.username,
|
username: username.present ? username.value : this.username,
|
||||||
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
||||||
isHidden: isHidden ?? this.isHidden,
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
|
||||||
);
|
);
|
||||||
UserDiscoveryAnnouncedUser copyWithCompanion(
|
UserDiscoveryAnnouncedUser copyWithCompanion(
|
||||||
UserDiscoveryAnnouncedUsersCompanion data,
|
UserDiscoveryAnnouncedUsersCompanion data,
|
||||||
|
|
@ -10536,6 +10573,9 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
? data.wasShownToTheUser.value
|
? data.wasShownToTheUser.value
|
||||||
: this.wasShownToTheUser,
|
: this.wasShownToTheUser,
|
||||||
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
|
isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden,
|
||||||
|
wasAskedFriends: data.wasAskedFriends.present
|
||||||
|
? data.wasAskedFriends.value
|
||||||
|
: this.wasAskedFriends,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10547,7 +10587,8 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
..write('publicId: $publicId, ')
|
..write('publicId: $publicId, ')
|
||||||
..write('username: $username, ')
|
..write('username: $username, ')
|
||||||
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
||||||
..write('isHidden: $isHidden')
|
..write('isHidden: $isHidden, ')
|
||||||
|
..write('wasAskedFriends: $wasAskedFriends')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -10560,6 +10601,7 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
username,
|
username,
|
||||||
wasShownToTheUser,
|
wasShownToTheUser,
|
||||||
isHidden,
|
isHidden,
|
||||||
|
wasAskedFriends,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
|
|
@ -10573,7 +10615,8 @@ class UserDiscoveryAnnouncedUser extends DataClass
|
||||||
other.publicId == this.publicId &&
|
other.publicId == this.publicId &&
|
||||||
other.username == this.username &&
|
other.username == this.username &&
|
||||||
other.wasShownToTheUser == this.wasShownToTheUser &&
|
other.wasShownToTheUser == this.wasShownToTheUser &&
|
||||||
other.isHidden == this.isHidden);
|
other.isHidden == this.isHidden &&
|
||||||
|
other.wasAskedFriends == this.wasAskedFriends);
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserDiscoveryAnnouncedUsersCompanion
|
class UserDiscoveryAnnouncedUsersCompanion
|
||||||
|
|
@ -10584,6 +10627,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
final Value<String?> username;
|
final Value<String?> username;
|
||||||
final Value<bool> wasShownToTheUser;
|
final Value<bool> wasShownToTheUser;
|
||||||
final Value<bool> isHidden;
|
final Value<bool> isHidden;
|
||||||
|
final Value<bool> wasAskedFriends;
|
||||||
const UserDiscoveryAnnouncedUsersCompanion({
|
const UserDiscoveryAnnouncedUsersCompanion({
|
||||||
this.announcedUserId = const Value.absent(),
|
this.announcedUserId = const Value.absent(),
|
||||||
this.announcedPublicKey = const Value.absent(),
|
this.announcedPublicKey = const Value.absent(),
|
||||||
|
|
@ -10591,6 +10635,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
this.username = const Value.absent(),
|
this.username = const Value.absent(),
|
||||||
this.wasShownToTheUser = const Value.absent(),
|
this.wasShownToTheUser = const Value.absent(),
|
||||||
this.isHidden = const Value.absent(),
|
this.isHidden = const Value.absent(),
|
||||||
|
this.wasAskedFriends = const Value.absent(),
|
||||||
});
|
});
|
||||||
UserDiscoveryAnnouncedUsersCompanion.insert({
|
UserDiscoveryAnnouncedUsersCompanion.insert({
|
||||||
this.announcedUserId = const Value.absent(),
|
this.announcedUserId = const Value.absent(),
|
||||||
|
|
@ -10599,6 +10644,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
this.username = const Value.absent(),
|
this.username = const Value.absent(),
|
||||||
this.wasShownToTheUser = const Value.absent(),
|
this.wasShownToTheUser = const Value.absent(),
|
||||||
this.isHidden = const Value.absent(),
|
this.isHidden = const Value.absent(),
|
||||||
|
this.wasAskedFriends = const Value.absent(),
|
||||||
}) : announcedPublicKey = Value(announcedPublicKey),
|
}) : announcedPublicKey = Value(announcedPublicKey),
|
||||||
publicId = Value(publicId);
|
publicId = Value(publicId);
|
||||||
static Insertable<UserDiscoveryAnnouncedUser> custom({
|
static Insertable<UserDiscoveryAnnouncedUser> custom({
|
||||||
|
|
@ -10608,6 +10654,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
Expression<String>? username,
|
Expression<String>? username,
|
||||||
Expression<bool>? wasShownToTheUser,
|
Expression<bool>? wasShownToTheUser,
|
||||||
Expression<bool>? isHidden,
|
Expression<bool>? isHidden,
|
||||||
|
Expression<bool>? wasAskedFriends,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (announcedUserId != null) 'announced_user_id': announcedUserId,
|
if (announcedUserId != null) 'announced_user_id': announcedUserId,
|
||||||
|
|
@ -10617,6 +10664,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
if (username != null) 'username': username,
|
if (username != null) 'username': username,
|
||||||
if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser,
|
if (wasShownToTheUser != null) 'was_shown_to_the_user': wasShownToTheUser,
|
||||||
if (isHidden != null) 'is_hidden': isHidden,
|
if (isHidden != null) 'is_hidden': isHidden,
|
||||||
|
if (wasAskedFriends != null) 'was_asked_friends': wasAskedFriends,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10627,6 +10675,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
Value<String?>? username,
|
Value<String?>? username,
|
||||||
Value<bool>? wasShownToTheUser,
|
Value<bool>? wasShownToTheUser,
|
||||||
Value<bool>? isHidden,
|
Value<bool>? isHidden,
|
||||||
|
Value<bool>? wasAskedFriends,
|
||||||
}) {
|
}) {
|
||||||
return UserDiscoveryAnnouncedUsersCompanion(
|
return UserDiscoveryAnnouncedUsersCompanion(
|
||||||
announcedUserId: announcedUserId ?? this.announcedUserId,
|
announcedUserId: announcedUserId ?? this.announcedUserId,
|
||||||
|
|
@ -10635,6 +10684,7 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
username: username ?? this.username,
|
username: username ?? this.username,
|
||||||
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
wasShownToTheUser: wasShownToTheUser ?? this.wasShownToTheUser,
|
||||||
isHidden: isHidden ?? this.isHidden,
|
isHidden: isHidden ?? this.isHidden,
|
||||||
|
wasAskedFriends: wasAskedFriends ?? this.wasAskedFriends,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10661,6 +10711,9 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
if (isHidden.present) {
|
if (isHidden.present) {
|
||||||
map['is_hidden'] = Variable<bool>(isHidden.value);
|
map['is_hidden'] = Variable<bool>(isHidden.value);
|
||||||
}
|
}
|
||||||
|
if (wasAskedFriends.present) {
|
||||||
|
map['was_asked_friends'] = Variable<bool>(wasAskedFriends.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10672,7 +10725,8 @@ class UserDiscoveryAnnouncedUsersCompanion
|
||||||
..write('publicId: $publicId, ')
|
..write('publicId: $publicId, ')
|
||||||
..write('username: $username, ')
|
..write('username: $username, ')
|
||||||
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
..write('wasShownToTheUser: $wasShownToTheUser, ')
|
||||||
..write('isHidden: $isHidden')
|
..write('isHidden: $isHidden, ')
|
||||||
|
..write('wasAskedFriends: $wasAskedFriends')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -21534,6 +21588,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableCreateCompanionBuilder =
|
||||||
Value<String?> username,
|
Value<String?> username,
|
||||||
Value<bool> wasShownToTheUser,
|
Value<bool> wasShownToTheUser,
|
||||||
Value<bool> isHidden,
|
Value<bool> isHidden,
|
||||||
|
Value<bool> wasAskedFriends,
|
||||||
});
|
});
|
||||||
typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
|
typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
|
||||||
UserDiscoveryAnnouncedUsersCompanion Function({
|
UserDiscoveryAnnouncedUsersCompanion Function({
|
||||||
|
|
@ -21543,6 +21598,7 @@ typedef $$UserDiscoveryAnnouncedUsersTableUpdateCompanionBuilder =
|
||||||
Value<String?> username,
|
Value<String?> username,
|
||||||
Value<bool> wasShownToTheUser,
|
Value<bool> wasShownToTheUser,
|
||||||
Value<bool> isHidden,
|
Value<bool> isHidden,
|
||||||
|
Value<bool> wasAskedFriends,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$UserDiscoveryAnnouncedUsersTableReferences
|
final class $$UserDiscoveryAnnouncedUsersTableReferences
|
||||||
|
|
@ -21631,6 +21687,11 @@ class $$UserDiscoveryAnnouncedUsersTableFilterComposer
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get wasAskedFriends => $composableBuilder(
|
||||||
|
column: $table.wasAskedFriends,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
Expression<bool> userDiscoveryUserRelationsRefs(
|
Expression<bool> userDiscoveryUserRelationsRefs(
|
||||||
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
|
Expression<bool> Function($$UserDiscoveryUserRelationsTableFilterComposer f)
|
||||||
f,
|
f,
|
||||||
|
|
@ -21697,6 +21758,11 @@ class $$UserDiscoveryAnnouncedUsersTableOrderingComposer
|
||||||
column: $table.isHidden,
|
column: $table.isHidden,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get wasAskedFriends => $composableBuilder(
|
||||||
|
column: $table.wasAskedFriends,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
|
class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
|
||||||
|
|
@ -21732,6 +21798,11 @@ class $$UserDiscoveryAnnouncedUsersTableAnnotationComposer
|
||||||
GeneratedColumn<bool> get isHidden =>
|
GeneratedColumn<bool> get isHidden =>
|
||||||
$composableBuilder(column: $table.isHidden, builder: (column) => column);
|
$composableBuilder(column: $table.isHidden, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get wasAskedFriends => $composableBuilder(
|
||||||
|
column: $table.wasAskedFriends,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
|
Expression<T> userDiscoveryUserRelationsRefs<T extends Object>(
|
||||||
Expression<T> Function(
|
Expression<T> Function(
|
||||||
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
|
$$UserDiscoveryUserRelationsTableAnnotationComposer a,
|
||||||
|
|
@ -21810,6 +21881,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
||||||
Value<String?> username = const Value.absent(),
|
Value<String?> username = const Value.absent(),
|
||||||
Value<bool> wasShownToTheUser = const Value.absent(),
|
Value<bool> wasShownToTheUser = const Value.absent(),
|
||||||
Value<bool> isHidden = const Value.absent(),
|
Value<bool> isHidden = const Value.absent(),
|
||||||
|
Value<bool> wasAskedFriends = const Value.absent(),
|
||||||
}) => UserDiscoveryAnnouncedUsersCompanion(
|
}) => UserDiscoveryAnnouncedUsersCompanion(
|
||||||
announcedUserId: announcedUserId,
|
announcedUserId: announcedUserId,
|
||||||
announcedPublicKey: announcedPublicKey,
|
announcedPublicKey: announcedPublicKey,
|
||||||
|
|
@ -21817,6 +21889,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
||||||
username: username,
|
username: username,
|
||||||
wasShownToTheUser: wasShownToTheUser,
|
wasShownToTheUser: wasShownToTheUser,
|
||||||
isHidden: isHidden,
|
isHidden: isHidden,
|
||||||
|
wasAskedFriends: wasAskedFriends,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
|
|
@ -21826,6 +21899,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
||||||
Value<String?> username = const Value.absent(),
|
Value<String?> username = const Value.absent(),
|
||||||
Value<bool> wasShownToTheUser = const Value.absent(),
|
Value<bool> wasShownToTheUser = const Value.absent(),
|
||||||
Value<bool> isHidden = const Value.absent(),
|
Value<bool> isHidden = const Value.absent(),
|
||||||
|
Value<bool> wasAskedFriends = const Value.absent(),
|
||||||
}) => UserDiscoveryAnnouncedUsersCompanion.insert(
|
}) => UserDiscoveryAnnouncedUsersCompanion.insert(
|
||||||
announcedUserId: announcedUserId,
|
announcedUserId: announcedUserId,
|
||||||
announcedPublicKey: announcedPublicKey,
|
announcedPublicKey: announcedPublicKey,
|
||||||
|
|
@ -21833,6 +21907,7 @@ class $$UserDiscoveryAnnouncedUsersTableTableManager
|
||||||
username: username,
|
username: username,
|
||||||
wasShownToTheUser: wasShownToTheUser,
|
wasShownToTheUser: wasShownToTheUser,
|
||||||
isHidden: isHidden,
|
isHidden: isHidden,
|
||||||
|
wasAskedFriends: wasAskedFriends,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map(
|
.map(
|
||||||
|
|
|
||||||
|
|
@ -8545,6 +8545,483 @@ i1.GeneratedColumn<int> _column_245(String aliasedName) =>
|
||||||
type: i1.DriftSqlType.int,
|
type: i1.DriftSqlType.int,
|
||||||
$customConstraints: 'NULL',
|
$customConstraints: 'NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema17 extends i0.VersionedSchema {
|
||||||
|
Schema17({required super.database}) : super(version: 17);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
contacts,
|
||||||
|
groups,
|
||||||
|
mediaFiles,
|
||||||
|
messages,
|
||||||
|
messageHistories,
|
||||||
|
reactions,
|
||||||
|
groupMembers,
|
||||||
|
receipts,
|
||||||
|
receivedReceipts,
|
||||||
|
signalIdentityKeyStores,
|
||||||
|
signalPreKeyStores,
|
||||||
|
signalSenderKeyStores,
|
||||||
|
signalSessionStores,
|
||||||
|
signalSignedPreKeyStores,
|
||||||
|
messageActions,
|
||||||
|
groupHistories,
|
||||||
|
keyVerifications,
|
||||||
|
verificationTokens,
|
||||||
|
userDiscoveryAnnouncedUsers,
|
||||||
|
userDiscoveryUserRelations,
|
||||||
|
userDiscoveryOtherPromotions,
|
||||||
|
userDiscoveryOwnPromotions,
|
||||||
|
userDiscoveryShares,
|
||||||
|
shortcuts,
|
||||||
|
shortcutMembers,
|
||||||
|
];
|
||||||
|
late final Shape39 contacts = Shape39(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'contacts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_106,
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_211,
|
||||||
|
_column_212,
|
||||||
|
_column_213,
|
||||||
|
_column_214,
|
||||||
|
_column_215,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape23 groups = Shape23(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'groups',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
_column_130,
|
||||||
|
_column_131,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_118,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
_column_138,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
_column_142,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape51 mediaFiles = Shape51(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'media_files',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(media_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
_column_146,
|
||||||
|
_column_147,
|
||||||
|
_column_148,
|
||||||
|
_column_149,
|
||||||
|
_column_239,
|
||||||
|
_column_240,
|
||||||
|
_column_207,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
_column_153,
|
||||||
|
_column_154,
|
||||||
|
_column_155,
|
||||||
|
_column_156,
|
||||||
|
_column_157,
|
||||||
|
_column_244,
|
||||||
|
_column_245,
|
||||||
|
_column_118,
|
||||||
|
_column_241,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape25 messages = Shape25(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'messages',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_144,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_165,
|
||||||
|
_column_153,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_169,
|
||||||
|
_column_118,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape26 messageHistories = Shape26(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_161,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape27 reactions = Shape27(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'reactions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
|
||||||
|
columns: [_column_174, _column_176, _column_177, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 groupMembers = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_158,
|
||||||
|
_column_178,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_209,
|
||||||
|
_column_210,
|
||||||
|
_column_181,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape37 receipts = Shape37(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
_column_208,
|
||||||
|
_column_187,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_118,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape30 receivedReceipts = Shape30(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'received_receipts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||||
|
columns: [_column_182, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape31 signalIdentityKeyStores = Shape31(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_identity_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_194, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 signalPreKeyStores = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
|
||||||
|
columns: [_column_195, _column_196, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 signalSenderKeyStores = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_sender_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
|
||||||
|
columns: [_column_197, _column_198],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape33 signalSessionStores = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_session_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||||
|
columns: [_column_192, _column_193, _column_199, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 signalSignedPreKeyStores = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'signal_signed_pre_key_stores',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'],
|
||||||
|
columns: [_column_242, _column_243, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape34 messageActions = Shape34(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'message_actions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
|
||||||
|
columns: [_column_174, _column_183, _column_144, _column_200],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 groupHistories = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'group_histories',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(group_history_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_201,
|
||||||
|
_column_158,
|
||||||
|
_column_202,
|
||||||
|
_column_203,
|
||||||
|
_column_204,
|
||||||
|
_column_205,
|
||||||
|
_column_206,
|
||||||
|
_column_144,
|
||||||
|
_column_200,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape40 keyVerifications = Shape40(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'key_verifications',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_216, _column_183, _column_144, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 verificationTokens = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'verification_tokens',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_217, _column_218, _column_118],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape52 userDiscoveryAnnouncedUsers = Shape52(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_announced_users',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_219,
|
||||||
|
_column_220,
|
||||||
|
_column_221,
|
||||||
|
_column_222,
|
||||||
|
_column_223,
|
||||||
|
_column_224,
|
||||||
|
_column_246,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 userDiscoveryUserRelations = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_user_relations',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
|
||||||
|
columns: [_column_225, _column_226, _column_227],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 userDiscoveryOtherPromotions = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_other_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_226,
|
||||||
|
_column_228,
|
||||||
|
_column_229,
|
||||||
|
_column_230,
|
||||||
|
_column_231,
|
||||||
|
_column_227,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 userDiscoveryOwnPromotions = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_own_promotions',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_232, _column_183, _column_233],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 userDiscoveryShares = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_discovery_shares',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_234, _column_235, _column_175],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 shortcuts = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcuts',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [_column_173, _column_236, _column_237],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 shortcutMembers = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'shortcut_members',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'],
|
||||||
|
columns: [_column_238, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape52 extends i0.VersionedTable {
|
||||||
|
Shape52({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get announcedUserId =>
|
||||||
|
columnsByName['announced_user_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<i2.Uint8List> get announcedPublicKey =>
|
||||||
|
columnsByName['announced_public_key']!
|
||||||
|
as i1.GeneratedColumn<i2.Uint8List>;
|
||||||
|
i1.GeneratedColumn<int> get publicId =>
|
||||||
|
columnsByName['public_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get username =>
|
||||||
|
columnsByName['username']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get wasShownToTheUser =>
|
||||||
|
columnsByName['was_shown_to_the_user']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get isHidden =>
|
||||||
|
columnsByName['is_hidden']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get wasAskedFriends =>
|
||||||
|
columnsByName['was_asked_friends']! as i1.GeneratedColumn<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_246(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>(
|
||||||
|
'was_asked_friends',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: i1.DriftSqlType.int,
|
||||||
|
$customConstraints:
|
||||||
|
'NOT NULL DEFAULT 0 CHECK (was_asked_friends IN (0, 1))',
|
||||||
|
defaultValue: const i1.CustomExpression('0'),
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
|
|
@ -8561,6 +9038,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
|
|
@ -8639,6 +9117,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from15To16(migrator, schema);
|
await from15To16(migrator, schema);
|
||||||
return 16;
|
return 16;
|
||||||
|
case 16:
|
||||||
|
final schema = Schema17(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from16To17(migrator, schema);
|
||||||
|
return 17;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
|
|
@ -8661,6 +9144,7 @@ i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
|
||||||
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
|
@ -8678,5 +9162,6 @@ i1.OnUpgrade stepByStep({
|
||||||
from13To14: from13To14,
|
from13To14: from13To14,
|
||||||
from14To15: from14To15,
|
from14To15: from14To15,
|
||||||
from15To16: from15To16,
|
from15To16: from15To16,
|
||||||
|
from16To17: from16To17,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -626,6 +626,54 @@ abstract class AppLocalizations {
|
||||||
/// **'{len} contact(s)'**
|
/// **'{len} contact(s)'**
|
||||||
String settingsPrivacyBlockUsersCount(Object len);
|
String settingsPrivacyBlockUsersCount(Object len);
|
||||||
|
|
||||||
|
/// No description provided for @settingsPrivacyProfileSelectionTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Security Profile'**
|
||||||
|
String get settingsPrivacyProfileSelectionTitle;
|
||||||
|
|
||||||
|
/// No description provided for @settingsPrivacyProfileSelectionDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose your setup path and security configuration'**
|
||||||
|
String get settingsPrivacyProfileSelectionDesc;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Security Profile'**
|
||||||
|
String get securityProfileTitle;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'**
|
||||||
|
String get securityProfileSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileNormalTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Normal Protection'**
|
||||||
|
String get securityProfileNormalTitle;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileNormalDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Good balance between a convenient mode without bothering you too much.'**
|
||||||
|
String get securityProfileNormalDesc;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileStrictTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Strict Protection'**
|
||||||
|
String get securityProfileStrictTitle;
|
||||||
|
|
||||||
|
/// No description provided for @securityProfileStrictDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Maximum anti-phishing protection but may be inconvenient.'**
|
||||||
|
String get securityProfileStrictDesc;
|
||||||
|
|
||||||
/// No description provided for @settingsNotification.
|
/// No description provided for @settingsNotification.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -911,8 +959,8 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationTypeSecretQrToken.
|
/// No description provided for @verificationTypeSecretQrToken.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'The other person scanned your QR code.'**
|
/// **'{username} has scanned your QR code.'**
|
||||||
String get verificationTypeSecretQrToken;
|
String verificationTypeSecretQrToken(Object username);
|
||||||
|
|
||||||
/// No description provided for @verificationTypeLink.
|
/// No description provided for @verificationTypeLink.
|
||||||
///
|
///
|
||||||
|
|
@ -2342,6 +2390,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Open your own QR code'**
|
/// **'Open your own QR code'**
|
||||||
String get openYourOwnQRcode;
|
String get openYourOwnQRcode;
|
||||||
|
|
||||||
|
/// No description provided for @addContactQrSheetSubtext.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Let a friend scan this QR code to add you'**
|
||||||
|
String get addContactQrSheetSubtext;
|
||||||
|
|
||||||
/// No description provided for @finishSetupCardTitle.
|
/// No description provided for @finishSetupCardTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2435,13 +2489,13 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @userDiscoverySettingsManualApproval.
|
/// No description provided for @userDiscoverySettingsManualApproval.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Manual approval'**
|
/// **'Ask every time before sharing'**
|
||||||
String get userDiscoverySettingsManualApproval;
|
String get userDiscoverySettingsManualApproval;
|
||||||
|
|
||||||
/// No description provided for @userDiscoverySettingsManualApprovalDesc.
|
/// No description provided for @userDiscoverySettingsManualApprovalDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Before someone is shared, you\'ll be asked first.'**
|
/// **'Before one of your friends is shared, you will be asked every time.'**
|
||||||
String get userDiscoverySettingsManualApprovalDesc;
|
String get userDiscoverySettingsManualApprovalDesc;
|
||||||
|
|
||||||
/// No description provided for @onboardingUserDiscoveryLetFriendsFindYou.
|
/// No description provided for @onboardingUserDiscoveryLetFriendsFindYou.
|
||||||
|
|
@ -2699,7 +2753,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationBadgeGeneralDesc.
|
/// No description provided for @verificationBadgeGeneralDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'**
|
/// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'**
|
||||||
String get verificationBadgeGeneralDesc;
|
String get verificationBadgeGeneralDesc;
|
||||||
|
|
||||||
/// No description provided for @verificationBadgeGreenDesc.
|
/// No description provided for @verificationBadgeGreenDesc.
|
||||||
|
|
@ -2720,6 +2774,36 @@ abstract class AppLocalizations {
|
||||||
/// **'A contact whose identity has *not* yet been verified.'**
|
/// **'A contact whose identity has *not* yet been verified.'**
|
||||||
String get verificationBadgeRedDesc;
|
String get verificationBadgeRedDesc;
|
||||||
|
|
||||||
|
/// No description provided for @deleteVerificationTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete verification?'**
|
||||||
|
String get deleteVerificationTitle;
|
||||||
|
|
||||||
|
/// No description provided for @deleteVerificationBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Are you sure you want to delete this verification?'**
|
||||||
|
String get deleteVerificationBody;
|
||||||
|
|
||||||
|
/// No description provided for @secretQrTokenVerifiedSnackbar.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{username} has scanned your QR code and is now verified.'**
|
||||||
|
String secretQrTokenVerifiedSnackbar(Object username);
|
||||||
|
|
||||||
|
/// No description provided for @mutualGroupsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'**
|
||||||
|
String mutualGroupsTitle(num count);
|
||||||
|
|
||||||
|
/// No description provided for @mutualGroupsSentMessages.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count, plural, =1{1 message sent} other{{count} messages sent}}'**
|
||||||
|
String mutualGroupsSentMessages(num count);
|
||||||
|
|
||||||
/// No description provided for @chatEntryFlameRestored.
|
/// No description provided for @chatEntryFlameRestored.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2852,6 +2936,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Mutual Friends'**
|
/// **'Mutual Friends'**
|
||||||
String get userDiscoverySettingsTitle;
|
String get userDiscoverySettingsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @userDiscoveryWhyThisIsUsed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Why this is used'**
|
||||||
|
String get userDiscoveryWhyThisIsUsed;
|
||||||
|
|
||||||
|
/// No description provided for @userDiscoveryFeatureOffers.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your benefits at a glance'**
|
||||||
|
String get userDiscoveryFeatureOffers;
|
||||||
|
|
||||||
/// No description provided for @userDiscoveryDisabledLearnMore.
|
/// No description provided for @userDiscoveryDisabledLearnMore.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2924,6 +3020,66 @@ abstract class AppLocalizations {
|
||||||
/// **'Request'**
|
/// **'Request'**
|
||||||
String get friendSuggestionsRequest;
|
String get friendSuggestionsRequest;
|
||||||
|
|
||||||
|
/// No description provided for @friendSuggestionsAskFriend.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask your friends'**
|
||||||
|
String get friendSuggestionsAskFriend;
|
||||||
|
|
||||||
|
/// No description provided for @askFriendsDialogTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask about {username}'**
|
||||||
|
String askFriendsDialogTitle(Object username);
|
||||||
|
|
||||||
|
/// No description provided for @askFriendsDialogDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select the friends you want to ask about this user:'**
|
||||||
|
String get askFriendsDialogDescription;
|
||||||
|
|
||||||
|
/// No description provided for @askFriendsDialogConfirm.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask'**
|
||||||
|
String get askFriendsDialogConfirm;
|
||||||
|
|
||||||
|
/// No description provided for @askFriendsDialogCancel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cancel'**
|
||||||
|
String get askFriendsDialogCancel;
|
||||||
|
|
||||||
|
/// No description provided for @chatAskAFriendReceivedDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Your friend just got this as a suggestion and wants to know if he knows this person.'**
|
||||||
|
String get chatAskAFriendReceivedDescription;
|
||||||
|
|
||||||
|
/// No description provided for @chatAskAFriendAddedDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'You have added this user to your contacts.'**
|
||||||
|
String get chatAskAFriendAddedDescription;
|
||||||
|
|
||||||
|
/// No description provided for @chatAskAFriendHide.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Hide'**
|
||||||
|
String get chatAskAFriendHide;
|
||||||
|
|
||||||
|
/// No description provided for @chatAskAFriendRequest.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Request'**
|
||||||
|
String get chatAskAFriendRequest;
|
||||||
|
|
||||||
|
/// No description provided for @chatAskAFriendUnknownUser.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'User {userId}'**
|
||||||
|
String chatAskAFriendUnknownUser(Object userId);
|
||||||
|
|
||||||
/// No description provided for @contactUserDiscoveryImagesLeft.
|
/// No description provided for @contactUserDiscoveryImagesLeft.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3241,6 +3397,102 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Drag to Zoom'**
|
/// **'Drag to Zoom'**
|
||||||
String get dragToZoom;
|
String get dragToZoom;
|
||||||
|
|
||||||
|
/// No description provided for @showUsername.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show username'**
|
||||||
|
String get showUsername;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose your setup path'**
|
||||||
|
String get onboardingProfileSelectionTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose how you want to configure your security and privacy settings.'**
|
||||||
|
String get onboardingProfileSelectionSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionDefaultTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Default'**
|
||||||
|
String get onboardingProfileSelectionDefaultTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionDefaultDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Instantly applies recommended settings so you can start using the app.'**
|
||||||
|
String get onboardingProfileSelectionDefaultDesc;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionDefaultBadge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Fast Setup'**
|
||||||
|
String get onboardingProfileSelectionDefaultBadge;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionCustomizeTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Customize'**
|
||||||
|
String get onboardingProfileSelectionCustomizeTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionCustomizeDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Step-by-step setup so you can decide for yourself.'**
|
||||||
|
String get onboardingProfileSelectionCustomizeDesc;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionStrictTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enhanced Protection'**
|
||||||
|
String get onboardingProfileSelectionStrictTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileSelectionStrictDesc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Maximum anti-phishing defense. Recommended for *journalists & public figures*.'**
|
||||||
|
String get onboardingProfileSelectionStrictDesc;
|
||||||
|
|
||||||
|
/// No description provided for @replyFlameRestored.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flames restored'**
|
||||||
|
String get replyFlameRestored;
|
||||||
|
|
||||||
|
/// No description provided for @replyAskAFriend.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask a friend'**
|
||||||
|
String get replyAskAFriend;
|
||||||
|
|
||||||
|
/// No description provided for @unverifiedWarningDirectTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Identity not verified in person'**
|
||||||
|
String get unverifiedWarningDirectTitle;
|
||||||
|
|
||||||
|
/// No description provided for @unverifiedWarningGroupTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Not all members are verified in person'**
|
||||||
|
String get unverifiedWarningGroupTitle;
|
||||||
|
|
||||||
|
/// No description provided for @unverifiedWarningBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.'**
|
||||||
|
String get unverifiedWarningBody;
|
||||||
|
|
||||||
|
/// No description provided for @unverifiedWarningButton.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Verify now'**
|
||||||
|
String get unverifiedWarningButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,34 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return '$len Kontakt(e)';
|
return '$len Kontakt(e)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPrivacyProfileSelectionDesc =>
|
||||||
|
'Wähle deinen Setup-Pfad und deine Sicherheitskonfiguration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileTitle => 'Sicherheitsprofil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileSubtitle =>
|
||||||
|
'Wähle das Schutzniveau, das zu deiner täglichen Nutzung passt. Dies kann jederzeit in den Einstellungen geändert werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileNormalTitle => 'Normaler Schutz';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileNormalDesc =>
|
||||||
|
'Gute Balance zwischen Komfort und Sicherheit, ohne dich zu sehr einzuschränken.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileStrictTitle => 'Strikter Schutz';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileStrictDesc =>
|
||||||
|
'Maximaler Schutz vor Phishing, kann aber unkomfortabel sein.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsNotification => 'Benachrichtigung';
|
String get settingsNotification => 'Benachrichtigung';
|
||||||
|
|
||||||
|
|
@ -447,8 +475,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeSecretQrToken =>
|
String verificationTypeSecretQrToken(Object username) {
|
||||||
'Die andere Person hat deinen QR-Code gescannt.';
|
return '$username hat deinen QR-Code gescannt.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||||
|
|
@ -1276,6 +1305,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
|
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addContactQrSheetSubtext =>
|
||||||
|
'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get finishSetupCardTitle => 'Profil vervollständigen';
|
String get finishSetupCardTitle => 'Profil vervollständigen';
|
||||||
|
|
||||||
|
|
@ -1329,11 +1362,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Erfahre, wer dich anfragt';
|
'Erfahre, wer dich anfragt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsManualApproval => 'Manuelle Zustimmung';
|
String get userDiscoverySettingsManualApproval => 'Vor jedem Teilen fragen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsManualApprovalDesc =>
|
String get userDiscoverySettingsManualApprovalDesc =>
|
||||||
'Bevor jemand geteilt wird, wirst du zuerst gefragt.';
|
'Bevor einer deiner Freunde geteilt wird, wirst du jedes Mal gefragt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
||||||
|
|
@ -1501,7 +1534,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGeneralDesc =>
|
String get verificationBadgeGeneralDesc =>
|
||||||
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.';
|
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
|
|
@ -1515,6 +1548,40 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteVerificationTitle => 'Verifizierung löschen?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteVerificationBody =>
|
||||||
|
'Möchtest du diese Verifizierung wirklich löschen?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String secretQrTokenVerifiedSnackbar(Object username) {
|
||||||
|
return '$username hat deinen QR-Code gescannt und ist nun verifiziert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mutualGroupsTitle(num count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count gemeinsame Gruppen',
|
||||||
|
one: '1 gemeinsame Gruppe',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mutualGroupsSentMessages(num count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count Nachrichten gesendet',
|
||||||
|
one: '1 Nachricht gesendet',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count Flammen wiederhergestellt';
|
return '$count Flammen wiederhergestellt';
|
||||||
|
|
@ -1594,6 +1661,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
|
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userDiscoveryWhyThisIsUsed => 'Warum dies verwendet wird';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoveryDisabledLearnMore => 'Mehr erfahren';
|
String get userDiscoveryDisabledLearnMore => 'Mehr erfahren';
|
||||||
|
|
||||||
|
|
@ -1637,6 +1710,43 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get friendSuggestionsRequest => 'Anfragen';
|
String get friendSuggestionsRequest => 'Anfragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get friendSuggestionsAskFriend => 'Deine Freunde fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askFriendsDialogTitle(Object username) {
|
||||||
|
return 'Nach $username fragen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogDescription =>
|
||||||
|
'Wähle die Freunde aus, die du zu diesem Nutzer fragen möchtest:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogConfirm => 'Fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogCancel => 'Abbrechen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendReceivedDescription =>
|
||||||
|
'Dein Freund hat diesen Nutzer als Vorschlag erhalten und möchte wissen, ob er diese Person kennt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendAddedDescription =>
|
||||||
|
'Du hast diesen Nutzer zu deinen Kontakten hinzugefügt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendHide => 'Ausblenden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendRequest => 'Anfragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chatAskAFriendUnknownUser(Object userId) {
|
||||||
|
return 'Nutzer $userId';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
||||||
return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.';
|
return 'Es fehlen noch $imagesLeft Bilder bis deine Freunde mit $username geteilt werden.';
|
||||||
|
|
@ -1823,4 +1933,59 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToZoom => 'Zum Zoomen ziehen';
|
String get dragToZoom => 'Zum Zoomen ziehen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showUsername => 'Benutzernamen anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionSubtitle =>
|
||||||
|
'Wähle aus, wie du deine Sicherheits- und Privatsphäre-Einstellungen konfigurieren möchtest.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultTitle => 'Standard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultDesc =>
|
||||||
|
'Wendet sofort die empfohlenen Einstellungen an, damit du die App direkt nutzen kannst.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultBadge => 'Schnelles Setup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionCustomizeTitle => 'Anpassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionCustomizeDesc =>
|
||||||
|
'Schritt-für-Schritt-Einrichtung, damit du selbst entscheiden kannst.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionStrictTitle => 'Erhöhter Schutz';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionStrictDesc =>
|
||||||
|
'Maximaler Schutz vor Phishing. Empfohlen für *Journalisten & Personen des öffentlichen Lebens*.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replyFlameRestored => 'Flammen wiederhergestellt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replyAskAFriend => 'Einen Freund fragen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningDirectTitle =>
|
||||||
|
'Identität nicht persönlich verifiziert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningGroupTitle =>
|
||||||
|
'Nicht alle Mitglieder sind persönlich verifiziert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningBody =>
|
||||||
|
'*Teile keine geheimen Daten*. Jemand könnte sich *als dein Freund ausgeben*.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningButton => 'Jetzt verifizieren';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,34 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return '$len contact(s)';
|
return '$len contact(s)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsPrivacyProfileSelectionDesc =>
|
||||||
|
'Choose your setup path and security configuration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileTitle => 'Security Profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileSubtitle =>
|
||||||
|
'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileNormalTitle => 'Normal Protection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileNormalDesc =>
|
||||||
|
'Good balance between a convenient mode without bothering you too much.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileStrictTitle => 'Strict Protection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securityProfileStrictDesc =>
|
||||||
|
'Maximum anti-phishing protection but may be inconvenient.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsNotification => 'Notification';
|
String get settingsNotification => 'Notification';
|
||||||
|
|
||||||
|
|
@ -442,8 +470,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeSecretQrToken =>
|
String verificationTypeSecretQrToken(Object username) {
|
||||||
'The other person scanned your QR code.';
|
return '$username has scanned your QR code.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Verified via link.';
|
String get verificationTypeLink => 'Verified via link.';
|
||||||
|
|
@ -1267,6 +1296,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get openYourOwnQRcode => 'Open your own QR code';
|
String get openYourOwnQRcode => 'Open your own QR code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addContactQrSheetSubtext =>
|
||||||
|
'Let a friend scan this QR code to add you';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get finishSetupCardTitle => 'Complete your profile';
|
String get finishSetupCardTitle => 'Complete your profile';
|
||||||
|
|
||||||
|
|
@ -1320,11 +1353,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'Be informed about who is requesting';
|
'Be informed about who is requesting';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsManualApproval => 'Manual approval';
|
String get userDiscoverySettingsManualApproval =>
|
||||||
|
'Ask every time before sharing';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsManualApprovalDesc =>
|
String get userDiscoverySettingsManualApprovalDesc =>
|
||||||
'Before someone is shared, you\'ll be asked first.';
|
'Before one of your friends is shared, you will be asked every time.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
String get onboardingUserDiscoveryLetFriendsFindYou =>
|
||||||
|
|
@ -1486,7 +1520,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGeneralDesc =>
|
String get verificationBadgeGeneralDesc =>
|
||||||
'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.';
|
'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
|
|
@ -1500,6 +1534,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'A contact whose identity has *not* yet been verified.';
|
'A contact whose identity has *not* yet been verified.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteVerificationTitle => 'Delete verification?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteVerificationBody =>
|
||||||
|
'Are you sure you want to delete this verification?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String secretQrTokenVerifiedSnackbar(Object username) {
|
||||||
|
return '$username has scanned your QR code and is now verified.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mutualGroupsTitle(num count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count mutual groups',
|
||||||
|
one: '1 mutual group',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String mutualGroupsSentMessages(num count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count messages sent',
|
||||||
|
one: '1 message sent',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count flames restored';
|
return '$count flames restored';
|
||||||
|
|
@ -1579,6 +1647,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsTitle => 'Mutual Friends';
|
String get userDiscoverySettingsTitle => 'Mutual Friends';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userDiscoveryWhyThisIsUsed => 'Why this is used';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoveryDisabledLearnMore => 'Learn more';
|
String get userDiscoveryDisabledLearnMore => 'Learn more';
|
||||||
|
|
||||||
|
|
@ -1622,6 +1696,43 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get friendSuggestionsRequest => 'Request';
|
String get friendSuggestionsRequest => 'Request';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get friendSuggestionsAskFriend => 'Ask your friends';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String askFriendsDialogTitle(Object username) {
|
||||||
|
return 'Ask about $username';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogDescription =>
|
||||||
|
'Select the friends you want to ask about this user:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogConfirm => 'Ask';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get askFriendsDialogCancel => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendReceivedDescription =>
|
||||||
|
'Your friend just got this as a suggestion and wants to know if he knows this person.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendAddedDescription =>
|
||||||
|
'You have added this user to your contacts.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendHide => 'Hide';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chatAskAFriendRequest => 'Request';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chatAskAFriendUnknownUser(Object userId) {
|
||||||
|
return 'User $userId';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
String contactUserDiscoveryImagesLeft(Object imagesLeft, Object username) {
|
||||||
return '$imagesLeft more images are needed until your friends are shared with $username.';
|
return '$imagesLeft more images are needed until your friends are shared with $username.';
|
||||||
|
|
@ -1807,4 +1918,58 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToZoom => 'Drag to Zoom';
|
String get dragToZoom => 'Drag to Zoom';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get showUsername => 'Show username';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionTitle => 'Choose your setup path';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionSubtitle =>
|
||||||
|
'Choose how you want to configure your security and privacy settings.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultTitle => 'Default';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultDesc =>
|
||||||
|
'Instantly applies recommended settings so you can start using the app.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionDefaultBadge => 'Fast Setup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionCustomizeTitle => 'Customize';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionCustomizeDesc =>
|
||||||
|
'Step-by-step setup so you can decide for yourself.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionStrictTitle => 'Enhanced Protection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileSelectionStrictDesc =>
|
||||||
|
'Maximum anti-phishing defense. Recommended for *journalists & public figures*.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replyFlameRestored => 'Flames restored';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get replyAskAFriend => 'Ask a friend';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningDirectTitle => 'Identity not verified in person';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningGroupTitle =>
|
||||||
|
'Not all members are verified in person';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningBody =>
|
||||||
|
'*Avoid sharing sensitive data*. Risk of *impersonation* without manual verification.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get unverifiedWarningButton => 'Verify now';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93
|
Subproject commit c33a4c3be99b38596abd0cfa91333db3a340dee2
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
part 'userdata.model.g.dart';
|
part 'userdata.model.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
@ -10,9 +11,9 @@ class UserData {
|
||||||
required this.displayName,
|
required this.displayName,
|
||||||
required this.subscriptionPlan,
|
required this.subscriptionPlan,
|
||||||
required this.currentSetupPage,
|
required this.currentSetupPage,
|
||||||
|
required this.appVersion,
|
||||||
});
|
});
|
||||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
factory UserData.fromJson(Map<String, dynamic> json) => _$UserDataFromJson(json);
|
||||||
_$UserDataFromJson(json);
|
|
||||||
|
|
||||||
final int userId;
|
final int userId;
|
||||||
|
|
||||||
|
|
@ -35,6 +36,12 @@ class UserData {
|
||||||
@JsonKey(defaultValue: 0)
|
@JsonKey(defaultValue: 0)
|
||||||
int deviceId = 0;
|
int deviceId = 0;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: SetupProfile.standard)
|
||||||
|
SetupProfile setupProfile = SetupProfile.standard;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: SecurityProfile.normal)
|
||||||
|
SecurityProfile securityProfile = SecurityProfile.normal;
|
||||||
|
|
||||||
// --- SUBSCRIPTION DTA ---
|
// --- SUBSCRIPTION DTA ---
|
||||||
|
|
||||||
@JsonKey(defaultValue: 'Free')
|
@JsonKey(defaultValue: 'Free')
|
||||||
|
|
@ -179,8 +186,7 @@ class TwonlySafeBackup {
|
||||||
required this.backupId,
|
required this.backupId,
|
||||||
required this.encryptionKey,
|
required this.encryptionKey,
|
||||||
});
|
});
|
||||||
factory TwonlySafeBackup.fromJson(Map<String, dynamic> json) =>
|
factory TwonlySafeBackup.fromJson(Map<String, dynamic> json) => _$TwonlySafeBackupFromJson(json);
|
||||||
_$TwonlySafeBackupFromJson(json);
|
|
||||||
|
|
||||||
int lastBackupSize = 0;
|
int lastBackupSize = 0;
|
||||||
LastBackupUploadState backupUploadState = LastBackupUploadState.none;
|
LastBackupUploadState backupUploadState = LastBackupUploadState.none;
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,22 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
displayName: json['displayName'] as String,
|
displayName: json['displayName'] as String,
|
||||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||||
currentSetupPage: json['currentSetupPage'] as String?,
|
currentSetupPage: json['currentSetupPage'] as String?,
|
||||||
|
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
|
||||||
)
|
)
|
||||||
..avatarSvg = json['avatarSvg'] as String?
|
..avatarSvg = json['avatarSvg'] as String?
|
||||||
..avatarJson = json['avatarJson'] as String?
|
..avatarJson = json['avatarJson'] as String?
|
||||||
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
|
|
||||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||||
|
..setupProfile =
|
||||||
|
$enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ??
|
||||||
|
SetupProfile.standard
|
||||||
|
..securityProfile =
|
||||||
|
$enumDecodeNullable(
|
||||||
|
_$SecurityProfileEnumMap,
|
||||||
|
json['securityProfile'],
|
||||||
|
) ??
|
||||||
|
SecurityProfile.normal
|
||||||
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
|
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
|
||||||
..lastImageSend = json['lastImageSend'] == null
|
..lastImageSend = json['lastImageSend'] == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -115,6 +124,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'avatarCounter': instance.avatarCounter,
|
'avatarCounter': instance.avatarCounter,
|
||||||
'isDeveloper': instance.isDeveloper,
|
'isDeveloper': instance.isDeveloper,
|
||||||
'deviceId': instance.deviceId,
|
'deviceId': instance.deviceId,
|
||||||
|
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
|
||||||
|
'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!,
|
||||||
'subscriptionPlan': instance.subscriptionPlan,
|
'subscriptionPlan': instance.subscriptionPlan,
|
||||||
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
|
||||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||||
|
|
@ -168,6 +179,17 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'hasZoomed': instance.hasZoomed,
|
'hasZoomed': instance.hasZoomed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$SetupProfileEnumMap = {
|
||||||
|
SetupProfile.standard: 'standard',
|
||||||
|
SetupProfile.customized: 'customized',
|
||||||
|
SetupProfile.maximum: 'maximum',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$SecurityProfileEnumMap = {
|
||||||
|
SecurityProfile.normal: 'normal',
|
||||||
|
SecurityProfile.strict: 'strict',
|
||||||
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
ThemeMode.system: 'system',
|
ThemeMode.system: 'system',
|
||||||
ThemeMode.light: 'light',
|
ThemeMode.light: 'light',
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@ message AdditionalMessageData {
|
||||||
LINK = 0;
|
LINK = 0;
|
||||||
CONTACTS = 1;
|
CONTACTS = 1;
|
||||||
RESTORED_FLAME_COUNTER = 2;
|
RESTORED_FLAME_COUNTER = 2;
|
||||||
|
ASK_ABOUT_USER = 3;
|
||||||
}
|
}
|
||||||
Type type = 1;
|
Type type = 1;
|
||||||
|
|
||||||
optional string link = 2;
|
optional string link = 2;
|
||||||
repeated SharedContact contacts = 3;
|
repeated SharedContact contacts = 3;
|
||||||
optional int64 restored_flame_counter = 4;
|
optional int64 restored_flame_counter = 4;
|
||||||
|
optional int64 ask_about_user_id = 5;
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +105,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
||||||
$core.String? link,
|
$core.String? link,
|
||||||
$core.Iterable<SharedContact>? contacts,
|
$core.Iterable<SharedContact>? contacts,
|
||||||
$fixnum.Int64? restoredFlameCounter,
|
$fixnum.Int64? restoredFlameCounter,
|
||||||
|
$fixnum.Int64? askAboutUserId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (type != null) result.type = type;
|
if (type != null) result.type = type;
|
||||||
|
|
@ -112,6 +113,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
||||||
if (contacts != null) result.contacts.addAll(contacts);
|
if (contacts != null) result.contacts.addAll(contacts);
|
||||||
if (restoredFlameCounter != null)
|
if (restoredFlameCounter != null)
|
||||||
result.restoredFlameCounter = restoredFlameCounter;
|
result.restoredFlameCounter = restoredFlameCounter;
|
||||||
|
if (askAboutUserId != null) result.askAboutUserId = askAboutUserId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +135,7 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
||||||
..pPM<SharedContact>(3, _omitFieldNames ? '' : 'contacts',
|
..pPM<SharedContact>(3, _omitFieldNames ? '' : 'contacts',
|
||||||
subBuilder: SharedContact.create)
|
subBuilder: SharedContact.create)
|
||||||
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
|
..aInt64(4, _omitFieldNames ? '' : 'restoredFlameCounter')
|
||||||
|
..aInt64(5, _omitFieldNames ? '' : 'askAboutUserId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
|
@ -184,6 +187,15 @@ class AdditionalMessageData extends $pb.GeneratedMessage {
|
||||||
$core.bool hasRestoredFlameCounter() => $_has(3);
|
$core.bool hasRestoredFlameCounter() => $_has(3);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
void clearRestoredFlameCounter() => $_clearField(4);
|
void clearRestoredFlameCounter() => $_clearField(4);
|
||||||
|
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$fixnum.Int64 get askAboutUserId => $_getI64(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
set askAboutUserId($fixnum.Int64 value) => $_setInt64(4, value);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool hasAskAboutUserId() => $_has(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
void clearAskAboutUserId() => $_clearField(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,19 @@ class AdditionalMessageData_Type extends $pb.ProtobufEnum {
|
||||||
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
|
static const AdditionalMessageData_Type RESTORED_FLAME_COUNTER =
|
||||||
AdditionalMessageData_Type._(
|
AdditionalMessageData_Type._(
|
||||||
2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER');
|
2, _omitEnumNames ? '' : 'RESTORED_FLAME_COUNTER');
|
||||||
|
static const AdditionalMessageData_Type ASK_ABOUT_USER =
|
||||||
|
AdditionalMessageData_Type._(3, _omitEnumNames ? '' : 'ASK_ABOUT_USER');
|
||||||
|
|
||||||
static const $core.List<AdditionalMessageData_Type> values =
|
static const $core.List<AdditionalMessageData_Type> values =
|
||||||
<AdditionalMessageData_Type>[
|
<AdditionalMessageData_Type>[
|
||||||
LINK,
|
LINK,
|
||||||
CONTACTS,
|
CONTACTS,
|
||||||
RESTORED_FLAME_COUNTER,
|
RESTORED_FLAME_COUNTER,
|
||||||
|
ASK_ABOUT_USER,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.List<AdditionalMessageData_Type?> _byValue =
|
static final $core.List<AdditionalMessageData_Type?> _byValue =
|
||||||
$pb.ProtobufEnum.$_initByValueList(values, 2);
|
$pb.ProtobufEnum.$_initByValueList(values, 3);
|
||||||
static AdditionalMessageData_Type? valueOf($core.int value) =>
|
static AdditionalMessageData_Type? valueOf($core.int value) =>
|
||||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,21 @@ const AdditionalMessageData$json = {
|
||||||
'10': 'restoredFlameCounter',
|
'10': 'restoredFlameCounter',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'ask_about_user_id',
|
||||||
|
'3': 5,
|
||||||
|
'4': 1,
|
||||||
|
'5': 3,
|
||||||
|
'9': 2,
|
||||||
|
'10': 'askAboutUserId',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'4': [AdditionalMessageData_Type$json],
|
'4': [AdditionalMessageData_Type$json],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_link'},
|
{'1': '_link'},
|
||||||
{'1': '_restored_flame_counter'},
|
{'1': '_restored_flame_counter'},
|
||||||
|
{'1': '_ask_about_user_id'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,6 +92,7 @@ const AdditionalMessageData_Type$json = {
|
||||||
{'1': 'LINK', '2': 0},
|
{'1': 'LINK', '2': 0},
|
||||||
{'1': 'CONTACTS', '2': 1},
|
{'1': 'CONTACTS', '2': 1},
|
||||||
{'1': 'RESTORED_FLAME_COUNTER', '2': 2},
|
{'1': 'RESTORED_FLAME_COUNTER', '2': 2},
|
||||||
|
{'1': 'ASK_ABOUT_USER', '2': 3},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -90,6 +101,7 @@ final $typed_data.Uint8List additionalMessageDataDescriptor = $convert.base64Dec
|
||||||
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
|
'ChVBZGRpdGlvbmFsTWVzc2FnZURhdGESLwoEdHlwZRgBIAEoDjIbLkFkZGl0aW9uYWxNZXNzYW'
|
||||||
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
|
'dlRGF0YS5UeXBlUgR0eXBlEhcKBGxpbmsYAiABKAlIAFIEbGlua4gBARIqCghjb250YWN0cxgD'
|
||||||
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
|
'IAMoCzIOLlNoYXJlZENvbnRhY3RSCGNvbnRhY3RzEjkKFnJlc3RvcmVkX2ZsYW1lX2NvdW50ZX'
|
||||||
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQEiOgoEVHlwZRIICgRMSU5LEAASDAoI'
|
'IYBCABKANIAVIUcmVzdG9yZWRGbGFtZUNvdW50ZXKIAQESLgoRYXNrX2Fib3V0X3VzZXJfaWQY'
|
||||||
'Q09OVEFDVFMQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAJCBwoFX2xpbmtCGQoXX3Jlc3'
|
'BSABKANIAlIOYXNrQWJvdXRVc2VySWSIAQEiTgoEVHlwZRIICgRMSU5LEAASDAoIQ09OVEFDVF'
|
||||||
'RvcmVkX2ZsYW1lX2NvdW50ZXI=');
|
'MQARIaChZSRVNUT1JFRF9GTEFNRV9DT1VOVEVSEAISEgoOQVNLX0FCT1VUX1VTRVIQA0IHCgVf'
|
||||||
|
'bGlua0IZChdfcmVzdG9yZWRfZmxhbWVfY291bnRlckIUChJfYXNrX2Fib3V0X3VzZXJfaWQ=');
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
$core.List<$core.int>? signedPrekeySignature,
|
$core.List<$core.int>? signedPrekeySignature,
|
||||||
$fixnum.Int64? signedPrekeyId,
|
$fixnum.Int64? signedPrekeyId,
|
||||||
$core.List<$core.int>? secretVerificationToken,
|
$core.List<$core.int>? secretVerificationToken,
|
||||||
|
$fixnum.Int64? timestamp,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (userId != null) result.userId = userId;
|
if (userId != null) result.userId = userId;
|
||||||
|
|
@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
||||||
if (secretVerificationToken != null)
|
if (secretVerificationToken != null)
|
||||||
result.secretVerificationToken = secretVerificationToken;
|
result.secretVerificationToken = secretVerificationToken;
|
||||||
|
if (timestamp != null) result.timestamp = timestamp;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
||||||
|
..aInt64(9, _omitFieldNames ? '' : 'timestamp')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
|
@ -230,6 +233,15 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
$core.bool hasSecretVerificationToken() => $_has(7);
|
$core.bool hasSecretVerificationToken() => $_has(7);
|
||||||
@$pb.TagNumber(8)
|
@$pb.TagNumber(8)
|
||||||
void clearSecretVerificationToken() => $_clearField(8);
|
void clearSecretVerificationToken() => $_clearField(8);
|
||||||
|
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
$fixnum.Int64 get timestamp => $_getI64(8);
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
set timestamp($fixnum.Int64 value) => $_setInt64(8, value);
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
$core.bool hasTimestamp() => $_has(8);
|
||||||
|
@$pb.TagNumber(9)
|
||||||
|
void clearTimestamp() => $_clearField(9);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,19 @@ const PublicProfile$json = {
|
||||||
'10': 'secretVerificationToken',
|
'10': 'secretVerificationToken',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'timestamp',
|
||||||
|
'3': 9,
|
||||||
|
'4': 1,
|
||||||
|
'5': 3,
|
||||||
|
'9': 1,
|
||||||
|
'10': 'timestamp',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_secret_verification_token'},
|
{'1': '_secret_verification_token'},
|
||||||
|
{'1': '_timestamp'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
|
||||||
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
||||||
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
||||||
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
||||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');
|
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX'
|
||||||
|
'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA==');
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,5 @@ message PublicProfile {
|
||||||
bytes signed_prekey_signature = 6;
|
bytes signed_prekey_signature = 6;
|
||||||
int64 signed_prekey_id = 7;
|
int64 signed_prekey_id = 7;
|
||||||
optional bytes secret_verification_token = 8;
|
optional bytes secret_verification_token = 8;
|
||||||
|
optional int64 timestamp = 9;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
|
@ -38,6 +39,7 @@ import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/privacy/profile_selection.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
|
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
|
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
|
||||||
|
|
@ -47,7 +49,10 @@ import 'package:twonly/src/visual/views/settings/subscription/subscription.view.
|
||||||
import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
|
import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
|
||||||
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
final routerProvider = GoRouter(
|
final routerProvider = GoRouter(
|
||||||
|
navigatorKey: rootNavigatorKey,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.home,
|
path: Routes.home,
|
||||||
|
|
@ -201,6 +206,10 @@ final routerProvider = GoRouter(
|
||||||
path: 'user_discovery',
|
path: 'user_discovery',
|
||||||
builder: (context, state) => const UserDiscoverySettingsView(),
|
builder: (context, state) => const UserDiscoverySettingsView(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'profile_selection',
|
||||||
|
builder: (context, state) => const ProfileSelectionSettingsView(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server;
|
||||||
as server;
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||||
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
|
|
@ -66,15 +65,13 @@ class ApiService {
|
||||||
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
||||||
|
|
||||||
final _connectionStateController = StreamController<bool>.broadcast();
|
final _connectionStateController = StreamController<bool>.broadcast();
|
||||||
Stream<bool> get onConnectionStateUpdated =>
|
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream;
|
||||||
_connectionStateController.stream;
|
|
||||||
|
|
||||||
final _appOutdatedController = StreamController<void>.broadcast();
|
final _appOutdatedController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
||||||
|
|
||||||
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onNewDeviceRegistered =>
|
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream;
|
||||||
_newDeviceRegisteredController.stream;
|
|
||||||
|
|
||||||
bool appIsOutdated = false;
|
bool appIsOutdated = false;
|
||||||
bool isAuthenticated = false;
|
bool isAuthenticated = false;
|
||||||
|
|
@ -83,8 +80,7 @@ class ApiService {
|
||||||
Timer? reconnectionTimer;
|
Timer? reconnectionTimer;
|
||||||
int _reconnectionDelay = 5;
|
int _reconnectionDelay = 5;
|
||||||
|
|
||||||
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests =
|
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests = HashMap();
|
||||||
HashMap();
|
|
||||||
IOWebSocketChannel? _channel;
|
IOWebSocketChannel? _channel;
|
||||||
// ignore: cancel_subscriptions
|
// ignore: cancel_subscriptions
|
||||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||||
|
|
@ -96,12 +92,20 @@ class ApiService {
|
||||||
Uri.parse(apiUrl),
|
Uri.parse(apiUrl),
|
||||||
pingInterval: const Duration(seconds: 30),
|
pingInterval: const Duration(seconds: 30),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await channel.ready.timeout(const Duration(seconds: 10));
|
||||||
|
} catch (e) {
|
||||||
|
channel.sink.close().ignore();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
_channel = channel;
|
_channel = channel;
|
||||||
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
|
||||||
await _channel!.ready;
|
|
||||||
Log.info('websocket connected to $apiUrl');
|
Log.info('websocket connected to $apiUrl');
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
|
_channel = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +152,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onClosed() async {
|
Future<void> onClosed() async {
|
||||||
|
if (_channel == null) return;
|
||||||
Log.info('websocket connection closed');
|
Log.info('websocket connection closed');
|
||||||
_channel = null;
|
_channel = null;
|
||||||
isAuthenticated = false;
|
isAuthenticated = false;
|
||||||
|
|
@ -179,15 +184,19 @@ class ApiService {
|
||||||
_reconnectionDelay = 3;
|
_reconnectionDelay = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close(Function callback) async {
|
Future<void> close(Function? callback) async {
|
||||||
Log.info('closing websocket connection');
|
Log.info('closing websocket connection');
|
||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
await _channel!.sink.close();
|
try {
|
||||||
|
await _channel!.sink.close().timeout(const Duration(seconds: 2));
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn('Timeout or error closing websocket: $e');
|
||||||
|
}
|
||||||
await onClosed();
|
await onClosed();
|
||||||
callback();
|
callback?.call();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback();
|
callback?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> listenToNetworkChanges() async {
|
Future<void> listenToNetworkChanges() async {
|
||||||
|
|
@ -245,7 +254,10 @@ class ApiService {
|
||||||
|
|
||||||
Future<void> _onData(dynamic msgBuffer) async {
|
Future<void> _onData(dynamic msgBuffer) async {
|
||||||
try {
|
try {
|
||||||
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
|
if (msgBuffer is! Uint8List) {
|
||||||
|
msgBuffer = Uint8List.fromList(msgBuffer as List<int>);
|
||||||
|
}
|
||||||
|
final msg = server.ServerToClient.fromBuffer(msgBuffer);
|
||||||
if (msg.v0.hasResponse()) {
|
if (msg.v0.hasResponse()) {
|
||||||
final completer = _pendingRequests.remove(msg.v0.seq);
|
final completer = _pendingRequests.remove(msg.v0.seq);
|
||||||
if (completer != null && !completer.isCompleted) {
|
if (completer != null && !completer.isCompleted) {
|
||||||
|
|
@ -406,9 +418,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
||||||
Log.warn('Contact deleted their account $contactId.');
|
Log.warn('Contact deleted their account $contactId.');
|
||||||
final contact = await twonlyDB.contactsDao
|
final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull();
|
||||||
.getContactByUserId(contactId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
await twonlyDB.contactsDao.updateContact(
|
await twonlyDB.contactsDao.updateContact(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -473,8 +483,7 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid &&
|
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
result.error != ErrorCode.ForegroundSessionConnected) {
|
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -512,8 +521,7 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid &&
|
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
result.error != ErrorCode.ForegroundSessionConnected) {
|
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -545,8 +553,7 @@ class ApiService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final handshake = Handshake()
|
final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||||
..getAuthChallenge = Handshake_GetAuthChallenge();
|
|
||||||
final req = createClientToServerFromHandshake(handshake);
|
final req = createClientToServerFromHandshake(handshake);
|
||||||
|
|
||||||
final result = await sendRequestSync(req, authenticated: false);
|
final result = await sendRequestSync(req, authenticated: false);
|
||||||
|
|
@ -611,9 +618,7 @@ class ApiService {
|
||||||
|
|
||||||
final register = Handshake_Register()
|
final register = Handshake_Register()
|
||||||
..username = username
|
..username = username
|
||||||
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
|
..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
|
||||||
.getPublicKey()
|
|
||||||
.serialize()
|
|
||||||
..registrationId = Int64(signalIdentity.registrationId)
|
..registrationId = Int64(signalIdentity.registrationId)
|
||||||
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
||||||
..signedPrekeySignature = signedPreKey.signature
|
..signedPrekeySignature = signedPreKey.signature
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ Future<void> handleAdditionalDataMessage(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_AdditionalDataMessage message,
|
EncryptedContent_AdditionalDataMessage message,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got a additional data message: ${message.senderMessageId} from $groupId',
|
'[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent message overwrite: reject if a message with this ID already
|
// Prevent message overwrite: reject if a message with this ID already
|
||||||
|
|
@ -22,7 +23,7 @@ Future<void> handleAdditionalDataMessage(
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (existing != null && existing.senderId != fromUserId) {
|
if (existing != null && existing.senderId != fromUserId) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +46,6 @@ Future<void> handleAdditionalDataMessage(
|
||||||
fromTimestamp(message.timestamp),
|
fromTimestamp(message.timestamp),
|
||||||
);
|
);
|
||||||
if (msg != null) {
|
if (msg != null) {
|
||||||
Log.info('Inserted a new text message with ID: ${msg.messageId}');
|
Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
||||||
await handleContactAccept(fromUserId);
|
await handleContactAccept(fromUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// contact was already accepted, so just accept the request in the background.
|
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
contact.userId,
|
contact.userId,
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
|
|
@ -36,6 +35,7 @@ Future<bool> handleNewContactRequest(int fromUserId) async {
|
||||||
type: EncryptedContent_ContactRequest_Type.ACCEPT,
|
type: EncryptedContent_ContactRequest_Type.ACCEPT,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blocking: false,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -88,16 +88,17 @@ Future<void> handleContactAccept(int fromUserId) async {
|
||||||
Future<bool> handleContactRequest(
|
Future<bool> handleContactRequest(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_ContactRequest contactRequest,
|
EncryptedContent_ContactRequest contactRequest,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
switch (contactRequest.type) {
|
switch (contactRequest.type) {
|
||||||
case EncryptedContent_ContactRequest_Type.REQUEST:
|
case EncryptedContent_ContactRequest_Type.REQUEST:
|
||||||
Log.info('Got a contact request from $fromUserId');
|
Log.info('[$receiptId] Got a contact request from $fromUserId');
|
||||||
return handleNewContactRequest(fromUserId);
|
return handleNewContactRequest(fromUserId);
|
||||||
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
case EncryptedContent_ContactRequest_Type.ACCEPT:
|
||||||
Log.info('Got a contact accept from $fromUserId');
|
Log.info('[$receiptId] Got a contact accept from $fromUserId');
|
||||||
await handleContactAccept(fromUserId);
|
await handleContactAccept(fromUserId);
|
||||||
case EncryptedContent_ContactRequest_Type.REJECT:
|
case EncryptedContent_ContactRequest_Type.REJECT:
|
||||||
Log.info('Got a contact reject from $fromUserId');
|
Log.info('[$receiptId] Got a contact reject from $fromUserId');
|
||||||
await twonlyDB.contactsDao.updateContact(
|
await twonlyDB.contactsDao.updateContact(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
const ContactsCompanion(
|
const ContactsCompanion(
|
||||||
|
|
@ -114,14 +115,15 @@ Future<void> handleContactUpdate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_ContactUpdate contactUpdate,
|
EncryptedContent_ContactUpdate contactUpdate,
|
||||||
int? senderProfileCounter,
|
int? senderProfileCounter,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
switch (contactUpdate.type) {
|
switch (contactUpdate.type) {
|
||||||
case EncryptedContent_ContactUpdate_Type.REQUEST:
|
case EncryptedContent_ContactUpdate_Type.REQUEST:
|
||||||
Log.info('Got a contact update request from $fromUserId');
|
Log.info('[$receiptId] Got a contact update request from $fromUserId');
|
||||||
await sendContactMyProfileData(fromUserId);
|
await sendContactMyProfileData(fromUserId);
|
||||||
|
|
||||||
case EncryptedContent_ContactUpdate_Type.UPDATE:
|
case EncryptedContent_ContactUpdate_Type.UPDATE:
|
||||||
Log.info('Got a contact update $fromUserId');
|
Log.info('[$receiptId] Got a contact update $fromUserId');
|
||||||
Uint8List? avatarSvgCompressed;
|
Uint8List? avatarSvgCompressed;
|
||||||
if (contactUpdate.hasAvatarSvgCompressed()) {
|
if (contactUpdate.hasAvatarSvgCompressed()) {
|
||||||
avatarSvgCompressed = Uint8List.fromList(
|
avatarSvgCompressed = Uint8List.fromList(
|
||||||
|
|
@ -188,8 +190,9 @@ Future<void> handleContactUpdate(
|
||||||
Future<void> handleFlameSync(
|
Future<void> handleFlameSync(
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_FlameSync flameSync,
|
EncryptedContent_FlameSync flameSync,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info('Got a flameSync for group $groupId');
|
Log.info('[$receiptId] Got a flameSync for group $groupId');
|
||||||
|
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
if (group == null || group.lastFlameCounterChange == null) return;
|
if (group == null || group.lastFlameCounterChange == null) return;
|
||||||
|
|
@ -235,6 +238,7 @@ Future<int?> checkForProfileUpdate(
|
||||||
type: EncryptedContent_ContactUpdate_Type.REQUEST,
|
type: EncryptedContent_ContactUpdate_Type.REQUEST,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blocking: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
Future<void> handleErrorMessage(
|
Future<void> handleErrorMessage(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_ErrorMessages error,
|
EncryptedContent_ErrorMessages error,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.error('Got error from $fromUserId: $error');
|
Log.error('[$receiptId] Got error from $fromUserId: $error');
|
||||||
|
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case EncryptedContent_ErrorMessages_Type
|
case EncryptedContent_ErrorMessages_Type
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,13 @@ Future<void> handleGroupCreate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_GroupCreate newGroup,
|
EncryptedContent_GroupCreate newGroup,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
final user = await twonlyDB.contactsDao
|
final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull();
|
||||||
.getContactByUserId(fromUserId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
// Only contacts can invite other contacts, so this can (via the UI) not happen.
|
// Only contacts can invite other contacts, so this can (via the UI) not happen.
|
||||||
Log.error(
|
Log.error(
|
||||||
'User is not a contact. Aborting.',
|
'[$receiptId] User is not a contact. Aborting.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +65,7 @@ Future<void> handleGroupCreate(
|
||||||
|
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Could not create new group. Probably because the group already existed.',
|
'[$receiptId] Could not create new group. Probably because the group already existed.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -108,12 +107,13 @@ Future<void> handleGroupUpdate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_GroupUpdate update,
|
EncryptedContent_GroupUpdate update,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info('Got group update for $groupId from $fromUserId');
|
Log.info('[$receiptId] Got group update for $groupId from $fromUserId');
|
||||||
|
|
||||||
final actionType = groupActionTypeFromString(update.groupActionType);
|
final actionType = groupActionTypeFromString(update.groupActionType);
|
||||||
if (actionType == null) {
|
if (actionType == null) {
|
||||||
Log.error('Group action ${update.groupActionType} is unknown ignoring.');
|
Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,10 +189,11 @@ Future<bool> handleGroupJoin(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_GroupJoin join,
|
EncryptedContent_GroupJoin join,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
|
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
|
||||||
if (!await addNewHiddenContact(fromUserId)) {
|
if (!await addNewHiddenContact(fromUserId)) {
|
||||||
Log.error('Got group join, but could not load contact.');
|
Log.error('[$receiptId] Got group join, but could not load contact.');
|
||||||
// This can happen in case the group join was received before the group create.
|
// This can happen in case the group join was received before the group create.
|
||||||
// In this case return false, which will cause the receipt to fail and the user
|
// In this case return false, which will cause the receipt to fail and the user
|
||||||
// will resend this message.
|
// will resend this message.
|
||||||
|
|
@ -213,6 +214,7 @@ Future<void> handleResendGroupPublicKey(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_GroupJoin join,
|
EncryptedContent_GroupJoin join,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
if (group == null || group.myGroupPrivateKey == null) return;
|
if (group == null || group.myGroupPrivateKey == null) return;
|
||||||
|
|
@ -225,6 +227,7 @@ Future<void> handleResendGroupPublicKey(
|
||||||
groupPublicKey: keyPair.getPublicKey().serialize(),
|
groupPublicKey: keyPair.getPublicKey().serialize(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blocking: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,6 +235,7 @@ Future<void> handleTypingIndicator(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_TypingIndicator indicator,
|
EncryptedContent_TypingIndicator indicator,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
var lastTypeIndicator = const Value<DateTime?>.absent();
|
var lastTypeIndicator = const Value<DateTime?>.absent();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ Future<void> handleMedia(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_Media media,
|
EncryptedContent_Media media,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
|
'[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
|
||||||
);
|
);
|
||||||
|
|
||||||
late MediaType mediaType;
|
late MediaType mediaType;
|
||||||
|
|
@ -33,7 +34,7 @@ Future<void> handleMedia(
|
||||||
message.senderId != fromUserId ||
|
message.senderId != fromUserId ||
|
||||||
message.mediaId == null) {
|
message.mediaId == null) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
'[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -82,13 +83,13 @@ Future<void> handleMedia(
|
||||||
if (messageTmp != null) {
|
if (messageTmp != null) {
|
||||||
if (messageTmp.senderId != fromUserId) {
|
if (messageTmp.senderId != fromUserId) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
'[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (messageTmp.mediaId == null) {
|
if (messageTmp.mediaId == null) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'This message already exit without a mediaId. Message is dropped.',
|
'[$receiptId] This message already exit without a mediaId. Message is dropped.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +98,7 @@ Future<void> handleMedia(
|
||||||
);
|
);
|
||||||
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
|
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'This message and media file already exit and was not requested again. Dropping it.',
|
'[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +122,9 @@ Future<void> handleMedia(
|
||||||
MediaFile? mediaFile;
|
MediaFile? mediaFile;
|
||||||
Message? message;
|
Message? message;
|
||||||
|
|
||||||
Log.info('Starting transaction for media message ${media.senderMessageId}');
|
Log.info(
|
||||||
|
'[$receiptId] Starting transaction for media message ${media.senderMessageId}',
|
||||||
|
);
|
||||||
await twonlyDB.transaction(() async {
|
await twonlyDB.transaction(() async {
|
||||||
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
|
|
@ -141,7 +144,7 @@ Future<void> handleMedia(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mediaFile == null) {
|
if (mediaFile == null) {
|
||||||
Log.error('Could not insert media file into database');
|
Log.error('[$receiptId] Could not insert media file into database');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +168,7 @@ Future<void> handleMedia(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Log.info(
|
Log.info(
|
||||||
'Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
|
'[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (message != null && mediaFile != null) {
|
if (message != null && mediaFile != null) {
|
||||||
|
|
@ -173,7 +176,9 @@ Future<void> handleMedia(
|
||||||
groupId,
|
groupId,
|
||||||
fromTimestamp(media.timestamp),
|
fromTimestamp(media.timestamp),
|
||||||
);
|
);
|
||||||
Log.info('Inserted a new media message with ID: ${message!.messageId}');
|
Log.info(
|
||||||
|
'[$receiptId] Inserted a new media message with ID: ${message!.messageId}',
|
||||||
|
);
|
||||||
await incFlameCounter(
|
await incFlameCounter(
|
||||||
message!.groupId,
|
message!.groupId,
|
||||||
true,
|
true,
|
||||||
|
|
@ -184,12 +189,16 @@ Future<void> handleMedia(
|
||||||
} else {
|
} else {
|
||||||
if (mediaFile == null && message == null) {
|
if (mediaFile == null && message == null) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Could not insert new message as both the message and mediaFile are empty.',
|
'[$receiptId] Could not insert new message as both the message and mediaFile are empty.',
|
||||||
);
|
);
|
||||||
} else if (mediaFile == null) {
|
} else if (mediaFile == null) {
|
||||||
Log.error('Could not insert new message as the mediaFile is empty.');
|
Log.error(
|
||||||
|
'[$receiptId] Could not insert new message as the mediaFile is empty.',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.error('Could not insert new message as the message is empty.');
|
Log.error(
|
||||||
|
'[$receiptId] Could not insert new message as the message is empty.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +206,7 @@ Future<void> handleMedia(
|
||||||
Future<void> handleMediaUpdate(
|
Future<void> handleMediaUpdate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_MediaUpdate mediaUpdate,
|
EncryptedContent_MediaUpdate mediaUpdate,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
final message = await twonlyDB.messagesDao
|
final message = await twonlyDB.messagesDao
|
||||||
.getMessageById(mediaUpdate.targetMessageId)
|
.getMessageById(mediaUpdate.targetMessageId)
|
||||||
|
|
@ -204,14 +214,14 @@ Future<void> handleMediaUpdate(
|
||||||
if (message == null) {
|
if (message == null) {
|
||||||
// this can happen, in case the message was already deleted.
|
// this can happen, in case the message was already deleted.
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
|
'[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.mediaId == null) {
|
if (message.mediaId == null) {
|
||||||
// this can happen, in case the message was already deleted.
|
// this can happen, in case the message was already deleted.
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
|
'[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -220,14 +230,14 @@ Future<void> handleMediaUpdate(
|
||||||
);
|
);
|
||||||
if (mediaFile == null) {
|
if (mediaFile == null) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got media file update, but media file was not found ${message.mediaId}',
|
'[$receiptId] Got media file update, but media file was not found ${message.mediaId}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mediaUpdate.type) {
|
switch (mediaUpdate.type) {
|
||||||
case EncryptedContent_MediaUpdate_Type.REOPENED:
|
case EncryptedContent_MediaUpdate_Type.REOPENED:
|
||||||
Log.info('Got media file reopened ${mediaFile.mediaId}');
|
Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}');
|
||||||
await twonlyDB.messagesDao.updateMessageId(
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
message.messageId,
|
message.messageId,
|
||||||
const MessagesCompanion(
|
const MessagesCompanion(
|
||||||
|
|
@ -235,7 +245,7 @@ Future<void> handleMediaUpdate(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case EncryptedContent_MediaUpdate_Type.STORED:
|
case EncryptedContent_MediaUpdate_Type.STORED:
|
||||||
Log.info('Got media file stored ${mediaFile.mediaId}');
|
Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}');
|
||||||
final mediaService = MediaFileService(mediaFile);
|
final mediaService = MediaFileService(mediaFile);
|
||||||
await mediaService.storeMediaFile();
|
await mediaService.storeMediaFile();
|
||||||
await twonlyDB.messagesDao.updateMessageId(
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
|
|
@ -246,7 +256,9 @@ Future<void> handleMediaUpdate(
|
||||||
);
|
);
|
||||||
|
|
||||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
Log.info(
|
||||||
|
'[$receiptId] Got media file decryption error ${mediaFile.mediaId}',
|
||||||
|
);
|
||||||
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
|
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
Future<void> handleMessageUpdate(
|
Future<void> handleMessageUpdate(
|
||||||
int contactId,
|
int contactId,
|
||||||
EncryptedContent_MessageUpdate messageUpdate,
|
EncryptedContent_MessageUpdate messageUpdate,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
switch (messageUpdate.type) {
|
switch (messageUpdate.type) {
|
||||||
case EncryptedContent_MessageUpdate_Type.OPENED:
|
case EncryptedContent_MessageUpdate_Type.OPENED:
|
||||||
Log.info(
|
Log.info(
|
||||||
'Opened message ${messageUpdate.multipleTargetMessageIds}',
|
'[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}',
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await twonlyDB.messagesDao.handleMessagesOpened(
|
await twonlyDB.messagesDao.handleMessagesOpened(
|
||||||
|
|
@ -20,13 +21,13 @@ Future<void> handleMessageUpdate(
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn(e);
|
Log.warn('[$receiptId] Error handling messages opened: $e');
|
||||||
}
|
}
|
||||||
case EncryptedContent_MessageUpdate_Type.DELETE:
|
case EncryptedContent_MessageUpdate_Type.DELETE:
|
||||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('Delete message ${messageUpdate.senderMessageId}');
|
Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}');
|
||||||
try {
|
try {
|
||||||
await twonlyDB.messagesDao.handleMessageDeletion(
|
await twonlyDB.messagesDao.handleMessageDeletion(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -34,13 +35,13 @@ Future<void> handleMessageUpdate(
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn(e);
|
Log.warn('[$receiptId] Error handling message deletion: $e');
|
||||||
}
|
}
|
||||||
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
||||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
if (!await isSender(contactId, messageUpdate.senderMessageId, receiptId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('Edit message ${messageUpdate.senderMessageId}');
|
Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}');
|
||||||
try {
|
try {
|
||||||
await twonlyDB.messagesDao.handleTextEdit(
|
await twonlyDB.messagesDao.handleTextEdit(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -49,12 +50,12 @@ Future<void> handleMessageUpdate(
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn(e);
|
Log.warn('[$receiptId] Error handling text edit: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isSender(int fromUserId, String messageId) async {
|
Future<bool> isSender(int fromUserId, String messageId, String receiptId) async {
|
||||||
final message = await twonlyDB.messagesDao
|
final message = await twonlyDB.messagesDao
|
||||||
.getMessageById(messageId)
|
.getMessageById(messageId)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
@ -62,6 +63,6 @@ Future<bool> isSender(int fromUserId, String messageId) async {
|
||||||
if (message.senderId == fromUserId) {
|
if (message.senderId == fromUserId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Log.error('Contact $fromUserId tried to modify the message $messageId');
|
Log.error('[$receiptId] Contact $fromUserId tried to modify the message $messageId');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
||||||
Future<void> handlePushKey(
|
Future<void> handlePushKey(
|
||||||
int contactId,
|
int contactId,
|
||||||
EncryptedContent_PushKeys pushKeys,
|
EncryptedContent_PushKeys pushKeys,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
switch (pushKeys.type) {
|
switch (pushKeys.type) {
|
||||||
case EncryptedContent_PushKeys_Type.REQUEST:
|
case EncryptedContent_PushKeys_Type.REQUEST:
|
||||||
Log.info('Got a pushkey request from $contactId');
|
Log.info('[$receiptId] Got a pushkey request from $contactId');
|
||||||
if (lastPushKeyRequest.isBefore(
|
if (lastPushKeyRequest.isBefore(
|
||||||
clock.now().subtract(const Duration(seconds: 60)),
|
clock.now().subtract(const Duration(seconds: 60)),
|
||||||
)) {
|
)) {
|
||||||
|
|
@ -22,7 +23,7 @@ Future<void> handlePushKey(
|
||||||
}
|
}
|
||||||
|
|
||||||
case EncryptedContent_PushKeys_Type.UPDATE:
|
case EncryptedContent_PushKeys_Type.UPDATE:
|
||||||
Log.info('Got a pushkey update from $contactId');
|
Log.info('[$receiptId] Got a pushkey update from $contactId');
|
||||||
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
|
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ Future<void> handleReaction(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_Reaction reaction,
|
EncryptedContent_Reaction reaction,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info('Got a reaction from $fromUserId (remove=${reaction.remove})');
|
Log.info('[$receiptId] Got a reaction from $fromUserId (remove=${reaction.remove})');
|
||||||
await twonlyDB.reactionsDao.updateReaction(
|
await twonlyDB.reactionsDao.updateReaction(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
reaction.targetMessageId,
|
reaction.targetMessageId,
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ Future<void> handleTextMessage(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
String groupId,
|
||||||
EncryptedContent_TextMessage textMessage,
|
EncryptedContent_TextMessage textMessage,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got a text message: ${textMessage.senderMessageId} from $groupId',
|
'[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent message overwrite: reject if a message with this ID already
|
// Prevent message overwrite: reject if a message with this ID already
|
||||||
|
|
@ -23,7 +24,7 @@ Future<void> handleTextMessage(
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (existing != null && existing.senderId != fromUserId) {
|
if (existing != null && existing.senderId != fromUserId) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'$fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +48,6 @@ Future<void> handleTextMessage(
|
||||||
fromTimestamp(textMessage.timestamp),
|
fromTimestamp(textMessage.timestamp),
|
||||||
);
|
);
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
Log.info('Inserted a new text message with ID: ${message.messageId}');
|
Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ void resetUserDiscoveryRequestUpdates() {
|
||||||
Future<void> checkForUserDiscoveryChanges(
|
Future<void> checkForUserDiscoveryChanges(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
List<int> receivedVersion,
|
List<int> receivedVersion,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
|
Log.info('[$receiptId] Checking for a new user discovery version.');
|
||||||
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
|
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
receivedVersion,
|
receivedVersion,
|
||||||
|
|
@ -26,7 +28,7 @@ Future<void> checkForUserDiscoveryChanges(
|
||||||
// Only request a new version once per app session
|
// Only request a new version once per app session
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('Having old version from contact. Requesting new version.');
|
Log.info('[$receiptId] Having old version from contact. Requesting new version.');
|
||||||
_requestedUpdates.add(fromUserId);
|
_requestedUpdates.add(fromUserId);
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
@ -35,6 +37,7 @@ Future<void> checkForUserDiscoveryChanges(
|
||||||
currentVersion: currentVersion.toList(),
|
currentVersion: currentVersion.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blocking: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,18 +45,19 @@ Future<void> checkForUserDiscoveryChanges(
|
||||||
Future<void> handleUserDiscoveryRequest(
|
Future<void> handleUserDiscoveryRequest(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_UserDiscoveryRequest request,
|
EncryptedContent_UserDiscoveryRequest request,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
Log.info('Got a user discovery request');
|
Log.info('[$receiptId] Got a user discovery request');
|
||||||
|
|
||||||
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
Log.warn('Got a user discovery request while it is disabled');
|
Log.warn('[$receiptId] Got a user discovery request while it is disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||||
|
|
||||||
if (!UserDiscoveryService.isContactAllowed(contact)) {
|
if (!UserDiscoveryService.isContactAllowed(contact)) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
'Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
|
'[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +67,7 @@ Future<void> handleUserDiscoveryRequest(
|
||||||
request.currentVersion,
|
request.currentVersion,
|
||||||
);
|
);
|
||||||
if (newMessages != null && newMessages.isNotEmpty) {
|
if (newMessages != null && newMessages.isNotEmpty) {
|
||||||
Log.info('Sending ${newMessages.length} user discovery messages');
|
Log.info('[$receiptId] Sending ${newMessages.length} user discovery messages');
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
|
|
@ -71,21 +75,23 @@ Future<void> handleUserDiscoveryRequest(
|
||||||
messages: newMessages,
|
messages: newMessages,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
blocking: false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.info('Got update request, but there are no new updates for the user');
|
Log.info('[$receiptId] Got update request, but there are no new updates for the user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleUserDiscoveryUpdate(
|
Future<void> handleUserDiscoveryUpdate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
EncryptedContent_UserDiscoveryUpdate update,
|
EncryptedContent_UserDiscoveryUpdate update,
|
||||||
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
Log.warn('Got a user discovery update while it is disabled');
|
Log.warn('[$receiptId] Got a user discovery update while it is disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('Got ${update.messages.length} user discovery messages');
|
Log.info('[$receiptId] Got ${update.messages.length} user discovery messages');
|
||||||
await UserDiscoveryService.handleNewMessages(
|
await UserDiscoveryService.handleNewMessages(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
update.messages.map(Uint8List.fromList).toList(),
|
update.messages.map(Uint8List.fromList).toList(),
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ Future<void> _protectMediaUpload(
|
||||||
) async {
|
) async {
|
||||||
final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new);
|
final mutex = _uploadMutexes.putIfAbsent(mediaId, Mutex.new);
|
||||||
await mutex.protect(action);
|
await mutex.protect(action);
|
||||||
_uploadMutexes.remove(mediaId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reuploadMediaFiles() async {
|
Future<void> reuploadMediaFiles() async {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ Future<void> retransmitAllMessages() async {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<String, Mutex> _tryToSendLocks = {};
|
||||||
|
|
||||||
// When the ackByServerAt is set this value is written in the receipted
|
// When the ackByServerAt is set this value is written in the receipted
|
||||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
String? receiptId,
|
String? receiptId,
|
||||||
|
|
@ -68,15 +70,41 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
bool onlyReturnEncryptedData = false,
|
bool onlyReturnEncryptedData = false,
|
||||||
bool blocking = true,
|
bool blocking = true,
|
||||||
}) async {
|
}) async {
|
||||||
|
final rId = receiptId ?? receipt?.receiptId;
|
||||||
|
if (rId == null) {
|
||||||
|
Log.error(
|
||||||
|
'Cannot try to send complete message as both receiptId and receipt are null.',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mutex = _tryToSendLocks.putIfAbsent(rId, Mutex.new);
|
||||||
|
return mutex.protect(() async {
|
||||||
|
return _tryToSendCompleteMessageInternal(
|
||||||
|
receiptId: receiptId,
|
||||||
|
receipt: receipt,
|
||||||
|
onlyReturnEncryptedData: onlyReturnEncryptedData,
|
||||||
|
blocking: blocking,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
|
||||||
|
String? receiptId,
|
||||||
|
Receipt? receipt,
|
||||||
|
bool onlyReturnEncryptedData = false,
|
||||||
|
bool blocking = true,
|
||||||
|
}) async {
|
||||||
|
// this should have a lock for every receiptID, split the function into a _internal withou the lock and a normal with the lock
|
||||||
if (apiService.appIsOutdated) return null;
|
if (apiService.appIsOutdated) return null;
|
||||||
|
if (receiptId == null && receipt == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (receiptId == null && receipt == null) return null;
|
|
||||||
if (receipt == null) {
|
if (receipt == null) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
|
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
|
||||||
if (receipt == null) {
|
if (receipt == null) {
|
||||||
Log.warn('Receipt not found.');
|
Log.warn('[$receiptId] Receipt not found.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +148,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
message.encryptedContent,
|
message.encryptedContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
Log.info('Uploading ${receipt.receiptId}.');
|
Log.info('Uploading message with receiptID ${receipt.receiptId}.');
|
||||||
|
|
||||||
Uint8List? pushData;
|
Uint8List? pushData;
|
||||||
if (receipt.retryCount == 0) {
|
if (receipt.retryCount == 0) {
|
||||||
|
|
@ -176,7 +204,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resp.isError) {
|
if (resp.isError) {
|
||||||
Log.warn('Could not transmit message got ${resp.error}.');
|
Log.warn('Could not transmit ${receipt.receiptId} got ${resp.error}.');
|
||||||
if (resp.error == ErrorCode.UserIdNotFound) {
|
if (resp.error == ErrorCode.UserIdNotFound) {
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||||
await twonlyDB.contactsDao.updateContact(
|
await twonlyDB.contactsDao.updateContact(
|
||||||
|
|
@ -210,7 +238,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Unknown Error when sending message: $e');
|
Log.error('[$receiptId] unknown error when sending message: $e');
|
||||||
if (receipt != null) {
|
if (receipt != null) {
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||||
}
|
}
|
||||||
|
|
@ -316,6 +344,52 @@ Future<void> insertAndSendContactShareMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> insertAndSendAskAboutUserMessage(
|
||||||
|
int contactId,
|
||||||
|
int askAboutUserId,
|
||||||
|
) async {
|
||||||
|
final directChat = await twonlyDB.groupsDao.createOrGetDirectChat(contactId);
|
||||||
|
if (directChat == null) {
|
||||||
|
Log.error('Failed to get or create direct chat group for contact $contactId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final groupId = directChat.groupId;
|
||||||
|
|
||||||
|
final additionalMessageData = AdditionalMessageData(
|
||||||
|
type: AdditionalMessageData_Type.ASK_ABOUT_USER,
|
||||||
|
askAboutUserId: Int64(askAboutUserId),
|
||||||
|
);
|
||||||
|
|
||||||
|
final message = await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion(
|
||||||
|
groupId: Value(groupId),
|
||||||
|
type: Value(MessageType.askAboutUser.name),
|
||||||
|
additionalMessageData: Value(additionalMessageData.writeToBuffer()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (message == null) {
|
||||||
|
Log.error('Could not insert message into database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final encryptedContent = pb.EncryptedContent(
|
||||||
|
additionalDataMessage: pb.EncryptedContent_AdditionalDataMessage(
|
||||||
|
senderMessageId: message.messageId,
|
||||||
|
additionalMessageData: additionalMessageData.writeToBuffer(),
|
||||||
|
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||||
|
type: MessageType.askAboutUser.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendCipherTextToGroup(
|
||||||
|
groupId,
|
||||||
|
encryptedContent,
|
||||||
|
messageId: message.messageId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> sendCipherTextToGroup(
|
Future<void> sendCipherTextToGroup(
|
||||||
String groupId,
|
String groupId,
|
||||||
pb.EncryptedContent encryptedContent, {
|
pb.EncryptedContent encryptedContent, {
|
||||||
|
|
@ -492,5 +566,5 @@ Future<void> sendContactMyProfileData(int contactId) async {
|
||||||
username: userService.currentUser.username,
|
username: userService.currentUser.username,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await sendCipherText(contactId, encryptedContent);
|
await sendCipherText(contactId, encryptedContent, blocking: false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
||||||
|
|
||||||
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
await apiService.sendResponse(ClientToServer()..v0 = v0);
|
||||||
AppState.gotMessageFromServer = true;
|
AppState.gotMessageFromServer = true;
|
||||||
Log.info('Message from server proccessed.');
|
Log.info('All messages from the server proccessed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
|
||||||
|
|
@ -86,10 +86,19 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
||||||
final receiptId = message.receiptId;
|
final receiptId = message.receiptId;
|
||||||
|
|
||||||
final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new);
|
final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new);
|
||||||
|
if (mutex.isLocked) {
|
||||||
|
Log.info(
|
||||||
|
'[$receiptId] Skipping — already being processed by another handler',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await mutex.protect(() async {
|
await mutex.protect(() async {
|
||||||
await _handleClient2ClientMessage(newMessage, message);
|
try {
|
||||||
|
await _handleClient2ClientMessage(newMessage, message);
|
||||||
|
} finally {
|
||||||
|
_messageLocks.remove(receiptId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_messageLocks.remove(receiptId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleClient2ClientMessage(
|
Future<void> _handleClient2ClientMessage(
|
||||||
|
|
@ -103,11 +112,11 @@ Future<void> _handleClient2ClientMessage(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info('Started processing message with receiptId $receiptId');
|
Log.info('[$receiptId] Started processing message');
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
||||||
Log.info('Got delivery receipt for $receiptId!');
|
Log.info('[$receiptId] Got delivery receipt!');
|
||||||
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
|
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
|
||||||
|
|
||||||
case Message_Type.PLAINTEXT_CONTENT:
|
case Message_Type.PLAINTEXT_CONTENT:
|
||||||
|
|
@ -120,13 +129,13 @@ Future<void> _handleClient2ClientMessage(
|
||||||
await handleSessionResync(fromUserId);
|
await handleSessionResync(fromUserId);
|
||||||
}
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
'[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}',
|
||||||
);
|
);
|
||||||
retry = true;
|
retry = true;
|
||||||
}
|
}
|
||||||
if (message.plaintextContent.hasRetryControlError()) {
|
if (message.plaintextContent.hasRetryControlError()) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got access control error for $receiptId. Resending message.',
|
'[$receiptId] Got access control error. Resending message.',
|
||||||
);
|
);
|
||||||
retry = true;
|
retry = true;
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +150,10 @@ Future<void> _handleClient2ClientMessage(
|
||||||
ackByServerAt: const Value(null),
|
ackByServerAt: const Value(null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tryToSendCompleteMessage(receiptId: newReceiptId);
|
Log.info(
|
||||||
|
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
|
||||||
|
);
|
||||||
|
await tryToSendCompleteMessage(receiptId: newReceiptId, blocking: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Message_Type.CIPHERTEXT:
|
case Message_Type.CIPHERTEXT:
|
||||||
|
|
@ -197,7 +209,6 @@ Future<void> _handleClient2ClientMessage(
|
||||||
receiptIdDB = const Value.absent();
|
receiptIdDB = const Value.absent();
|
||||||
} else {
|
} else {
|
||||||
// Message was successful processed
|
// Message was successful processed
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,9 +224,9 @@ Future<void> _handleClient2ClientMessage(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.warn(e);
|
Log.warn('[$receiptId] Error inserting receipt: $e');
|
||||||
}
|
}
|
||||||
await tryToSendCompleteMessage(receiptId: receiptId);
|
await tryToSendCompleteMessage(receiptId: receiptId, blocking: false);
|
||||||
}
|
}
|
||||||
case Message_Type.TEST_NOTIFICATION:
|
case Message_Type.TEST_NOTIFICATION:
|
||||||
break;
|
break;
|
||||||
|
|
@ -223,9 +234,9 @@ Future<void> _handleClient2ClientMessage(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
||||||
Log.info('Got a message with receiptId $receiptId');
|
Log.info('[$receiptId] Finished processing');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error marking message as received $receiptId: $e');
|
Log.error('[$receiptId] Error marking message as received: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,26 +246,26 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
||||||
Message_Type messageType,
|
Message_Type messageType,
|
||||||
String receiptId,
|
String receiptId,
|
||||||
) async {
|
) async {
|
||||||
final (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
|
Log.info('[$receiptId] calling signalDecryptMessage');
|
||||||
|
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
encryptedContentRaw,
|
encryptedContentRaw,
|
||||||
messageType.value,
|
messageType.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (encryptedContent == null) {
|
if (encryptedContent == null) {
|
||||||
if (decryptionErrorType == null) {
|
|
||||||
// Duplicate message
|
|
||||||
return (null, null);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
PlaintextContent()
|
PlaintextContent(
|
||||||
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
|
decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage(
|
||||||
..type = decryptionErrorType),
|
type: decryptionErrorType ??=
|
||||||
|
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.info('Calling handleEncryptedMessage for $receiptId');
|
Log.info('[$receiptId] Calling handleEncryptedMessage');
|
||||||
|
|
||||||
final (a, b) = await handleEncryptedMessage(
|
final (a, b) = await handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
@ -263,7 +274,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
||||||
receiptId,
|
receiptId,
|
||||||
);
|
);
|
||||||
|
|
||||||
Log.info('Finished handleEncryptedMessage for $receiptId');
|
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||||
|
|
||||||
if (Platform.isAndroid && a == null && b == null) {
|
if (Platform.isAndroid && a == null && b == null) {
|
||||||
// Message was handled without any error -> Show push notification to the user.
|
// Message was handled without any error -> Show push notification to the user.
|
||||||
|
|
@ -294,11 +305,16 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await checkForUserDiscoveryChanges(
|
await checkForUserDiscoveryChanges(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.senderUserDiscoveryVersion,
|
content.senderUserDiscoveryVersion,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasContactRequest()) {
|
if (content.hasContactRequest()) {
|
||||||
if (!await handleContactRequest(fromUserId, content.contactRequest)) {
|
if (!await handleContactRequest(
|
||||||
|
fromUserId,
|
||||||
|
content.contactRequest,
|
||||||
|
receiptId,
|
||||||
|
)) {
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
PlaintextContent()
|
PlaintextContent()
|
||||||
|
|
@ -312,6 +328,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await handleErrorMessage(
|
await handleErrorMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.errorMessages,
|
content.errorMessages,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -321,6 +338,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.contactUpdate,
|
content.contactUpdate,
|
||||||
senderProfileCounter,
|
senderProfileCounter,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -329,6 +347,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await handleUserDiscoveryRequest(
|
await handleUserDiscoveryRequest(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.userDiscoveryRequest,
|
content.userDiscoveryRequest,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -337,12 +356,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await handleUserDiscoveryUpdate(
|
await handleUserDiscoveryUpdate(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.userDiscoveryUpdate,
|
content.userDiscoveryUpdate,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasPushKeys()) {
|
if (content.hasPushKeys()) {
|
||||||
await handlePushKey(fromUserId, content.pushKeys);
|
await handlePushKey(fromUserId, content.pushKeys, receiptId);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,6 +370,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await handleMessageUpdate(
|
await handleMessageUpdate(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.messageUpdate,
|
content.messageUpdate,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -366,12 +387,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
await handleMediaUpdate(
|
await handleMediaUpdate(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.mediaUpdate,
|
content.mediaUpdate,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.hasGroupId()) {
|
if (!content.hasGroupId()) {
|
||||||
Log.error('Messages should have a groupId $fromUserId.');
|
Log.error('[$receiptId] Messages should have a groupId $fromUserId.');
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,6 +402,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.groupCreate,
|
content.groupCreate,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -392,12 +415,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
.getContactByUserId(fromUserId)
|
.getContactByUserId(fromUserId)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
Log.info(
|
Log.info(
|
||||||
'Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
|
'[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
|
||||||
);
|
);
|
||||||
if (contact == null || !contact.accepted || contact.deletedByUser) {
|
if (contact == null || !contact.accepted || contact.deletedByUser) {
|
||||||
await handleNewContactRequest(fromUserId);
|
await handleNewContactRequest(fromUserId);
|
||||||
Log.error(
|
Log.error(
|
||||||
'User tries to send message to direct chat while the user does not exists !',
|
'[$receiptId] User tries to send message to direct chat while the user does not exist!',
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
|
|
@ -411,7 +434,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
'Creating new DirectChat between two users',
|
'[$receiptId] Creating new DirectChat between two users',
|
||||||
);
|
);
|
||||||
await twonlyDB.groupsDao.createNewDirectChat(
|
await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
@ -422,7 +445,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
} else {
|
} else {
|
||||||
if (content.hasGroupJoin()) {
|
if (content.hasGroupJoin()) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.',
|
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
|
||||||
);
|
);
|
||||||
// In case the group join was received before the GroupCreate the sender should send it later again.
|
// In case the group join was received before the GroupCreate the sender should send it later again.
|
||||||
return (
|
return (
|
||||||
|
|
@ -432,13 +455,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.error('User $fromUserId tried to access group ${content.groupId}.');
|
Log.error(
|
||||||
|
'[$receiptId] User $fromUserId tried to access group ${content.groupId}.',
|
||||||
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasFlameSync()) {
|
if (content.hasFlameSync()) {
|
||||||
await handleFlameSync(content.groupId, content.flameSync);
|
await handleFlameSync(content.groupId, content.flameSync, receiptId);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +472,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.groupUpdate,
|
content.groupUpdate,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -456,6 +482,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.groupJoin,
|
content.groupJoin,
|
||||||
|
receiptId,
|
||||||
)) {
|
)) {
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
|
|
@ -471,6 +498,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.groupJoin,
|
content.groupJoin,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -480,6 +508,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.additionalDataMessage,
|
content.additionalDataMessage,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -489,6 +518,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.textMessage,
|
content.textMessage,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -498,6 +528,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.reaction,
|
content.reaction,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -507,6 +538,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.media,
|
content.media,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
@ -516,6 +548,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
content.groupId,
|
content.groupId,
|
||||||
content.typingIndicator,
|
content.typingIndicator,
|
||||||
|
receiptId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,15 @@ Future<void> handlePeriodicTask({int lastExecutionInSecondsLimit = 120}) async {
|
||||||
if (!shouldBeExecuted) return;
|
if (!shouldBeExecuted) return;
|
||||||
|
|
||||||
Log.info('eu.twonly.periodic_task was called.');
|
Log.info('eu.twonly.periodic_task was called.');
|
||||||
|
AppState.gotMessageFromServer = false;
|
||||||
|
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
// Issue: Because the background isolate can be reused across multiple periodic tasks,
|
||||||
|
// the API connection state might be stale or disconnected from a previous run.
|
||||||
|
// Explicitly close it here to ensure a clean slate before connecting.
|
||||||
|
await apiService.close(null);
|
||||||
|
|
||||||
if (!await apiService.connect()) {
|
if (!await apiService.connect()) {
|
||||||
Log.info('Could not connect to the api. Returning early.');
|
Log.info('Could not connect to the api. Returning early.');
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -65,23 +65,18 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||||
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
({int counter, bool isExpiring}) getFlameCounterFromGroup(Group? group) {
|
||||||
const zero = (counter: 0, isExpiring: false);
|
const zero = (counter: 0, isExpiring: false);
|
||||||
if (group == null) return zero;
|
if (group == null) return zero;
|
||||||
if (group.lastMessageSend == null ||
|
if (group.lastMessageSend == null || group.lastMessageReceived == null || group.lastFlameCounterChange == null) {
|
||||||
group.lastMessageReceived == null ||
|
|
||||||
group.lastFlameCounterChange == null) {
|
|
||||||
return zero;
|
return zero;
|
||||||
}
|
}
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||||
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
|
||||||
if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
|
if (group.lastMessageSend!.isAfter(twoDaysAgo) && group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
||||||
group.lastMessageReceived!.isAfter(twoDaysAgo) ||
|
|
||||||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
|
||||||
// Flame is expiring when today no exchange has happened yet:
|
// Flame is expiring when today no exchange has happened yet:
|
||||||
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
// both lastMessageSend and lastMessageReceived are before startOfToday.
|
||||||
final isExpiring =
|
final isExpiring = group.lastMessageSend!.isBefore(oneDayAgo) || group.lastMessageReceived!.isBefore(oneDayAgo);
|
||||||
group.lastMessageSend!.isBefore(oneDayAgo) ||
|
|
||||||
group.lastMessageReceived!.isBefore(oneDayAgo);
|
|
||||||
return (counter: group.flameCounter, isExpiring: isExpiring);
|
return (counter: group.flameCounter, isExpiring: isExpiring);
|
||||||
} else {
|
} else {
|
||||||
return zero;
|
return zero;
|
||||||
|
|
@ -122,8 +117,7 @@ Future<void> incFlameCounter(
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
|
||||||
if (group.lastMessageSend!.isBefore(twoDaysAgo) ||
|
if (group.lastMessageSend!.isBefore(twoDaysAgo) || group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
||||||
group.lastMessageReceived!.isBefore(twoDaysAgo)) {
|
|
||||||
flameCounter = 0;
|
flameCounter = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,25 +129,21 @@ Future<void> incFlameCounter(
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
if (group.lastFlameCounterChange == null ||
|
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
||||||
group.lastFlameCounterChange!.isBefore(startOfToday)) {
|
|
||||||
// last flame update was yesterday. check if it can be updated.
|
// last flame update was yesterday. check if it can be updated.
|
||||||
var updateFlame = false;
|
var updateFlame = false;
|
||||||
if (received) {
|
if (received) {
|
||||||
if (group.lastMessageSend != null &&
|
if (group.lastMessageSend != null && group.lastMessageSend!.isAfter(startOfToday)) {
|
||||||
group.lastMessageSend!.isAfter(startOfToday)) {
|
|
||||||
// today a message was already send -> update flame
|
// today a message was already send -> update flame
|
||||||
updateFlame = true;
|
updateFlame = true;
|
||||||
}
|
}
|
||||||
} else if (group.lastMessageReceived != null &&
|
} else if (group.lastMessageReceived != null && group.lastMessageReceived!.isAfter(startOfToday)) {
|
||||||
group.lastMessageReceived!.isAfter(startOfToday)) {
|
|
||||||
// today a message was already received -> update flame
|
// today a message was already received -> update flame
|
||||||
updateFlame = true;
|
updateFlame = true;
|
||||||
}
|
}
|
||||||
if (updateFlame) {
|
if (updateFlame) {
|
||||||
flameCounter += 1;
|
flameCounter += 1;
|
||||||
if (group.lastFlameCounterChange == null ||
|
if (group.lastFlameCounterChange == null || group.lastFlameCounterChange!.isBefore(timestamp)) {
|
||||||
group.lastFlameCounterChange!.isBefore(timestamp)) {
|
|
||||||
// only update if the timestamp is newer
|
// only update if the timestamp is newer
|
||||||
lastFlameCounterChange = Value(timestamp);
|
lastFlameCounterChange = Value(timestamp);
|
||||||
}
|
}
|
||||||
|
|
@ -170,13 +160,11 @@ Future<void> incFlameCounter(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (received) {
|
if (received) {
|
||||||
if (group.lastMessageReceived == null ||
|
if (group.lastMessageReceived == null || group.lastMessageReceived!.isBefore(timestamp)) {
|
||||||
group.lastMessageReceived!.isBefore(timestamp)) {
|
|
||||||
lastMessageReceived = Value(timestamp);
|
lastMessageReceived = Value(timestamp);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (group.lastMessageSend == null ||
|
if (group.lastMessageSend == null || group.lastMessageSend!.isBefore(timestamp)) {
|
||||||
group.lastMessageSend!.isBefore(timestamp)) {
|
|
||||||
lastMessageSend = Value(timestamp);
|
lastMessageSend = Value(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,3 +191,18 @@ bool isItPossibleToRestoreFlames(Group group) {
|
||||||
clock.now().subtract(const Duration(days: 7)),
|
clock.now().subtract(const Duration(days: 7)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> restoreFlames(String groupId) async {
|
||||||
|
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
|
if (group == null) return;
|
||||||
|
final now = clock.now();
|
||||||
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
|
groupId,
|
||||||
|
GroupsCompanion(
|
||||||
|
flameCounter: Value(group.maxFlameCounter),
|
||||||
|
lastFlameCounterChange: Value(now),
|
||||||
|
lastMessageSend: Value(now),
|
||||||
|
lastMessageReceived: Value(now),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,17 @@ import 'dart:typed_data';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
as pb;
|
as pb;
|
||||||
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
|
||||||
class KeyVerificationService {
|
class KeyVerificationService {
|
||||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||||
|
|
@ -70,6 +73,18 @@ class KeyVerificationService {
|
||||||
VerificationType.secretQrToken,
|
VerificationType.secretQrToken,
|
||||||
);
|
);
|
||||||
Log.info('Contact was verified via secretQrToken');
|
Log.info('Contact was verified via secretQrToken');
|
||||||
|
|
||||||
|
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
||||||
|
final context = rootNavigatorKey.currentContext;
|
||||||
|
if (context != null && context.mounted && contact != null) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
context.lang.secretQrTokenVerifiedSnackbar(
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,15 @@ class MediaFileService {
|
||||||
if (message.openedAt == null) {
|
if (message.openedAt == null) {
|
||||||
// Message was not yet opened from all persons, so wait...
|
// Message was not yet opened from all persons, so wait...
|
||||||
delete = false;
|
delete = false;
|
||||||
|
} else if (message.openedAt!.isAfter(
|
||||||
|
clock.now().subtract(const Duration(minutes: 3)),
|
||||||
|
)) {
|
||||||
|
// When the message was opened in the last two minutes, do not purge.
|
||||||
|
// Bug: When the user opens an image immediately after starting the app, there is a race condition:
|
||||||
|
// The message is marked as opened, but then purgeTempFolder is run
|
||||||
|
// (it is unawaited) and deletes the file. Thi gives a grace period:
|
||||||
|
// The image must have been opened within the last two minutes, otherwise do not delete it.
|
||||||
|
delete = false;
|
||||||
} else if (mediaFile.requiresAuthentication ||
|
} else if (mediaFile.requiresAuthentication ||
|
||||||
mediaFile.displayLimitInMilliseconds != null) {
|
mediaFile.displayLimitInMilliseconds != null) {
|
||||||
// Message was opened by all persons, and they can not reopen the image.
|
// Message was opened by all persons, and they can not reopen the image.
|
||||||
|
|
|
||||||
8
lib/src/services/profile.service.dart
Normal file
8
lib/src/services/profile.service.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
enum SetupProfile { standard, customized, maximum }
|
||||||
|
|
||||||
|
enum SecurityProfile { normal, strict }
|
||||||
|
|
||||||
|
extension SecurityProfileExtension on SecurityProfile {
|
||||||
|
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
|
||||||
|
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
|
||||||
|
}
|
||||||
|
|
@ -42,62 +42,72 @@ signalDecryptMessage(
|
||||||
) async {
|
) async {
|
||||||
// Hold the lock only for the cryptographic operation, not for network I/O
|
// Hold the lock only for the cryptographic operation, not for network I/O
|
||||||
Log.info('Acquiring lockingSignalProtocol for $fromUserId');
|
Log.info('Acquiring lockingSignalProtocol for $fromUserId');
|
||||||
final (decryptedContent, errorType, needsResync) = await lockingSignalProtocol
|
final (
|
||||||
.protect(() async {
|
decryptedContent,
|
||||||
Log.info('Lock acquired for $fromUserId');
|
errorType,
|
||||||
try {
|
needsResync,
|
||||||
final session = SessionCipher.fromStore(
|
) = await lockingSignalProtocol.protect(() async {
|
||||||
(await getSignalStore())!,
|
Log.info('Lock acquired for $fromUserId');
|
||||||
getSignalAddress(fromUserId),
|
try {
|
||||||
);
|
final session = SessionCipher.fromStore(
|
||||||
|
(await getSignalStore())!,
|
||||||
|
getSignalAddress(fromUserId),
|
||||||
|
);
|
||||||
|
|
||||||
Uint8List plaintext;
|
Uint8List plaintext;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CiphertextMessage.prekeyType:
|
case CiphertextMessage.prekeyType:
|
||||||
plaintext = await session.decrypt(
|
plaintext = await session.decrypt(
|
||||||
PreKeySignalMessage(encryptedContentRaw),
|
PreKeySignalMessage(encryptedContentRaw),
|
||||||
);
|
|
||||||
case CiphertextMessage.whisperType:
|
|
||||||
plaintext = await session.decryptFromSignal(
|
|
||||||
SignalMessage.fromSerialized(encryptedContentRaw),
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
Log.error('Unknown Message Decryption Type: $type');
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (EncryptedContent.fromBuffer(plaintext), null, false);
|
|
||||||
} on InvalidKeyIdException catch (e) {
|
|
||||||
Log.warn(e);
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
} on DuplicateMessageException catch (e) {
|
case CiphertextMessage.whisperType:
|
||||||
Log.info(e.toString());
|
plaintext = await session.decryptFromSignal(
|
||||||
return (null, null, false);
|
SignalMessage.fromSerialized(encryptedContentRaw),
|
||||||
} on InvalidMessageException catch (e) {
|
|
||||||
Log.warn(e);
|
|
||||||
return (
|
|
||||||
null,
|
|
||||||
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
default:
|
||||||
Log.error(e);
|
Log.error('Unknown Message Decryption Type: $type');
|
||||||
return (
|
return (
|
||||||
null,
|
null,
|
||||||
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return (EncryptedContent.fromBuffer(plaintext), null, false);
|
||||||
|
} on InvalidKeyIdException catch (e) {
|
||||||
|
Log.warn(e);
|
||||||
|
return (
|
||||||
|
null,
|
||||||
|
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} on DuplicateMessageException catch (e) {
|
||||||
|
// This is normal behavior: This can happen in case a message was decrypted, but before further processing
|
||||||
|
// the user killed the app. This results in a new transmission from the server, but as the message was already
|
||||||
|
// decrypted, this error happens. In this case, request the message again.
|
||||||
|
Log.info(e);
|
||||||
|
return (
|
||||||
|
null,
|
||||||
|
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} on InvalidMessageException catch (e) {
|
||||||
|
Log.warn(e);
|
||||||
|
return (
|
||||||
|
null,
|
||||||
|
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
return (
|
||||||
|
null,
|
||||||
|
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Log.info('Released lockingSignalProtocol for $fromUserId');
|
Log.info('Released lockingSignalProtocol for $fromUserId');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ class UserDiscoveryService {
|
||||||
|
|
||||||
static Future<Uint8List?> getCurrentVersion() async {
|
static Future<Uint8List?> getCurrentVersion() async {
|
||||||
try {
|
try {
|
||||||
return await FlutterUserDiscovery.getCurrentVersion();
|
return await FlutterUserDiscovery.getCurrentVersion()
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -140,7 +141,7 @@ class UserDiscoveryService {
|
||||||
return await FlutterUserDiscovery.shouldRequestNewMessages(
|
return await FlutterUserDiscovery.shouldRequestNewMessages(
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
version: receivedVersion,
|
version: receivedVersion,
|
||||||
);
|
).timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -155,7 +156,7 @@ class UserDiscoveryService {
|
||||||
return await FlutterUserDiscovery.getNewMessages(
|
return await FlutterUserDiscovery.getNewMessages(
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
receivedVersion: receivedVersion,
|
receivedVersion: receivedVersion,
|
||||||
);
|
).timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -175,7 +176,7 @@ class UserDiscoveryService {
|
||||||
messages: messages,
|
messages: messages,
|
||||||
publicKeyVerifiedTimestamp:
|
publicKeyVerifiedTimestamp:
|
||||||
verifications.lastOrNull?.createdAt.millisecondsSinceEpoch,
|
verifications.lastOrNull?.createdAt.millisecondsSinceEpoch,
|
||||||
);
|
).timeout(const Duration(seconds: 5));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,13 @@ class Log {
|
||||||
Logger.root.onRecord.listen((record) async {
|
Logger.root.onRecord.listen((record) async {
|
||||||
unawaited(_writeLogToFile(record));
|
unawaited(_writeLogToFile(record));
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
// ignore: avoid_print
|
if (!Platform.environment.containsKey('FLUTTER_TEST') ||
|
||||||
print(
|
record.level >= Level.WARNING) {
|
||||||
'${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}',
|
// ignore: avoid_print
|
||||||
);
|
print(
|
||||||
|
'${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +139,7 @@ Future<void> cleanLogFile() async {
|
||||||
}
|
}
|
||||||
final lines = await logFile.readAsLines();
|
final lines = await logFile.readAsLines();
|
||||||
|
|
||||||
final twoWeekAgo = clock.now().subtract(const Duration(days: 14));
|
final twoWeekAgo = clock.now().subtract(const Duration(days: 3));
|
||||||
var keepStartIndex = -1;
|
var keepStartIndex = -1;
|
||||||
|
|
||||||
for (var i = 0; i < lines.length; i += 100) {
|
for (var i = 0; i < lines.length; i += 100) {
|
||||||
|
|
|
||||||
|
|
@ -293,9 +293,10 @@ Future<List<int>> sha256File(File file) async {
|
||||||
List<TextSpan> formattedText(
|
List<TextSpan> formattedText(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String input, {
|
String input, {
|
||||||
|
Color? textColor,
|
||||||
Color? boldTextColor,
|
Color? boldTextColor,
|
||||||
}) {
|
}) {
|
||||||
final defaultColor = Theme.of(context).colorScheme.onSurface;
|
final defaultColor = textColor ?? Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
final regex = RegExp(r'\*(.*?)\*');
|
final regex = RegExp(r'\*(.*?)\*');
|
||||||
final spans = <TextSpan>[];
|
final spans = <TextSpan>[];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart' show ListExtensions;
|
import 'package:collection/collection.dart' show ListExtensions;
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
@ -41,6 +42,7 @@ class QrCodeUtils {
|
||||||
signedPrekeySignature: signedPreKey.signature,
|
signedPrekeySignature: signedPreKey.signature,
|
||||||
signedPrekeyId: Int64(signedPreKey.id),
|
signedPrekeyId: Int64(signedPreKey.id),
|
||||||
secretVerificationToken: secretVerificationToken,
|
secretVerificationToken: secretVerificationToken,
|
||||||
|
timestamp: Int64(clock.now().millisecondsSinceEpoch),
|
||||||
);
|
);
|
||||||
|
|
||||||
final data = publicProfile.writeToBuffer();
|
final data = publicProfile.writeToBuffer();
|
||||||
|
|
@ -94,7 +96,18 @@ class QrCodeUtils {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (verificationOk) {
|
if (verificationOk) {
|
||||||
if (profile.hasSecretVerificationToken()) {
|
var useSecretVerificationToken = profile.hasSecretVerificationToken();
|
||||||
|
if (profile.hasTimestamp()) {
|
||||||
|
// Only notify the scanned user if the QR code was generated within the last 10 minutes.
|
||||||
|
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
profile.timestamp.toInt(),
|
||||||
|
);
|
||||||
|
final tenMinutesAgo = clock.now().subtract(const Duration(minutes: 10));
|
||||||
|
if (timestamp.isBefore(tenMinutesAgo)) {
|
||||||
|
useSecretVerificationToken = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useSecretVerificationToken) {
|
||||||
unawaited(
|
unawaited(
|
||||||
KeyVerificationService.handleScannedVerificationToken(
|
KeyVerificationService.handleScannedVerificationToken(
|
||||||
contact.userId,
|
contact.userId,
|
||||||
|
|
|
||||||
56
lib/src/visual/components/contact_request_badge.comp.dart
Normal file
56
lib/src/visual/components/contact_request_badge.comp.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class ContactRequestBadgeComp extends StatelessWidget {
|
||||||
|
const ContactRequestBadgeComp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<int?>(
|
||||||
|
stream: twonlyDB.contactsDao.watchContactsRequestedCount(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final count = snapshot.data ?? 0;
|
||||||
|
if (count == 0) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: NotificationBadgeComp(
|
||||||
|
backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
|
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
count: count.toString(),
|
||||||
|
child: IconButton(
|
||||||
|
color: Colors.black,
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.userPlus,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
lib/src/visual/components/profile_qr_code.comp.dart
Normal file
96
lib/src/visual/components/profile_qr_code.comp.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
|
|
||||||
|
class ProfileQrCodeComp extends StatefulWidget {
|
||||||
|
const ProfileQrCodeComp({
|
||||||
|
this.size = 250,
|
||||||
|
this.showAvatar = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
final bool showAvatar;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileQrCodeComp> createState() => _ProfileQrCodeCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
|
||||||
|
String? _qrCode;
|
||||||
|
Uint8List? _userAvatar;
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadData() async {
|
||||||
|
final qr = await QrCodeUtils.publicProfileLink();
|
||||||
|
final avatar = widget.showAvatar ? await getUserAvatar() : null;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_qrCode = qr;
|
||||||
|
_userAvatar = avatar;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loaded = !_isLoading && _qrCode != null;
|
||||||
|
return SizedBox(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: loaded
|
||||||
|
? Container(
|
||||||
|
key: const ValueKey('qr_code_container'),
|
||||||
|
// padding: const EdgeInsets.all(3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.primary,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 3,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: QrImageView.withQr(
|
||||||
|
qr: QrCode.fromData(
|
||||||
|
data: _qrCode!,
|
||||||
|
errorCorrectLevel: QrErrorCorrectLevel.M,
|
||||||
|
),
|
||||||
|
eyeStyle: QrEyeStyle(
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
borderRadius: 2,
|
||||||
|
),
|
||||||
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
borderRadius: 2,
|
||||||
|
),
|
||||||
|
gapless: false,
|
||||||
|
embeddedImage: (widget.showAvatar && _userAvatar != null) ? MemoryImage(_userAvatar!) : null,
|
||||||
|
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||||
|
size: const Size(60, 66),
|
||||||
|
embeddedImageShape: EmbeddedImageShape.square,
|
||||||
|
shapeColor: context.color.primary,
|
||||||
|
safeArea: true,
|
||||||
|
),
|
||||||
|
size: widget.size,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(key: ValueKey('qr_code_placeholder')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,12 @@ void _showOverlay({
|
||||||
required Duration displayDuration,
|
required Duration displayDuration,
|
||||||
required void Function(AnimationController) onAnimationControllerInit,
|
required void Function(AnimationController) onAnimationControllerInit,
|
||||||
}) {
|
}) {
|
||||||
final overlayState = Overlay.maybeOf(context);
|
var overlayState = Overlay.maybeOf(context);
|
||||||
|
if (overlayState == null) {
|
||||||
|
if (context is StatefulElement && context.state is NavigatorState) {
|
||||||
|
overlayState = (context.state as NavigatorState).overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
late OverlayEntry overlayEntry;
|
late OverlayEntry overlayEntry;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class VerificationBadgeComp extends StatefulWidget {
|
||||||
|
|
||||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
bool _isVerified = false;
|
bool _isVerified = false;
|
||||||
bool _isVerifiedByTransferredTrust = false;
|
int _verifiedByTransferredTrustCount = 0;
|
||||||
|
|
||||||
StreamSubscription<VerificationStatus>? _streamAllVerified;
|
StreamSubscription<VerificationStatus>? _streamAllVerified;
|
||||||
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
||||||
|
|
@ -42,25 +42,40 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.group != null) {
|
initAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initAsync() async {
|
||||||
|
var group = widget.group;
|
||||||
|
var contact = widget.contact;
|
||||||
|
|
||||||
|
if (group?.isDirectChat == true) {
|
||||||
|
final members = await twonlyDB.groupsDao.getGroupContact(group!.groupId);
|
||||||
|
if (members.isNotEmpty) {
|
||||||
|
contact = members.first;
|
||||||
|
group = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group != null) {
|
||||||
_streamAllVerified = twonlyDB.keyVerificationDao
|
_streamAllVerified = twonlyDB.keyVerificationDao
|
||||||
.watchAllGroupMembersVerified(widget.group!.groupId)
|
.watchAllGroupMembersVerified(group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVerified = false;
|
_isVerified = false;
|
||||||
_isVerifiedByTransferredTrust = false;
|
_verifiedByTransferredTrustCount = 0;
|
||||||
if (update == VerificationStatus.trusted) {
|
if (update == VerificationStatus.trusted) {
|
||||||
_isVerified = true;
|
_isVerified = true;
|
||||||
}
|
}
|
||||||
if (update == VerificationStatus.partialTrusted) {
|
if (update == VerificationStatus.partialTrusted) {
|
||||||
_isVerifiedByTransferredTrust = true;
|
_verifiedByTransferredTrustCount = 10;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (widget.contact != null) {
|
} else if (contact != null) {
|
||||||
_streamContactVerification = twonlyDB.keyVerificationDao
|
_streamContactVerification = twonlyDB.keyVerificationDao
|
||||||
.watchContactVerification(widget.contact!.userId)
|
.watchContactVerification(contact.userId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -69,16 +84,16 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
});
|
});
|
||||||
|
|
||||||
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||||
.watchTransferredTrustVerifications(widget.contact!.userId)
|
.watchTransferredTrustVerifications(contact.userId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVerifiedByTransferredTrust = update.isNotEmpty;
|
_verifiedByTransferredTrustCount = update.length;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (widget.isVerifiedByTransferredTrust != null) {
|
} else if (widget.isVerifiedByTransferredTrust != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVerifiedByTransferredTrust = widget.isVerifiedByTransferredTrust!;
|
_verifiedByTransferredTrustCount = 10;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +109,7 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_isVerified &&
|
if (!_isVerified &&
|
||||||
!_isVerifiedByTransferredTrust &&
|
_verifiedByTransferredTrustCount == 0 &&
|
||||||
widget.showOnlyIfVerified) {
|
widget.showOnlyIfVerified) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
@ -112,10 +127,12 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
bottom: 3,
|
bottom: 3,
|
||||||
),
|
),
|
||||||
child: SvgIcon(
|
child: SvgIcon(
|
||||||
assetPath: (_isVerified || _isVerifiedByTransferredTrust)
|
assetPath: _isVerified
|
||||||
? SvgIcons.verifiedGreen
|
? SvgIcons.verifiedGreen
|
||||||
|
: _verifiedByTransferredTrustCount > 0
|
||||||
|
? SvgIcons.verifiedNumeric(_verifiedByTransferredTrustCount)
|
||||||
: SvgIcons.verifiedRed,
|
: SvgIcons.verifiedRed,
|
||||||
color: (_isVerifiedByTransferredTrust && !_isVerified)
|
color: (_verifiedByTransferredTrustCount > 0 && !_isVerified)
|
||||||
? colorVerificationBadgeYellow
|
? colorVerificationBadgeYellow
|
||||||
: null,
|
: null,
|
||||||
size: widget.size,
|
size: widget.size,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
@ -23,16 +25,18 @@ class VerificationBadgeInfo extends StatelessWidget {
|
||||||
description: context.lang.verificationBadgeGreenDesc,
|
description: context.lang.verificationBadgeGreenDesc,
|
||||||
boldTextColor: primaryColor,
|
boldTextColor: primaryColor,
|
||||||
),
|
),
|
||||||
_buildItem(
|
if (userService.currentUser.securityProfile != SecurityProfile.strict ||
|
||||||
context,
|
userService.currentUser.isUserDiscoveryEnabled)
|
||||||
icon: const SvgIcon(
|
_buildItem(
|
||||||
assetPath: SvgIcons.verifiedGreen,
|
context,
|
||||||
size: 40,
|
icon: const SvgIcon(
|
||||||
color: colorVerificationBadgeYellow,
|
assetPath: SvgIcons.verifiedGreen,
|
||||||
|
size: 40,
|
||||||
|
color: colorVerificationBadgeYellow,
|
||||||
|
),
|
||||||
|
description: context.lang.verificationBadgeYellowDesc,
|
||||||
|
boldTextColor: colorVerificationBadgeYellow,
|
||||||
),
|
),
|
||||||
description: context.lang.verificationBadgeYellowDesc,
|
|
||||||
boldTextColor: colorVerificationBadgeYellow,
|
|
||||||
),
|
|
||||||
_buildItem(
|
_buildItem(
|
||||||
context,
|
context,
|
||||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
class SvgIcons {
|
class SvgIcons {
|
||||||
static const String verifiedGreen = 'assets/icons/verified_badge_green.svg';
|
static const String verifiedGreen = 'assets/icons/verified_badge_green.svg';
|
||||||
static const String verifiedRed = 'assets/icons/verified_badge_red.svg';
|
static const String verifiedRed = 'assets/icons/verified_badge_red.svg';
|
||||||
|
|
||||||
|
static String verifiedNumeric(int value) {
|
||||||
|
if (value >= 4) {
|
||||||
|
return verifiedGreen;
|
||||||
|
}
|
||||||
|
return 'assets/icons/verification_badge_numeric/verified_badge_$value.svg';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SvgIcon extends StatelessWidget {
|
class SvgIcon extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -46,24 +46,43 @@ class ScreenshotController {
|
||||||
}
|
}
|
||||||
late GlobalKey _containerKey;
|
late GlobalKey _containerKey;
|
||||||
|
|
||||||
Future<ScreenshotImageHelper?> capture({double? pixelRatio}) async {
|
Future<ScreenshotImageHelper?> capture({
|
||||||
|
double? pixelRatio,
|
||||||
|
int retries = 20,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final findRenderObject = _containerKey.currentContext?.findRenderObject();
|
final findRenderObject = _containerKey.currentContext?.findRenderObject();
|
||||||
if (findRenderObject == null) {
|
if (findRenderObject == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final boundary = findRenderObject as RenderRepaintBoundary;
|
final boundary = findRenderObject as RenderRepaintBoundary;
|
||||||
|
|
||||||
final context = _containerKey.currentContext;
|
final context = _containerKey.currentContext;
|
||||||
var tmpPixelRatio = pixelRatio;
|
var tmpPixelRatio = pixelRatio;
|
||||||
if (tmpPixelRatio == null) {
|
if (tmpPixelRatio == null) {
|
||||||
if (context != null && context.mounted) {
|
if (context != null && context.mounted) {
|
||||||
tmpPixelRatio =
|
tmpPixelRatio = tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
|
||||||
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
|
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
|
||||||
return ScreenshotImageHelper(image: image);
|
return ScreenshotImageHelper(image: image);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (retries > 0) {
|
||||||
|
final completer = Completer<ScreenshotImageHelper?>();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
final result = await capture(
|
||||||
|
pixelRatio: pixelRatio,
|
||||||
|
retries: retries - 1,
|
||||||
|
);
|
||||||
|
completer.complete(result);
|
||||||
|
});
|
||||||
|
Timer(const Duration(milliseconds: 50), () {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
WidgetsBinding.instance.scheduleFrame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ class _StartNewChatView extends State<AddNewShortcutView> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||||
floatingActionButton: FilledButton.icon(
|
floatingActionButton: FilledButton.icon(
|
||||||
onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null)
|
onPressed: (_selectedGroups.isEmpty || shortcutEmoji == null)
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ class CameraScannedOverlay extends StatelessWidget {
|
||||||
...mainController.contactsVerified.values.map(
|
...mainController.contactsVerified.values.map(
|
||||||
(c) => _buildVerifiedContactTile(context, c),
|
(c) => _buildVerifiedContactTile(context, c),
|
||||||
),
|
),
|
||||||
if (mainController.scannedUrl != null)
|
if (mainController.scannedUrl != null) _buildScannedUrlTile(context, mainController.scannedUrl!),
|
||||||
_buildScannedUrlTile(context, mainController.scannedUrl!),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -46,15 +45,14 @@ class CameraScannedOverlay extends StatelessWidget {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
c.isLoading = true;
|
c.isLoading = true;
|
||||||
mainController.setState();
|
mainController.setState?.call();
|
||||||
|
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
context,
|
context,
|
||||||
context.lang.requestedUserToastText(c.profile.username),
|
context.lang.requestedUserToastText(c.profile.username),
|
||||||
level: SnackbarLevel.success,
|
level: SnackbarLevel.success,
|
||||||
);
|
);
|
||||||
if (await addNewContactFromPublicProfile(c.profile) &&
|
if (await addNewContactFromPublicProfile(c.profile) && context.mounted) {
|
||||||
context.mounted) {
|
|
||||||
// showSnackbar(
|
// showSnackbar(
|
||||||
// context,
|
// context,
|
||||||
// context.lang.requestedUserToastText(c.profile.username),
|
// context.lang.requestedUserToastText(c.profile.username),
|
||||||
|
|
@ -113,7 +111,7 @@ class CameraScannedOverlay extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
getContactDisplayName(c.contact, maxLength: 13),
|
getContactDisplayName(c.contact, maxLength: 9),
|
||||||
),
|
),
|
||||||
Expanded(child: Container()),
|
Expanded(child: Container()),
|
||||||
ColoredBox(
|
ColoredBox(
|
||||||
|
|
@ -121,13 +119,11 @@ class CameraScannedOverlay extends StatelessWidget {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 30,
|
width: 30,
|
||||||
child: Lottie.asset(
|
child: Lottie.asset(
|
||||||
c.verificationOk
|
c.verificationOk ? 'assets/animations/success.lottie' : 'assets/animations/failed.lottie',
|
||||||
? 'assets/animations/success.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), () {
|
||||||
mainController.setState();
|
mainController.setState?.call();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class SelectedCameraDetails {
|
||||||
bool cameraLoaded = false;
|
bool cameraLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CameraPreviewControllerView extends StatelessWidget {
|
class CameraPreviewControllerView extends StatefulWidget {
|
||||||
const CameraPreviewControllerView({
|
const CameraPreviewControllerView({
|
||||||
required this.mainController,
|
required this.mainController,
|
||||||
required this.isVisible,
|
required this.isVisible,
|
||||||
|
|
@ -62,23 +62,52 @@ class CameraPreviewControllerView extends StatelessWidget {
|
||||||
final bool isVisible;
|
final bool isVisible;
|
||||||
final bool hideControllers;
|
final bool hideControllers;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CameraPreviewControllerView> createState() => _CameraPreviewControllerViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraPreviewControllerViewState extends State<CameraPreviewControllerView> {
|
||||||
|
Future<bool>? _permissionsFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (!AppState.hasCameraPermissions) {
|
||||||
|
_permissionsFuture = checkPermissions().then((hasPermission) {
|
||||||
|
if (hasPermission) {
|
||||||
|
AppState.hasCameraPermissions = true;
|
||||||
|
}
|
||||||
|
return hasPermission;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
if (AppState.hasCameraPermissions) {
|
||||||
future: checkPermissions(),
|
return CameraPreviewView(
|
||||||
|
sendToGroup: widget.sendToGroup,
|
||||||
|
mainCameraController: widget.mainController,
|
||||||
|
isVisible: widget.isVisible,
|
||||||
|
hideControllers: widget.hideControllers,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FutureBuilder<bool>(
|
||||||
|
future: _permissionsFuture,
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
if (snap.hasData) {
|
if (snap.hasData) {
|
||||||
if (snap.data!) {
|
if (snap.data!) {
|
||||||
return CameraPreviewView(
|
return CameraPreviewView(
|
||||||
sendToGroup: sendToGroup,
|
sendToGroup: widget.sendToGroup,
|
||||||
mainCameraController: mainController,
|
mainCameraController: widget.mainController,
|
||||||
isVisible: isVisible,
|
isVisible: widget.isVisible,
|
||||||
hideControllers: hideControllers,
|
hideControllers: widget.hideControllers,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return PermissionHandlerView(
|
return PermissionHandlerView(
|
||||||
onSuccess: () {
|
onSuccess: () {
|
||||||
mainController.selectCamera(0, true);
|
widget.mainController.selectCamera(0, true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -210,8 +239,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_hasAudioPermission = await Permission.microphone.isGranted;
|
_hasAudioPermission = await Permission.microphone.isGranted;
|
||||||
|
|
||||||
if (!_hasAudioPermission &&
|
if (!_hasAudioPermission && !userService.currentUser.requestedAudioPermission) {
|
||||||
!userService.currentUser.requestedAudioPermission) {
|
|
||||||
await UserService.update((u) => u.requestedAudioPermission = true);
|
await UserService.update((u) => u.requestedAudioPermission = true);
|
||||||
await requestMicrophonePermission();
|
await requestMicrophonePermission();
|
||||||
}
|
}
|
||||||
|
|
@ -232,8 +260,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateScaleFactor(double newScale) async {
|
Future<void> updateScaleFactor(double newScale) async {
|
||||||
if (mc.selectedCameraDetails.scaleFactor == newScale ||
|
if (mc.selectedCameraDetails.scaleFactor == newScale || mc.cameraController == null) {
|
||||||
mc.cameraController == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await mc.cameraController?.setZoomLevel(
|
await mc.cameraController?.setZoomLevel(
|
||||||
|
|
@ -316,9 +343,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
bool sharedFromGallery = false,
|
bool sharedFromGallery = false,
|
||||||
MediaType? mediaType,
|
MediaType? mediaType,
|
||||||
}) async {
|
}) async {
|
||||||
final type =
|
final type = mediaType ?? ((videoFilePath != null) ? MediaType.video : MediaType.image);
|
||||||
mediaType ??
|
|
||||||
((videoFilePath != null) ? MediaType.video : MediaType.image);
|
|
||||||
final mediaFileService = await initializeMediaUpload(
|
final mediaFileService = await initializeMediaUpload(
|
||||||
type,
|
type,
|
||||||
userService.currentUser.defaultShowTime,
|
userService.currentUser.defaultShowTime,
|
||||||
|
|
@ -359,10 +384,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
mainCameraController: mc,
|
mainCameraController: mc,
|
||||||
previewLink: mc.sharedLinkForPreview,
|
previewLink: mc.sharedLinkForPreview,
|
||||||
),
|
),
|
||||||
transitionsBuilder:
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
(context, animation, secondaryAnimation, child) {
|
return child;
|
||||||
return child;
|
},
|
||||||
},
|
|
||||||
transitionDuration: Duration.zero,
|
transitionDuration: Duration.zero,
|
||||||
reverseTransitionDuration: Duration.zero,
|
reverseTransitionDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
|
|
@ -392,16 +416,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isFront =>
|
bool get isFront => mc.cameraController?.description.lensDirection == CameraLensDirection.front;
|
||||||
mc.cameraController?.description.lensDirection ==
|
|
||||||
CameraLensDirection.front;
|
|
||||||
|
|
||||||
Future<void> onPanUpdate(dynamic details) async {
|
Future<void> onPanUpdate(dynamic details) async {
|
||||||
if (details == null) {
|
if (details == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mc.cameraController == null ||
|
if (mc.cameraController == null || !mc.cameraController!.value.isInitialized) {
|
||||||
!mc.cameraController!.value.isInitialized) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,8 +551,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startVideoRecording() async {
|
Future<void> startVideoRecording() async {
|
||||||
if (mc.cameraController != null &&
|
if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) {
|
||||||
mc.cameraController!.value.isRecordingVideo) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -551,8 +571,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_currentTime = clock.now();
|
_currentTime = clock.now();
|
||||||
});
|
});
|
||||||
if (_videoRecordingStarted != null &&
|
if (_videoRecordingStarted != null &&
|
||||||
_currentTime.difference(_videoRecordingStarted!).inSeconds >=
|
_currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) {
|
||||||
maxVideoRecordingTime) {
|
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
_videoRecordingTimer = null;
|
_videoRecordingTimer = null;
|
||||||
stopVideoRecording();
|
stopVideoRecording();
|
||||||
|
|
@ -589,8 +608,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_videoRecordingLocked = false;
|
_videoRecordingLocked = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mc.cameraController == null ||
|
if (mc.cameraController == null || !mc.cameraController!.value.isRecordingVideo) {
|
||||||
!mc.cameraController!.value.isRecordingVideo) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -618,8 +636,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length ||
|
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) {
|
||||||
mc.cameraController == null) {
|
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
|
|
@ -643,9 +660,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
|
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
|
||||||
});
|
});
|
||||||
// Get the position of the pointer
|
// Get the position of the pointer
|
||||||
final renderBox =
|
final renderBox = keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
|
||||||
keyTriggerButton.currentContext!.findRenderObject()!
|
|
||||||
as RenderBox;
|
|
||||||
final localPosition = renderBox.globalToLocal(
|
final localPosition = renderBox.globalToLocal(
|
||||||
details.globalPosition,
|
details.globalPosition,
|
||||||
);
|
);
|
||||||
|
|
@ -681,24 +696,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown &&
|
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && !mc.isVideoRecording)
|
||||||
widget.sendToGroup != null &&
|
|
||||||
!mc.isVideoRecording)
|
|
||||||
ShowTitleText(
|
ShowTitleText(
|
||||||
title: widget.sendToGroup!.groupName,
|
title: widget.sendToGroup!.groupName,
|
||||||
desc: context.lang.cameraPreviewSendTo,
|
desc: context.lang.cameraPreviewSendTo,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown &&
|
if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && !mc.isVideoRecording)
|
||||||
mc.sharedLinkForPreview != null &&
|
|
||||||
!mc.isVideoRecording)
|
|
||||||
ShowTitleText(
|
ShowTitleText(
|
||||||
title: mc.sharedLinkForPreview?.host ?? '',
|
title: mc.sharedLinkForPreview?.host ?? '',
|
||||||
desc: 'Link',
|
desc: 'Link',
|
||||||
isLink: true,
|
isLink: true,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown &&
|
if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && !widget.hideControllers)
|
||||||
!mc.isVideoRecording &&
|
|
||||||
!widget.hideControllers)
|
|
||||||
CameraTopActions(
|
CameraTopActions(
|
||||||
selectedCameraDetails: mc.selectedCameraDetails,
|
selectedCameraDetails: mc.selectedCameraDetails,
|
||||||
hasAudioPermission: _hasAudioPermission,
|
hasAudioPermission: _hasAudioPermission,
|
||||||
|
|
@ -742,8 +751,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
videoRecordingStarted: _videoRecordingStarted,
|
videoRecordingStarted: _videoRecordingStarted,
|
||||||
maxVideoRecordingTime: maxVideoRecordingTime,
|
maxVideoRecordingTime: maxVideoRecordingTime,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null ||
|
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers)
|
||||||
widget.hideControllers)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 5,
|
left: 5,
|
||||||
top: 10,
|
top: 10,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class ScannedNewProfile {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainCameraController {
|
class MainCameraController {
|
||||||
late void Function() setState;
|
void Function()? setState;
|
||||||
CameraController? cameraController;
|
CameraController? cameraController;
|
||||||
ScreenshotController screenshotController = ScreenshotController();
|
ScreenshotController screenshotController = ScreenshotController();
|
||||||
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
|
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
|
||||||
|
|
@ -61,12 +61,12 @@ class MainCameraController {
|
||||||
|
|
||||||
void setSharedLinkForPreview(Uri? url) {
|
void setSharedLinkForPreview(Uri? url) {
|
||||||
sharedLinkForPreview = url;
|
sharedLinkForPreview = url;
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onImageSend() {
|
void onImageSend() {
|
||||||
scannedUrl = '';
|
scannedUrl = '';
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
final BarcodeScanner _barcodeScanner = BarcodeScanner();
|
||||||
|
|
@ -85,35 +85,50 @@ class MainCameraController {
|
||||||
FaceFilterType _currentFilterType = FaceFilterType.none;
|
FaceFilterType _currentFilterType = FaceFilterType.none;
|
||||||
FaceFilterType get currentFilterType => _currentFilterType;
|
FaceFilterType get currentFilterType => _currentFilterType;
|
||||||
|
|
||||||
|
Future<void>? _initializeFuture;
|
||||||
Future<void>? _pendingDisposal;
|
Future<void>? _pendingDisposal;
|
||||||
|
int _cameraSessionId = 0;
|
||||||
|
|
||||||
Future<void> closeCamera() async {
|
Future<void> closeCamera() async {
|
||||||
|
_cameraSessionId++;
|
||||||
contactsVerified = {};
|
contactsVerified = {};
|
||||||
scannedNewProfiles = {};
|
scannedNewProfiles = {};
|
||||||
scannedUrl = null;
|
scannedUrl = null;
|
||||||
try {
|
|
||||||
await cameraController?.stopImageStream();
|
|
||||||
// ignore: empty_catches
|
|
||||||
} catch (e) {}
|
|
||||||
final cameraControllerTemp = cameraController;
|
final cameraControllerTemp = cameraController;
|
||||||
cameraController = null;
|
cameraController = null;
|
||||||
|
final initFutureTemp = _initializeFuture;
|
||||||
|
_initializeFuture = null;
|
||||||
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
|
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
|
||||||
_pendingDisposal = Future.delayed(
|
_pendingDisposal = Future.delayed(
|
||||||
const Duration(milliseconds: 100),
|
const Duration(milliseconds: 100),
|
||||||
() async {
|
() async {
|
||||||
|
try {
|
||||||
|
if (initFutureTemp != null) {
|
||||||
|
await initFutureTemp;
|
||||||
|
}
|
||||||
|
// ignore: empty_catches
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
await cameraControllerTemp?.stopImageStream();
|
||||||
|
// ignore: empty_catches
|
||||||
|
} catch (e) {}
|
||||||
await cameraControllerTemp?.dispose();
|
await cameraControllerTemp?.dispose();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
initCameraStarted = false;
|
initCameraStarted = false;
|
||||||
selectedCameraDetails = SelectedCameraDetails();
|
selectedCameraDetails = SelectedCameraDetails();
|
||||||
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectCamera(int sCameraId, bool init) async {
|
Future<void> selectCamera(int sCameraId, bool init) async {
|
||||||
await _pendingDisposal;
|
|
||||||
initCameraStarted = true;
|
initCameraStarted = true;
|
||||||
|
final sessionId = ++_cameraSessionId;
|
||||||
|
await _pendingDisposal;
|
||||||
|
if (sessionId != _cameraSessionId) return;
|
||||||
|
|
||||||
if (AppEnvironment.cameras.isEmpty) {
|
if (AppEnvironment.cameras.isEmpty) {
|
||||||
AppEnvironment.cameras = await availableCameras();
|
AppEnvironment.cameras = await availableCameras();
|
||||||
|
if (sessionId != _cameraSessionId) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cameraId = sCameraId;
|
var cameraId = sCameraId;
|
||||||
|
|
@ -126,8 +141,7 @@ class MainCameraController {
|
||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
for (; cameraId < AppEnvironment.cameras.length; cameraId++) {
|
for (; cameraId < AppEnvironment.cameras.length; cameraId++) {
|
||||||
if (AppEnvironment.cameras[cameraId].lensDirection ==
|
if (AppEnvironment.cameras[cameraId].lensDirection == CameraLensDirection.back) {
|
||||||
CameraLensDirection.back) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,18 +150,23 @@ class MainCameraController {
|
||||||
selectedCameraDetails.isZoomAble = false;
|
selectedCameraDetails.isZoomAble = false;
|
||||||
|
|
||||||
if (cameraController == null) {
|
if (cameraController == null) {
|
||||||
|
final hasMic = await Permission.microphone.isGranted;
|
||||||
|
if (sessionId != _cameraSessionId) return;
|
||||||
|
|
||||||
cameraController = CameraController(
|
cameraController = CameraController(
|
||||||
AppEnvironment.cameras[cameraId],
|
AppEnvironment.cameras[cameraId],
|
||||||
ResolutionPreset.high,
|
ResolutionPreset.high,
|
||||||
enableAudio: await Permission.microphone.isGranted,
|
enableAudio: hasMic,
|
||||||
imageFormatGroup: Platform.isAndroid
|
imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,
|
||||||
? ImageFormatGroup.nv21
|
|
||||||
: ImageFormatGroup.bgra8888,
|
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await cameraController?.initialize();
|
_initializeFuture = cameraController?.initialize();
|
||||||
|
await _initializeFuture;
|
||||||
|
if (cameraController == null) return;
|
||||||
await cameraController?.startImageStream(_processCameraImage);
|
await cameraController?.startImageStream(_processCameraImage);
|
||||||
|
if (cameraController == null) return;
|
||||||
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
|
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||||
|
if (cameraController == null) return;
|
||||||
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
|
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
|
||||||
await cameraController?.setVideoStabilizationMode(
|
await cameraController?.setVideoStabilizationMode(
|
||||||
VideoStabilizationMode.level1,
|
VideoStabilizationMode.level1,
|
||||||
|
|
@ -165,10 +184,13 @@ class MainCameraController {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.info(e);
|
Log.info(e);
|
||||||
}
|
}
|
||||||
|
if (cameraController == null) return;
|
||||||
selectedCameraDetails.scaleFactor = 1;
|
selectedCameraDetails.scaleFactor = 1;
|
||||||
|
|
||||||
await cameraController?.setZoomLevel(1);
|
await cameraController?.setZoomLevel(1);
|
||||||
|
if (cameraController == null) return;
|
||||||
await cameraController?.setDescription(AppEnvironment.cameras[cameraId]);
|
await cameraController?.setDescription(AppEnvironment.cameras[cameraId]);
|
||||||
|
if (cameraController == null) return;
|
||||||
try {
|
try {
|
||||||
if (!isVideoRecording) {
|
if (!isVideoRecording) {
|
||||||
await cameraController?.startImageStream(_processCameraImage);
|
await cameraController?.startImageStream(_processCameraImage);
|
||||||
|
|
@ -179,20 +201,19 @@ class MainCameraController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (cameraController == null) return;
|
||||||
await cameraController?.lockCaptureOrientation(
|
await cameraController?.lockCaptureOrientation(
|
||||||
DeviceOrientation.portraitUp,
|
DeviceOrientation.portraitUp,
|
||||||
);
|
);
|
||||||
|
if (cameraController == null) return;
|
||||||
await cameraController?.setFlashMode(
|
await cameraController?.setFlashMode(
|
||||||
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
|
||||||
);
|
);
|
||||||
selectedCameraDetails.maxAvailableZoom =
|
if (cameraController == null) return;
|
||||||
await cameraController?.getMaxZoomLevel() ?? 1;
|
selectedCameraDetails.maxAvailableZoom = await cameraController?.getMaxZoomLevel() ?? 1;
|
||||||
selectedCameraDetails.minAvailableZoom =
|
selectedCameraDetails.minAvailableZoom = await cameraController?.getMinZoomLevel() ?? 1;
|
||||||
await cameraController?.getMinZoomLevel() ?? 1;
|
|
||||||
selectedCameraDetails
|
selectedCameraDetails
|
||||||
..isZoomAble =
|
..isZoomAble = selectedCameraDetails.maxAvailableZoom != selectedCameraDetails.minAvailableZoom
|
||||||
selectedCameraDetails.maxAvailableZoom !=
|
|
||||||
selectedCameraDetails.minAvailableZoom
|
|
||||||
..cameraLoaded = true
|
..cameraLoaded = true
|
||||||
..cameraId = cameraId;
|
..cameraId = cameraId;
|
||||||
|
|
||||||
|
|
@ -201,7 +222,7 @@ class MainCameraController {
|
||||||
isSelectingFaceFilters = false;
|
isSelectingFaceFilters = false;
|
||||||
setFilter(FaceFilterType.none);
|
setFilter(FaceFilterType.none);
|
||||||
zoomButtonKey = GlobalKey();
|
zoomButtonKey = GlobalKey();
|
||||||
setState();
|
setState?.call();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
cameraController = null;
|
cameraController = null;
|
||||||
|
|
@ -214,8 +235,7 @@ class MainCameraController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onTapDown(TapDownDetails details) async {
|
Future<void> onTapDown(TapDownDetails details) async {
|
||||||
final box =
|
final box = cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
cameraPreviewKey.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
final localPosition = box.globalToLocal(details.globalPosition);
|
final localPosition = box.globalToLocal(details.globalPosition);
|
||||||
|
|
||||||
|
|
@ -224,15 +244,14 @@ class MainCameraController {
|
||||||
final dx = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
|
final dx = (localPosition.dx / box.size.width).clamp(0.0, 1.0);
|
||||||
final dy = (localPosition.dy / box.size.height).clamp(0.0, 1.0);
|
final dy = (localPosition.dy / box.size.height).clamp(0.0, 1.0);
|
||||||
|
|
||||||
setState();
|
setState?.call();
|
||||||
|
|
||||||
await HapticFeedback.lightImpact();
|
await HapticFeedback.lightImpact();
|
||||||
try {
|
try {
|
||||||
await cameraController?.setFocusPoint(Offset(dx, dy));
|
await cameraController?.setFocusPoint(Offset(dx, dy));
|
||||||
await cameraController?.setFocusMode(FocusMode.auto);
|
await cameraController?.setFocusMode(FocusMode.auto);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is CameraException &&
|
if (e is CameraException && (e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
|
||||||
(e.code == 'setFocusPointFailed' || e.code == 'setFocusModeFailed')) {
|
|
||||||
Log.info('Focus point or mode not supported on this device');
|
Log.info('Focus point or mode not supported on this device');
|
||||||
} else {
|
} else {
|
||||||
Log.warn(e);
|
Log.warn(e);
|
||||||
|
|
@ -243,7 +262,7 @@ class MainCameraController {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
focusPointOffset = null;
|
focusPointOffset = null;
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setFilter(FaceFilterType type) {
|
void setFilter(FaceFilterType type) {
|
||||||
|
|
@ -253,7 +272,7 @@ class MainCameraController {
|
||||||
facePaint = null;
|
facePaint = null;
|
||||||
_isBusyFaces = false;
|
_isBusyFaces = false;
|
||||||
}
|
}
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
FaceFilterPainter? faceFilterPainter;
|
FaceFilterPainter? faceFilterPainter;
|
||||||
|
|
@ -273,8 +292,7 @@ class MainCameraController {
|
||||||
if (inputImage == null) return;
|
if (inputImage == null) return;
|
||||||
_processBarcode(inputImage);
|
_processBarcode(inputImage);
|
||||||
// check if front camera is selected
|
// check if front camera is selected
|
||||||
if (cameraController?.description.lensDirection ==
|
if (cameraController?.description.lensDirection == CameraLensDirection.front) {
|
||||||
CameraLensDirection.front) {
|
|
||||||
if (_currentFilterType != FaceFilterType.none) {
|
if (_currentFilterType != FaceFilterType.none) {
|
||||||
_processFaces(inputImage);
|
_processFaces(inputImage);
|
||||||
}
|
}
|
||||||
|
|
@ -293,16 +311,14 @@ class MainCameraController {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
|
||||||
} else if (Platform.isAndroid) {
|
} else if (Platform.isAndroid) {
|
||||||
var rotationCompensation =
|
var rotationCompensation = _orientations[cameraController!.value.deviceOrientation];
|
||||||
_orientations[cameraController!.value.deviceOrientation];
|
|
||||||
if (rotationCompensation == null) return null;
|
if (rotationCompensation == null) return null;
|
||||||
if (camera.lensDirection == CameraLensDirection.front) {
|
if (camera.lensDirection == CameraLensDirection.front) {
|
||||||
// front-facing
|
// front-facing
|
||||||
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
|
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
|
||||||
} else {
|
} else {
|
||||||
// back-facing
|
// back-facing
|
||||||
rotationCompensation =
|
rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
|
||||||
(sensorOrientation - rotationCompensation + 360) % 360;
|
|
||||||
}
|
}
|
||||||
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
|
||||||
}
|
}
|
||||||
|
|
@ -344,9 +360,7 @@ class MainCameraController {
|
||||||
if (_isBusy) return;
|
if (_isBusy) return;
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||||
if (inputImage.metadata?.size != null &&
|
if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) {
|
||||||
inputImage.metadata?.rotation != null &&
|
|
||||||
cameraController != null) {
|
|
||||||
final painter = BarcodeDetectorPainter(
|
final painter = BarcodeDetectorPainter(
|
||||||
barcodes,
|
barcodes,
|
||||||
inputImage.metadata!.size,
|
inputImage.metadata!.size,
|
||||||
|
|
@ -423,16 +437,14 @@ class MainCameraController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_isBusy = false;
|
_isBusy = false;
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processFaces(InputImage inputImage) async {
|
Future<void> _processFaces(InputImage inputImage) async {
|
||||||
if (_isBusyFaces) return;
|
if (_isBusyFaces) return;
|
||||||
_isBusyFaces = true;
|
_isBusyFaces = true;
|
||||||
final faces = await _faceDetector.processImage(inputImage);
|
final faces = await _faceDetector.processImage(inputImage);
|
||||||
if (inputImage.metadata?.size != null &&
|
if (inputImage.metadata?.size != null && inputImage.metadata?.rotation != null && cameraController != null) {
|
||||||
inputImage.metadata?.rotation != null &&
|
|
||||||
cameraController != null) {
|
|
||||||
if (faces.isNotEmpty) {
|
if (faces.isNotEmpty) {
|
||||||
CustomPainter? painter;
|
CustomPainter? painter;
|
||||||
switch (_currentFilterType) {
|
switch (_currentFilterType) {
|
||||||
|
|
@ -471,6 +483,6 @@ class MainCameraController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_isBusyFaces = false;
|
_isBusyFaces = false;
|
||||||
setState();
|
setState?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||||
|
|
||||||
class CameraZoomButtons extends StatefulWidget {
|
String beautifulZoomScale(double scale) {
|
||||||
|
var tmp = scale.toStringAsFixed(1);
|
||||||
|
if (tmp[0] == '0') {
|
||||||
|
tmp = tmp.substring(1, tmp.length);
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CameraZoomButtons extends StatelessWidget {
|
||||||
const CameraZoomButtons({
|
const CameraZoomButtons({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.updateScaleFactor,
|
required this.updateScaleFactor,
|
||||||
|
|
@ -25,32 +33,10 @@ class CameraZoomButtons extends StatefulWidget {
|
||||||
final Future<void> Function(int sCameraId, bool init) selectCamera;
|
final Future<void> Function(int sCameraId, bool init) selectCamera;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CameraZoomButtons> createState() => _CameraZoomButtonsState();
|
Widget build(BuildContext context) {
|
||||||
}
|
final showWideAngleZoom = selectedCameraDetails.minAvailableZoom < 1;
|
||||||
|
|
||||||
String beautifulZoomScale(double scale) {
|
|
||||||
var tmp = scale.toStringAsFixed(1);
|
|
||||||
if (tmp[0] == '0') {
|
|
||||||
tmp = tmp.substring(1, tmp.length);
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|
||||||
bool showWideAngleZoom = false;
|
|
||||||
bool showWideAngleZoomIOS = false;
|
|
||||||
bool _isDisposed = false;
|
|
||||||
int? _wideCameraIndex;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
unawaited(initAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
|
||||||
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
|
|
||||||
|
|
||||||
|
int? wideCameraIndex;
|
||||||
var index = AppEnvironment.cameras.indexWhere(
|
var index = AppEnvironment.cameras.indexWhere(
|
||||||
(t) => t.lensType == CameraLensType.ultraWide,
|
(t) => t.lensType == CameraLensType.ultraWide,
|
||||||
);
|
);
|
||||||
|
|
@ -60,33 +46,13 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_wideCameraIndex = index;
|
wideCameraIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isFront =
|
final isFront = controller.description.lensDirection == CameraLensDirection.front;
|
||||||
widget.controller.description.lensDirection ==
|
|
||||||
CameraLensDirection.front;
|
|
||||||
|
|
||||||
if (!showWideAngleZoom &&
|
final showWideAngleZoomIOS = !showWideAngleZoom && Platform.isIOS && wideCameraIndex != null && !isFront;
|
||||||
Platform.isIOS &&
|
|
||||||
_wideCameraIndex != null &&
|
|
||||||
!isFront) {
|
|
||||||
showWideAngleZoomIOS = true;
|
|
||||||
} else {
|
|
||||||
showWideAngleZoomIOS = false;
|
|
||||||
}
|
|
||||||
if (_isDisposed) return;
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_isDisposed = true; // Set the flag to true when disposing
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final zoomButtonStyle = TextButton.styleFrom(
|
final zoomButtonStyle = TextButton.styleFrom(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|
@ -97,24 +63,21 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
|
|
||||||
const zoomTextStyle = TextStyle(fontSize: 13);
|
const zoomTextStyle = TextStyle(fontSize: 13);
|
||||||
final isSmallerFocused =
|
final isSmallerFocused =
|
||||||
widget.scaleFactor < 1 ||
|
scaleFactor < 1 || (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex);
|
||||||
(showWideAngleZoomIOS &&
|
|
||||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
|
||||||
final isMiddleFocused =
|
final isMiddleFocused =
|
||||||
widget.scaleFactor >= 1 &&
|
scaleFactor >= 1 &&
|
||||||
widget.scaleFactor < 2 &&
|
scaleFactor < 2 &&
|
||||||
!(showWideAngleZoomIOS &&
|
!(showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex);
|
||||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
|
||||||
|
|
||||||
final maxLevel = max(
|
final maxLevel = max(
|
||||||
min(widget.selectedCameraDetails.maxAvailableZoom, 2),
|
min(selectedCameraDetails.maxAvailableZoom, 2),
|
||||||
widget.scaleFactor,
|
scaleFactor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final minLevel = beautifulZoomScale(
|
final minLevel = beautifulZoomScale(
|
||||||
widget.selectedCameraDetails.minAvailableZoom,
|
selectedCameraDetails.minAvailableZoom,
|
||||||
);
|
);
|
||||||
final currentLevel = beautifulZoomScale(widget.scaleFactor);
|
final currentLevel = beautifulZoomScale(scaleFactor);
|
||||||
return Center(
|
return Center(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(40),
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
|
@ -132,20 +95,18 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (showWideAngleZoomIOS) {
|
if (showWideAngleZoomIOS) {
|
||||||
if (_wideCameraIndex != null) {
|
if (wideCameraIndex != null) {
|
||||||
await widget.selectCamera(_wideCameraIndex!, true);
|
await selectCamera(wideCameraIndex, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final level = await widget.controller.getMinZoomLevel();
|
final level = await controller.getMinZoomLevel();
|
||||||
widget.updateScaleFactor(level);
|
updateScaleFactor(level);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: showWideAngleZoomIOS
|
child: showWideAngleZoomIOS
|
||||||
? const Text('0.5')
|
? const Text('0.5')
|
||||||
: Text(
|
: Text(
|
||||||
widget.scaleFactor < 1
|
scaleFactor < 1 ? '${currentLevel}x' : '${minLevel}x',
|
||||||
? '${currentLevel}x'
|
|
||||||
: '${minLevel}x',
|
|
||||||
style: zoomTextStyle,
|
style: zoomTextStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -156,39 +117,33 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (showWideAngleZoomIOS &&
|
if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) {
|
||||||
widget.selectedCameraDetails.cameraId ==
|
await selectCamera(0, true);
|
||||||
_wideCameraIndex) {
|
|
||||||
await widget.selectCamera(0, true);
|
|
||||||
} else {
|
} else {
|
||||||
widget.updateScaleFactor(1.0);
|
updateScaleFactor(1.0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
isMiddleFocused
|
isMiddleFocused ? '${beautifulZoomScale(scaleFactor)}x' : '1.0x',
|
||||||
? '${beautifulZoomScale(widget.scaleFactor)}x'
|
|
||||||
: '1.0x',
|
|
||||||
style: zoomTextStyle,
|
style: zoomTextStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: zoomButtonStyle.copyWith(
|
style: zoomButtonStyle.copyWith(
|
||||||
foregroundColor: WidgetStateProperty.all(
|
foregroundColor: WidgetStateProperty.all(
|
||||||
(widget.scaleFactor >= 2) ? Colors.yellow : Colors.white,
|
(scaleFactor >= 2) ? Colors.yellow : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final level = min(
|
final level = min(
|
||||||
await widget.controller.getMaxZoomLevel(),
|
await controller.getMaxZoomLevel(),
|
||||||
2,
|
2,
|
||||||
).toDouble();
|
).toDouble();
|
||||||
|
|
||||||
if (showWideAngleZoomIOS &&
|
if (showWideAngleZoomIOS && selectedCameraDetails.cameraId == wideCameraIndex) {
|
||||||
widget.selectedCameraDetails.cameraId ==
|
await selectCamera(0, true);
|
||||||
_wideCameraIndex) {
|
|
||||||
await widget.selectCamera(0, true);
|
|
||||||
}
|
}
|
||||||
widget.updateScaleFactor(level);
|
updateScaleFactor(level);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'${beautifulZoomScale(maxLevel.toDouble())}x',
|
'${beautifulZoomScale(maxLevel.toDouble())}x',
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_mainCameraController.setState = null;
|
||||||
_mainCameraController.closeCamera();
|
_mainCameraController.closeCamera();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -31,21 +32,24 @@ class QrCodeScannerViewState extends State<QrCodeScannerView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: Stack(
|
||||||
onDoubleTap: _mainCameraController.onDoubleTap,
|
children: [
|
||||||
onTapDown: _mainCameraController.onTapDown,
|
MainCameraPreview(
|
||||||
child: Stack(
|
mainCameraController: _mainCameraController,
|
||||||
children: [
|
),
|
||||||
MainCameraPreview(
|
Positioned.fill(
|
||||||
mainCameraController: _mainCameraController,
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||||
|
onTapDown: _mainCameraController.onTapDown,
|
||||||
),
|
),
|
||||||
CameraPreviewControllerView(
|
),
|
||||||
mainController: _mainCameraController,
|
CameraPreviewControllerView(
|
||||||
hideControllers: true,
|
mainController: _mainCameraController,
|
||||||
isVisible: true,
|
hideControllers: true,
|
||||||
),
|
isVisible: true,
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_mainCameraController.setState = null;
|
||||||
_mainCameraController.closeCamera();
|
_mainCameraController.closeCamera();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -33,21 +34,24 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: Stack(
|
||||||
onDoubleTap: _mainCameraController.onDoubleTap,
|
children: [
|
||||||
onTapDown: _mainCameraController.onTapDown,
|
MainCameraPreview(
|
||||||
child: Stack(
|
mainCameraController: _mainCameraController,
|
||||||
children: [
|
),
|
||||||
MainCameraPreview(
|
Positioned.fill(
|
||||||
mainCameraController: _mainCameraController,
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||||
|
onTapDown: _mainCameraController.onTapDown,
|
||||||
),
|
),
|
||||||
CameraPreviewControllerView(
|
),
|
||||||
mainController: _mainCameraController,
|
CameraPreviewControllerView(
|
||||||
sendToGroup: widget.sendToGroup,
|
mainController: _mainCameraController,
|
||||||
isVisible: true,
|
sendToGroup: widget.sendToGroup,
|
||||||
),
|
isVisible: true,
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -15,6 +13,7 @@ import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/components/contact_request_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||||
import 'package:twonly/src/visual/elements/headline.element.dart';
|
import 'package:twonly/src/visual/elements/headline.element.dart';
|
||||||
|
|
@ -22,6 +21,7 @@ import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
|
import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
||||||
|
|
||||||
class ShareImageView extends StatefulWidget {
|
class ShareImageView extends StatefulWidget {
|
||||||
const ShareImageView({
|
const ShareImageView({
|
||||||
|
|
@ -111,9 +111,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
for (final group in groups) {
|
for (final group in groups) {
|
||||||
if (group.pinned) continue;
|
if (group.pinned) continue;
|
||||||
if (!group.archived &&
|
if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) {
|
||||||
getFlameCounterFromGroup(group).counter > 0 &&
|
|
||||||
bestFriends.length < 6) {
|
|
||||||
bestFriends.add(group);
|
bestFriends.add(group);
|
||||||
} else {
|
} else {
|
||||||
otherUsers.add(group);
|
otherUsers.add(group);
|
||||||
|
|
@ -133,10 +131,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
await updateGroups(
|
await updateGroups(
|
||||||
_allGroups
|
_allGroups
|
||||||
.where(
|
.where(
|
||||||
(x) =>
|
(x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId),
|
||||||
!x.archived ||
|
|
||||||
!hideArchivedUsers ||
|
|
||||||
widget.selectedGroupIds.contains(x.groupId),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
@ -160,31 +155,23 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.lang.shareImageTitle),
|
title: Text(context.lang.shareImageTitle),
|
||||||
|
actions: const [
|
||||||
|
ContactRequestBadgeComp(),
|
||||||
|
SizedBox(width: 15),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: 40,
|
bottom: 40,
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 20,
|
|
||||||
right: 10,
|
right: 10,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (_allGroups.isEmpty)
|
if (_allGroups.isEmpty)
|
||||||
Expanded(
|
const EmptyChatListComp()
|
||||||
child: Center(
|
else ...[
|
||||||
child: FilledButton.icon(
|
|
||||||
icon: const Icon(Icons.person_add),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
label: Text(
|
|
||||||
context.lang.chatListViewSearchUserNameBtn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (_allGroups.isNotEmpty)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
|
@ -195,163 +182,162 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ShortcutRowComp(
|
ShortcutRowComp(
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
||||||
BestFriendsSelector(
|
BestFriendsSelector(
|
||||||
groups: _pinnedContacts,
|
groups: _pinnedContacts,
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
title: context.lang.shareImagePinnedContacts,
|
title: context.lang.shareImagePinnedContacts,
|
||||||
showSelectAll:
|
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
BestFriendsSelector(
|
||||||
BestFriendsSelector(
|
groups: _bestFriends,
|
||||||
groups: _bestFriends,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
title: context.lang.shareImageBestFriends,
|
||||||
title: context.lang.shareImageBestFriends,
|
showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||||
showSelectAll:
|
),
|
||||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
const SizedBox(height: 10),
|
||||||
),
|
if (_otherUsers.isNotEmpty)
|
||||||
const SizedBox(height: 10),
|
Row(
|
||||||
if (_otherUsers.isNotEmpty)
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
HeadLineComp(context.lang.shareImageAllUsers),
|
||||||
children: [
|
if (_allGroups.any((x) => x.archived))
|
||||||
HeadLineComp(context.lang.shareImageAllUsers),
|
Row(
|
||||||
if (_allGroups.any((x) => x.archived))
|
children: [
|
||||||
Row(
|
Text(
|
||||||
children: [
|
context.lang.shareImageShowArchived,
|
||||||
Text(
|
style: const TextStyle(fontSize: 10),
|
||||||
context.lang.shareImageShowArchived,
|
),
|
||||||
style: const TextStyle(fontSize: 10),
|
Transform.scale(
|
||||||
),
|
scale: 0.75,
|
||||||
Transform.scale(
|
child: Checkbox(
|
||||||
scale: 0.75,
|
value: !hideArchivedUsers,
|
||||||
child: Checkbox(
|
side: WidgetStateBorderSide.resolveWith(
|
||||||
value: !hideArchivedUsers,
|
(states) {
|
||||||
side: WidgetStateBorderSide.resolveWith(
|
if (states.contains(WidgetState.selected)) {
|
||||||
(states) {
|
return const BorderSide(width: 0);
|
||||||
if (states.contains(WidgetState.selected)) {
|
}
|
||||||
return const BorderSide(width: 0);
|
return BorderSide(
|
||||||
}
|
color: Theme.of(
|
||||||
return BorderSide(
|
context,
|
||||||
color: Theme.of(
|
).colorScheme.outline,
|
||||||
context,
|
);
|
||||||
).colorScheme.outline,
|
},
|
||||||
);
|
),
|
||||||
|
onChanged: (a) async {
|
||||||
|
hideArchivedUsers = !hideArchivedUsers;
|
||||||
|
await _filterUsers(lastQuery);
|
||||||
|
if (mounted) setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onChanged: (a) async {
|
|
||||||
hideArchivedUsers = !hideArchivedUsers;
|
|
||||||
await _filterUsers(lastQuery);
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
if (_otherUsers.isNotEmpty)
|
||||||
if (_otherUsers.isNotEmpty)
|
UserList(
|
||||||
Expanded(
|
|
||||||
child: UserList(
|
|
||||||
List.from(_otherUsers),
|
List.from(_otherUsers),
|
||||||
selectedGroupIds: widget.selectedGroupIds,
|
selectedGroupIds: widget.selectedGroupIds,
|
||||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: SizedBox(
|
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||||
height: 168,
|
floatingActionButton: _allGroups.isEmpty
|
||||||
child: Padding(
|
? null
|
||||||
padding: const EdgeInsets.only(bottom: 20, right: 20),
|
: SizedBox(
|
||||||
child: Column(
|
height: 168,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.only(bottom: 20, right: 20),
|
||||||
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
child: Column(
|
||||||
_screenshotImage?.image != null &&
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
userService.currentUser.showShowImagePreviewWhenSending)
|
children: [
|
||||||
SizedBox(
|
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
|
||||||
height: 100,
|
_screenshotImage?.image != null &&
|
||||||
width: 100 * 9 / 16,
|
userService.currentUser.showShowImagePreviewWhenSending)
|
||||||
child: Container(
|
SizedBox(
|
||||||
clipBehavior: Clip.hardEdge,
|
height: 100,
|
||||||
decoration: BoxDecoration(
|
width: 100 * 9 / 16,
|
||||||
border: Border.all(
|
child: Container(
|
||||||
color: context.color.primary,
|
clipBehavior: Clip.hardEdge,
|
||||||
width: 2,
|
decoration: BoxDecoration(
|
||||||
),
|
border: Border.all(
|
||||||
color: context.color.primary,
|
color: context.color.primary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
width: 2,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
color: context.color.primary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: CustomPaint(
|
),
|
||||||
painter: UiImagePainter(_screenshotImage!.image!),
|
child: ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
child: CustomPaint(
|
||||||
),
|
painter: UiImagePainter(_screenshotImage!.image!),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
),
|
||||||
icon: !mediaStoreFutureReady || sendingImage
|
|
||||||
? SizedBox(
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
FilledButton.icon(
|
||||||
onPressed: () async {
|
icon: !mediaStoreFutureReady || sendingImage
|
||||||
if (!mediaStoreFutureReady ||
|
? SizedBox(
|
||||||
widget.selectedGroupIds.isEmpty) {
|
height: 12,
|
||||||
return;
|
width: 12,
|
||||||
}
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||||
|
onPressed: () async {
|
||||||
|
if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingImage = true;
|
sendingImage = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
|
// in case mediaStoreFutureReady is ready, the image is stored in the originalPath
|
||||||
await insertMediaFileInMessagesTable(
|
await insertMediaFileInMessagesTable(
|
||||||
widget.mediaFileService,
|
widget.mediaFileService,
|
||||||
widget.selectedGroupIds.toList(),
|
widget.selectedGroupIds.toList(),
|
||||||
additionalData: widget.additionalData,
|
additionalData: widget.additionalData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||||
),
|
),
|
||||||
backgroundColor: WidgetStateProperty.all<Color>(
|
backgroundColor: WidgetStateProperty.all<Color>(
|
||||||
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
|
||||||
? context.color.onSurface
|
? context.color.onSurface
|
||||||
: context.color.primary,
|
: context.color.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -375,6 +361,8 @@ class UserList extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
restorationId: 'new_message_users_list',
|
restorationId: 'new_message_users_list',
|
||||||
itemCount: groups.length,
|
itemCount: groups.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ class UserCheckbox extends StatelessWidget {
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
substringBy(group.groupName, 12),
|
substringBy(group.groupName, 11),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -214,8 +214,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
List<Widget> get actionsAtTheRight {
|
List<Widget> get actionsAtTheRight {
|
||||||
if (layers.isNotEmpty &&
|
if (layers.isNotEmpty &&
|
||||||
(layers.first.isEditing ||
|
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
|
|
@ -291,13 +290,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
if (media.type == MediaType.video) ...[
|
if (media.type == MediaType.video) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ActionButton(
|
ActionButton(
|
||||||
(mediaService.removeAudio)
|
(mediaService.removeAudio) ? Icons.volume_off_rounded : Icons.volume_up_rounded,
|
||||||
? Icons.volume_off_rounded
|
|
||||||
: Icons.volume_up_rounded,
|
|
||||||
tooltipText: 'Enable Audio in Video',
|
tooltipText: 'Enable Audio in Video',
|
||||||
color: (mediaService.removeAudio)
|
color: (mediaService.removeAudio) ? Colors.white.withAlpha(160) : Colors.white,
|
||||||
? Colors.white.withAlpha(160)
|
|
||||||
: Colors.white,
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await mediaService.toggleRemoveAudio();
|
await mediaService.toggleRemoveAudio();
|
||||||
if (mediaService.removeAudio) {
|
if (mediaService.removeAudio) {
|
||||||
|
|
@ -335,9 +330,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
FontAwesomeIcons.shieldHeart,
|
FontAwesomeIcons.shieldHeart,
|
||||||
tooltipText: context.lang.protectAsARealTwonly,
|
tooltipText: context.lang.protectAsARealTwonly,
|
||||||
color: media.requiresAuthentication
|
color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white,
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Colors.white,
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await mediaService.setRequiresAuth(!media.requiresAuthentication);
|
await mediaService.setRequiresAuth(!media.requiresAuthentication);
|
||||||
selectedGroupIds = HashSet();
|
selectedGroupIds = HashSet();
|
||||||
|
|
@ -383,8 +376,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
|
|
||||||
List<Widget> get actionsAtTheTop {
|
List<Widget> get actionsAtTheTop {
|
||||||
if (layers.isNotEmpty &&
|
if (layers.isNotEmpty &&
|
||||||
(layers.first.isEditing ||
|
(layers.first.isEditing || (layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|
@ -474,6 +466,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
return (layers.first as BackgroundLayerData).image.image;
|
return (layers.first as BackgroundLayerData).image.image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (layers.length == 2) {
|
||||||
|
final filterLayer = layers[1];
|
||||||
|
if (layers.first is BackgroundLayerData && filterLayer is FilterLayerData) {
|
||||||
|
if (filterLayer.page == 1) {
|
||||||
|
return (layers.first as BackgroundLayerData).image.image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (final x in layers) {
|
for (final x in layers) {
|
||||||
x.showCustomButtons = false;
|
x.showCustomButtons = false;
|
||||||
|
|
@ -513,15 +513,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScreenshotImageHelper? image;
|
ScreenshotImageHelper? image;
|
||||||
var bytes = await widget.screenshotImage?.getBytes();
|
|
||||||
if (media.type == MediaType.gif) {
|
if (media.type == MediaType.gif) {
|
||||||
|
final bytes = await widget.screenshotImage?.getBytes();
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
mediaService.originalPath.writeAsBytesSync(bytes.toList());
|
mediaService.originalPath.writeAsBytesSync(bytes.toList());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
image = await getEditedImageBytes();
|
image = await getEditedImageBytes();
|
||||||
if (image == null) return null;
|
if (image == null) return null;
|
||||||
bytes = await image.getBytes();
|
final bytes = await image.getBytes();
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
Log.error('imageBytes are empty');
|
Log.error('imageBytes are empty');
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -657,9 +657,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
await askToCloseThenClose();
|
await askToCloseThenClose();
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: widget.sharedFromGallery
|
backgroundColor: 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,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/connection_status.comp.dart';
|
import 'package:twonly/src/visual/components/connection_status.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
||||||
|
|
@ -31,11 +32,16 @@ class ChatListView extends StatefulWidget {
|
||||||
|
|
||||||
class _ChatListViewState extends State<ChatListView> {
|
class _ChatListViewState extends State<ChatListView> {
|
||||||
StreamSubscription<void>? _userSub;
|
StreamSubscription<void>? _userSub;
|
||||||
late StreamSubscription<List<Group>> _contactsSub;
|
StreamSubscription<List<Group>>? _contactsSub;
|
||||||
|
StreamSubscription<List<Contact>>? _contactsCountSub;
|
||||||
List<Group> _groupsNotPinned = [];
|
List<Group> _groupsNotPinned = [];
|
||||||
List<Group> _groupsPinned = [];
|
List<Group> _groupsPinned = [];
|
||||||
List<Group> _groupsArchived = [];
|
List<Group> _groupsArchived = [];
|
||||||
|
|
||||||
|
bool _hasContacts = false;
|
||||||
|
bool _loading = true;
|
||||||
|
bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty;
|
||||||
|
|
||||||
GlobalKey searchForOtherUsers = GlobalKey();
|
GlobalKey searchForOtherUsers = GlobalKey();
|
||||||
bool showFeedbackShortcut = false;
|
bool showFeedbackShortcut = false;
|
||||||
|
|
||||||
|
|
@ -58,33 +64,35 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_contactsSub = stream.listen((groups) {
|
_contactsSub = stream.listen((groups) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_groupsNotPinned = groups
|
_groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList();
|
||||||
.where((x) => !x.pinned && !x.archived)
|
|
||||||
.toList();
|
|
||||||
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
||||||
_groupsArchived = groups.where((x) => x.archived).toList();
|
_groupsArchived = groups.where((x) => x.archived).toList();
|
||||||
|
_loading = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_countContactRequestStream = twonlyDB.contactsDao
|
_contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) {
|
||||||
.watchContactsRequestedCount()
|
if (!mounted) return;
|
||||||
.listen((update) {
|
setState(() {
|
||||||
if (update != null) {
|
_hasContacts = contacts.isNotEmpty;
|
||||||
if (!mounted) return;
|
});
|
||||||
setState(() {
|
});
|
||||||
_countContactRequest = update;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_countAnnouncedStream = twonlyDB.userDiscoveryDao
|
_countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) {
|
||||||
.watchNewAnnouncementsWithDataCount()
|
if (update != null) {
|
||||||
.listen((update) {
|
if (!mounted) return;
|
||||||
if (!mounted) return;
|
setState(() {
|
||||||
setState(() {
|
_countContactRequest = update;
|
||||||
_countAnnouncedUsers = update;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_countAnnouncedUsers = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||||
|
|
@ -93,8 +101,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
changeLog.codeUnits,
|
changeLog.codeUnits,
|
||||||
)).bytes;
|
)).bytes;
|
||||||
if (!userService.currentUser.hideChangeLog &&
|
if (!userService.currentUser.hideChangeLog &&
|
||||||
userService.currentUser.lastChangeLogHash.toString() !=
|
userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||||
changeLogHash.toString()) {
|
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.lastChangeLogHash = changeLogHash;
|
u.lastChangeLogHash = changeLogHash;
|
||||||
});
|
});
|
||||||
|
|
@ -113,7 +120,8 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contactsSub.cancel();
|
_contactsSub?.cancel();
|
||||||
|
_contactsCountSub?.cancel();
|
||||||
_countContactRequestStream.cancel();
|
_countContactRequestStream.cancel();
|
||||||
_countAnnouncedStream.cancel();
|
_countAnnouncedStream.cancel();
|
||||||
_userSub?.cancel();
|
_userSub?.cancel();
|
||||||
|
|
@ -182,16 +190,11 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: NotificationBadgeComp(
|
child: NotificationBadgeComp(
|
||||||
backgroundColor: isDarkMode(context)
|
backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
textColor: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
count: (_countAnnouncedUsers + _countContactRequest)
|
count: (_countAnnouncedUsers + _countContactRequest).toString(),
|
||||||
.toString(),
|
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
color: (_countAnnouncedUsers + _countContactRequest > 0)
|
color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null,
|
||||||
? Colors.black
|
|
||||||
: null,
|
|
||||||
key: searchForOtherUsers,
|
key: searchForOtherUsers,
|
||||||
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
|
@ -217,21 +220,15 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
children: [
|
children: [
|
||||||
const FinishSetupComp(),
|
const FinishSetupComp(),
|
||||||
const MissingBackupComp(),
|
const MissingBackupComp(),
|
||||||
if (_groupsNotPinned.isEmpty &&
|
if (_loading)
|
||||||
_groupsPinned.isEmpty &&
|
const Expanded(
|
||||||
_groupsArchived.isEmpty)
|
child: SizedBox.shrink(),
|
||||||
|
)
|
||||||
|
else if (!_hasOpenGroup)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: ListView(
|
||||||
child: Padding(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(10),
|
children: const [EmptyChatListComp()],
|
||||||
child: FilledButton.icon(
|
|
||||||
icon: const Icon(Icons.person_add),
|
|
||||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
|
||||||
label: Text(
|
|
||||||
context.lang.chatListViewSearchUserNameBtn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -243,10 +240,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
_groupsNotPinned.length +
|
_groupsNotPinned.length +
|
||||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >=
|
if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) {
|
||||||
_groupsNotPinned.length +
|
|
||||||
_groupsPinned.length +
|
|
||||||
(_groupsPinned.isNotEmpty ? 1 : 0)) {
|
|
||||||
if (_groupsArchived.isEmpty) return Container();
|
if (_groupsArchived.isEmpty) return Container();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
@ -289,42 +283,45 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: Padding(
|
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
floatingActionButton: !_hasContacts
|
||||||
child: Column(
|
? null
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.only(bottom: 30),
|
||||||
Material(
|
child: Column(
|
||||||
elevation: 3,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
shape: const CircleBorder(),
|
children: [
|
||||||
color: context.color.primary,
|
Material(
|
||||||
child: InkWell(
|
elevation: 3,
|
||||||
borderRadius: BorderRadius.circular(12),
|
shape: const CircleBorder(),
|
||||||
onTap: () => context.push(Routes.settingsPublicProfile),
|
color: context.color.primary,
|
||||||
child: SizedBox(
|
child: InkWell(
|
||||||
width: 45,
|
borderRadius: BorderRadius.circular(12),
|
||||||
height: 45,
|
onTap: () => context.push(Routes.settingsPublicProfile),
|
||||||
child: Center(
|
child: SizedBox(
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
child: Center(
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.qrcode,
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FloatingActionButton(
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
onPressed: () => context.push(Routes.chatsStartNewChat),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.qrcode,
|
FontAwesomeIcons.penToSquare,
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
FloatingActionButton(
|
|
||||||
backgroundColor: context.color.primary,
|
|
||||||
onPressed: () => context.push(Routes.chatsStartNewChat),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.penToSquare,
|
|
||||||
color: isDarkMode(context) ? Colors.black : Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons;
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
class EmptyChatListComp extends StatelessWidget {
|
||||||
|
const EmptyChatListComp({super.key});
|
||||||
|
|
||||||
|
Future<void> _shareProfile(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
final pubKey = await getUserPublicKey();
|
||||||
|
final params = ShareParams(
|
||||||
|
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
|
);
|
||||||
|
await SharePlus.instance.share(params);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await context.push(Routes.chatsAddNewUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Find your first friend',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Let friends scan your QR code, or share them your profile.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
const Center(child: ProfileQrCodeComp()),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
// 3. Action Buttons
|
||||||
|
// Button 1: Share Profile (Full Width)
|
||||||
|
FilledButton.icon(
|
||||||
|
style: primaryColorButtonStyle,
|
||||||
|
onPressed: () => _shareProfile(context),
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20),
|
||||||
|
label: const Text(
|
||||||
|
'Share your profile',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Button Row: Scan QR Code & Enter Username
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: secondaryGreyButtonStyle(context),
|
||||||
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
|
icon: const Icon(Icons.qr_code_scanner_rounded, size: 20),
|
||||||
|
label: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'Scan QR Code',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: secondaryGreyButtonStyle(context),
|
||||||
|
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||||
|
icon: const Icon(Icons.person_add_rounded, size: 20),
|
||||||
|
label: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
'Add by Username',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
|
||||||
|
|
@ -73,37 +74,31 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_lastReactionStream = twonlyDB.reactionsDao
|
_lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) {
|
||||||
.watchLastReactions(widget.group.groupId)
|
if (!mounted) return;
|
||||||
.listen((update) {
|
setState(() {
|
||||||
if (!mounted) return;
|
_lastReaction = update;
|
||||||
setState(() {
|
});
|
||||||
_lastReaction = update;
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
_messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) {
|
||||||
.watchMessageNotOpened(widget.group.groupId)
|
protectUpdateState.protect(() async {
|
||||||
.listen((update) {
|
await updateState(_lastMessage, update);
|
||||||
protectUpdateState.protect(() async {
|
});
|
||||||
await updateState(_lastMessage, update);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
_lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
||||||
.watchNewestMediaFiles()
|
if (!mounted) return;
|
||||||
.listen((mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
if (!mounted) return;
|
final index = _previewMediaFiles.indexWhere(
|
||||||
for (final mediaFile in mediaFiles) {
|
(t) => t.mediaId == mediaFile.mediaId,
|
||||||
final index = _previewMediaFiles.indexWhere(
|
);
|
||||||
(t) => t.mediaId == mediaFile.mediaId,
|
if (index >= 0) {
|
||||||
);
|
_previewMediaFiles[index] = mediaFile;
|
||||||
if (index >= 0) {
|
}
|
||||||
_previewMediaFiles[index] = mediaFile;
|
}
|
||||||
}
|
setState(() {});
|
||||||
}
|
});
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
|
|
||||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
widget.group.groupId,
|
widget.group.groupId,
|
||||||
|
|
@ -125,9 +120,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
_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 = newMessagesNotOpened
|
final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||||
.where((x) => x.senderId != null)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (receivedMessages.isNotEmpty) {
|
if (receivedMessages.isNotEmpty) {
|
||||||
_previewMessages = receivedMessages;
|
_previewMessages = receivedMessages;
|
||||||
|
|
@ -151,9 +144,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||||
.where((x) => x.type == MessageType.media.name)
|
|
||||||
.toList();
|
|
||||||
if (msgs.isNotEmpty &&
|
if (msgs.isNotEmpty &&
|
||||||
msgs.first.type == MessageType.media.name &&
|
msgs.first.type == MessageType.media.name &&
|
||||||
!msgs.first.isDeletedFromSender &&
|
!msgs.first.isDeletedFromSender &&
|
||||||
|
|
@ -165,8 +156,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = await twonlyDB.mediaFilesDao.getMediaFileById(
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
message.mediaId!,
|
message.mediaId!,
|
||||||
);
|
);
|
||||||
|
|
@ -191,9 +181,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_hasNonOpenedMediaFile) {
|
if (_hasNonOpenedMediaFile) {
|
||||||
final msgs = _previewMessages
|
final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList();
|
||||||
.where((x) => x.type == MessageType.media.name)
|
|
||||||
.toList();
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
msgs.first.mediaId!,
|
msgs.first.mediaId!,
|
||||||
);
|
);
|
||||||
|
|
@ -219,97 +207,99 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GroupContextMenu(
|
return StreamBuilder<void>(
|
||||||
group: widget.group,
|
stream: userService.onUserUpdated,
|
||||||
child: ListTile(
|
builder: (context, snapshot) {
|
||||||
title: Row(
|
return GroupContextMenu(
|
||||||
children: [
|
group: widget.group,
|
||||||
Text(
|
child: ListTile(
|
||||||
substringBy(widget.group.groupName, 30),
|
title: Row(
|
||||||
),
|
children: [
|
||||||
const SizedBox(width: 3),
|
Text(
|
||||||
VerificationBadgeComp(
|
substringBy(widget.group.groupName, 30),
|
||||||
group: widget.group,
|
|
||||||
showOnlyIfVerified: true,
|
|
||||||
clickable: false,
|
|
||||||
size: 12,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: _receiverDeletedAccount
|
|
||||||
? Text(context.lang.userDeletedAccount)
|
|
||||||
: (_currentMessage == null)
|
|
||||||
? (widget.group.totalMediaCounter == 0)
|
|
||||||
? Text(context.lang.chatsTapToSend)
|
|
||||||
: Row(
|
|
||||||
children: [
|
|
||||||
LastMessageTimeComp(
|
|
||||||
dateTime: widget.group.lastMessageExchange,
|
|
||||||
),
|
|
||||||
FlameCounterWidget(
|
|
||||||
groupId: widget.group.groupId,
|
|
||||||
prefix: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Row(
|
|
||||||
children: [
|
|
||||||
TypingIndicatorSubtitleComp(
|
|
||||||
groupId: widget.group.groupId,
|
|
||||||
),
|
|
||||||
MessageSendStateIcon(
|
|
||||||
_previewMessages,
|
|
||||||
_previewMediaFiles,
|
|
||||||
lastReaction: _lastReaction,
|
|
||||||
group: widget.group,
|
|
||||||
),
|
|
||||||
const Text('•'),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
if (_currentMessage != null)
|
|
||||||
LastMessageTimeComp(message: _currentMessage),
|
|
||||||
FlameCounterWidget(
|
|
||||||
groupId: widget.group.groupId,
|
|
||||||
prefix: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
leading: GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
if (widget.group.isDirectChat) {
|
|
||||||
final contacts = await twonlyDB.groupsDao.getGroupContact(
|
|
||||||
widget.group.groupId,
|
|
||||||
);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
await context.push(Routes.profileContact(contacts.first.userId));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
await context.push(Routes.profileGroup(widget.group.groupId));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: AvatarIcon(group: widget.group),
|
|
||||||
),
|
|
||||||
trailing: (widget.group.leftGroup || _receiverDeletedAccount)
|
|
||||||
? null
|
|
||||||
: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_hasNonOpenedMediaFile) {
|
|
||||||
context.push(Routes.chatsMessages(widget.group.groupId));
|
|
||||||
} else {
|
|
||||||
context.push(
|
|
||||||
Routes.chatsCameraSendTo,
|
|
||||||
extra: widget.group,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: FaIcon(
|
|
||||||
_hasNonOpenedMediaFile
|
|
||||||
? FontAwesomeIcons.solidComments
|
|
||||||
: FontAwesomeIcons.camera,
|
|
||||||
color: context.color.outline.withAlpha(150),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 3),
|
||||||
onTap: onTap,
|
VerificationBadgeComp(
|
||||||
),
|
group: widget.group,
|
||||||
|
showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList,
|
||||||
|
clickable: false,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: _receiverDeletedAccount
|
||||||
|
? Text(context.lang.userDeletedAccount)
|
||||||
|
: (_currentMessage == null)
|
||||||
|
? (widget.group.totalMediaCounter == 0)
|
||||||
|
? Text(context.lang.chatsTapToSend)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
LastMessageTimeComp(
|
||||||
|
dateTime: widget.group.lastMessageExchange,
|
||||||
|
),
|
||||||
|
FlameCounterWidget(
|
||||||
|
groupId: widget.group.groupId,
|
||||||
|
prefix: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
TypingIndicatorSubtitleComp(
|
||||||
|
groupId: widget.group.groupId,
|
||||||
|
),
|
||||||
|
MessageSendStateIcon(
|
||||||
|
_previewMessages,
|
||||||
|
_previewMediaFiles,
|
||||||
|
lastReaction: _lastReaction,
|
||||||
|
group: widget.group,
|
||||||
|
),
|
||||||
|
const Text('•'),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage),
|
||||||
|
FlameCounterWidget(
|
||||||
|
groupId: widget.group.groupId,
|
||||||
|
prefix: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (widget.group.isDirectChat) {
|
||||||
|
final contacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
|
widget.group.groupId,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await context.push(Routes.profileContact(contacts.first.userId));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await context.push(Routes.profileGroup(widget.group.groupId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AvatarIcon(group: widget.group),
|
||||||
|
),
|
||||||
|
trailing: (widget.group.leftGroup || _receiverDeletedAccount)
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_hasNonOpenedMediaFile) {
|
||||||
|
context.push(Routes.chatsMessages(widget.group.groupId));
|
||||||
|
} else {
|
||||||
|
context.push(
|
||||||
|
Routes.chatsCameraSendTo,
|
||||||
|
extra: widget.group,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: FaIcon(
|
||||||
|
_hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera,
|
||||||
|
color: context.color.outline.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 2), (
|
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 2), (
|
||||||
_,
|
_,
|
||||||
) async {
|
) async {
|
||||||
await sendTypingIndication(widget.groupId, false);
|
if (_isViewActive()) {
|
||||||
|
await sendTypingIndication(widget.groupId, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
|
||||||
|
|
@ -137,12 +138,24 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
if (widget.message.type == MessageType.contacts.name) {
|
if (widget.message.type == MessageType.contacts.name) {
|
||||||
return ChatContactsEntry(
|
return ChatContactsEntry(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
info: info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.message.type == MessageType.restoreFlameCounter.name) {
|
if (widget.message.type == MessageType.restoreFlameCounter.name) {
|
||||||
return ChatFlameRestoredEntry(
|
return ChatFlameRestoredEntry(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
info: info,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.message.type == MessageType.askAboutUser.name) {
|
||||||
|
return ChatAskAFriendEntry(
|
||||||
|
message: widget.message,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
info: info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
|
||||||
|
class ChatAskAFriendEntry extends StatefulWidget {
|
||||||
|
const ChatAskAFriendEntry({
|
||||||
|
required this.message,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.info,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
final BorderRadiusGeometry borderRadius;
|
||||||
|
final BubbleInfo info;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatAskAFriendEntry> createState() => _ChatAskAFriendEntryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _username;
|
||||||
|
bool _isSent = false;
|
||||||
|
AdditionalMessageData? _data;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isSent = widget.message.senderId == null;
|
||||||
|
if (widget.message.additionalMessageData != null) {
|
||||||
|
try {
|
||||||
|
_data = AdditionalMessageData.fromBuffer(
|
||||||
|
widget.message.additionalMessageData!,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_data = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loadUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUser() async {
|
||||||
|
if (_data == null || !_data!.hasAskAboutUserId()) return;
|
||||||
|
final userId = _data!.askAboutUserId.toInt();
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (_isSent) {
|
||||||
|
// Try getting from contacts
|
||||||
|
final contact = await twonlyDB.contactsDao.getContactById(userId);
|
||||||
|
if (contact != null) {
|
||||||
|
_username = contact.displayName ?? contact.username;
|
||||||
|
} else {
|
||||||
|
// Try getting from announced users
|
||||||
|
final announced = await twonlyDB.userDiscoveryDao
|
||||||
|
.getAnnouncedUserById(userId);
|
||||||
|
if (announced != null && announced.username != null) {
|
||||||
|
_username = announced.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Receiver side: try contacts first
|
||||||
|
final contact = await twonlyDB.contactsDao.getContactById(userId);
|
||||||
|
if (contact != null) {
|
||||||
|
_username = contact.displayName ?? contact.username;
|
||||||
|
} else {
|
||||||
|
// Fetch from API
|
||||||
|
final userdata = await apiService.getUserById(userId);
|
||||||
|
if (userdata != null) {
|
||||||
|
_username = utf8.decode(userdata.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _hideUser() async {
|
||||||
|
if (_data == null || !_data!.hasAskAboutUserId()) return;
|
||||||
|
await twonlyDB.userDiscoveryDao.updateAnnouncedUser(
|
||||||
|
_data!.askAboutUserId.toInt(),
|
||||||
|
const UserDiscoveryAnnouncedUsersCompanion(
|
||||||
|
isHidden: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestUser() async {
|
||||||
|
if (_data == null || !_data!.hasAskAboutUserId()) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final userId = _data!.askAboutUserId.toInt();
|
||||||
|
final userdata = await apiService.getUserById(userId);
|
||||||
|
if (userdata != null) {
|
||||||
|
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||||
|
ContactsCompanion(
|
||||||
|
username: Value(utf8.decode(userdata.username)),
|
||||||
|
userId: Value(userdata.userId.toInt()),
|
||||||
|
requested: const Value(false),
|
||||||
|
blocked: const Value(false),
|
||||||
|
deletedByUser: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await importSignalContactAndCreateRequest(userdata);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_data == null || !_data!.hasAskAboutUserId()) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = _data!.askAboutUserId.toInt();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.info.color,
|
||||||
|
borderRadius: widget.borderRadius,
|
||||||
|
),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
StreamBuilder<Contact?>(
|
||||||
|
stream: twonlyDB.contactsDao.watchContact(userId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final contactInDb = snapshot.data;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (contactInDb != null) {
|
||||||
|
context.push(Routes.profileContact(userId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AvatarIcon(
|
||||||
|
contactId: userId,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (_isLoading && _username == null)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_username != null)
|
||||||
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: Text(
|
||||||
|
_username!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: Text(
|
||||||
|
context.lang.chatAskAFriendUnknownUser(
|
||||||
|
userId.toString(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (contactInDb != null) ...[
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.chevronRight,
|
||||||
|
size: 10,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
] else ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!_isSent) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
context.lang.chatAskAFriendReceivedDescription,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
StreamBuilder<Contact?>(
|
||||||
|
stream: twonlyDB.contactsDao.watchContact(userId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final contactInDb = snapshot.data;
|
||||||
|
if (contactInDb != null) {
|
||||||
|
return Text(
|
||||||
|
context.lang.chatAskAFriendAddedDescription,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _hideUser,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
context.lang.chatAskAFriendHide,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: widget.info.textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
).merge(secondaryGreyButtonStyle(context)),
|
||||||
|
onPressed: _isLoading ? null : _requestUser,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
context.lang.chatAskAFriendRequest,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,10 +18,14 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/c
|
||||||
class ChatContactsEntry extends StatefulWidget {
|
class ChatContactsEntry extends StatefulWidget {
|
||||||
const ChatContactsEntry({
|
const ChatContactsEntry({
|
||||||
required this.message,
|
required this.message,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.info,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final BorderRadiusGeometry borderRadius;
|
||||||
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatContactsEntry> createState() => _ChatContactsEntryState();
|
State<ChatContactsEntry> createState() => _ChatContactsEntryState();
|
||||||
|
|
@ -46,23 +50,14 @@ class _ChatContactsEntryState extends State<ChatContactsEntry> {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final info = getBubbleInfo(
|
|
||||||
context,
|
|
||||||
widget.message,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: info.color,
|
color: widget.info.color,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: widget.borderRadius,
|
||||||
),
|
),
|
||||||
child: IntrinsicWidth(
|
child: IntrinsicWidth(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,22 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
import 'package:twonly/src/visual/elements/better_text.element.dart';
|
||||||
|
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart';
|
||||||
|
|
||||||
class ChatFlameRestoredEntry extends StatelessWidget {
|
class ChatFlameRestoredEntry extends StatelessWidget {
|
||||||
const ChatFlameRestoredEntry({
|
const ChatFlameRestoredEntry({
|
||||||
required this.message,
|
required this.message,
|
||||||
|
required this.borderRadius,
|
||||||
|
required this.info,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final BorderRadiusGeometry borderRadius;
|
||||||
|
final BubbleInfo info;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -34,16 +41,29 @@ class ChatFlameRestoredEntry extends StatelessWidget {
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.orange,
|
color: info.color,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: borderRadius,
|
||||||
),
|
),
|
||||||
child: BetterText(
|
child: Row(
|
||||||
text: context.lang.chatEntryFlameRestored(
|
mainAxisSize: MainAxisSize.min,
|
||||||
data.restoredFlameCounter.toInt(),
|
children: [
|
||||||
),
|
const SizedBox(
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: EmojiAnimationComp(emoji: '🔥'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: BetterText(
|
||||||
|
text: context.lang.chatEntryFlameRestored(
|
||||||
|
data.restoredFlameCounter.toInt(),
|
||||||
|
),
|
||||||
|
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -18,7 +19,9 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/unverified_contact_warning.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/user_discovery_manual_approval.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||||
|
|
||||||
class MessageInput extends StatefulWidget {
|
class MessageInput extends StatefulWidget {
|
||||||
const MessageInput({
|
const MessageInput({
|
||||||
|
|
@ -51,6 +54,8 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
Offset _recordingOffset = Offset.zero;
|
Offset _recordingOffset = Offset.zero;
|
||||||
RecordingState _recordingState = RecordingState.none;
|
RecordingState _recordingState = RecordingState.none;
|
||||||
Timer? _nextTypingIndicator;
|
Timer? _nextTypingIndicator;
|
||||||
|
DateTime? _lastTextChangeTime;
|
||||||
|
int? _contactId;
|
||||||
|
|
||||||
Future<void> _sendMessage() async {
|
Future<void> _sendMessage() async {
|
||||||
if (_textFieldController.text == '') return;
|
if (_textFieldController.text == '') return;
|
||||||
|
|
@ -70,6 +75,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_textFieldController = TextEditingController();
|
_textFieldController = TextEditingController();
|
||||||
|
_textFieldController.addListener(_handleTextChange);
|
||||||
if (widget.group.draftMessage != null) {
|
if (widget.group.draftMessage != null) {
|
||||||
_textFieldController.text = widget.group.draftMessage!;
|
_textFieldController.text = widget.group.draftMessage!;
|
||||||
}
|
}
|
||||||
|
|
@ -78,16 +84,20 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
|
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
|
||||||
_,
|
_,
|
||||||
) async {
|
) async {
|
||||||
if (widget.textFieldFocus.hasFocus) {
|
if (widget.textFieldFocus.hasFocus &&
|
||||||
|
_lastTextChangeTime != null &&
|
||||||
|
DateTime.now().difference(_lastTextChangeTime!) <= const Duration(seconds: 6)) {
|
||||||
await sendTypingIndication(widget.group.groupId, true);
|
await sendTypingIndication(widget.group.groupId, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_initializeControllers();
|
_initializeControllers();
|
||||||
|
_loadContactId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_textFieldController.removeListener(_handleTextChange);
|
||||||
widget.textFieldFocus.removeListener(_handleTextFocusChange);
|
widget.textFieldFocus.removeListener(_handleTextFocusChange);
|
||||||
widget.textFieldFocus.dispose();
|
widget.textFieldFocus.dispose();
|
||||||
recorderController.dispose();
|
recorderController.dispose();
|
||||||
|
|
@ -105,6 +115,10 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTextChange() {
|
||||||
|
_lastTextChangeTime = clock.now();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTextFocusChange() {
|
void _handleTextFocusChange() {
|
||||||
if (widget.textFieldFocus.hasFocus) {
|
if (widget.textFieldFocus.hasFocus) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -194,316 +208,319 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadContactId() async {
|
||||||
|
if (widget.group.isDirectChat) {
|
||||||
|
final members = await twonlyDB.groupsDao.getGroupContact(widget.group.groupId);
|
||||||
|
if (members.isNotEmpty && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_contactId = members.first.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
UserDiscoveryManualApprovalComp(group: widget.group),
|
UserDiscoveryManualApprovalComp(group: widget.group),
|
||||||
Padding(
|
if (_contactId != null)
|
||||||
padding: const EdgeInsets.only(
|
Container(
|
||||||
bottom: 10,
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
left: 10,
|
decoration: BoxDecoration(
|
||||||
top: 10,
|
color: context.color.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: RestoreFlameComp(
|
||||||
|
contactId: _contactId!,
|
||||||
|
flameOnRightSide: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
UnverifiedContactWarningComp(
|
||||||
children: [
|
group: widget.group,
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: Container(
|
padding: const EdgeInsets.only(
|
||||||
padding: const EdgeInsets.symmetric(
|
bottom: 10,
|
||||||
horizontal: 3,
|
left: 10,
|
||||||
),
|
top: 5,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: context.color.surfaceContainer,
|
child: Row(
|
||||||
borderRadius: BorderRadius.circular(20),
|
children: [
|
||||||
),
|
Expanded(
|
||||||
child: Row(
|
child: Container(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
if (_recordingState != RecordingState.recording)
|
horizontal: 3,
|
||||||
GestureDetector(
|
),
|
||||||
onTap: () {
|
decoration: BoxDecoration(
|
||||||
setState(() {
|
color: context.color.surfaceContainer,
|
||||||
_emojiShowing = !_emojiShowing;
|
borderRadius: BorderRadius.circular(20),
|
||||||
if (_emojiShowing) {
|
),
|
||||||
widget.textFieldFocus.unfocus();
|
child: Row(
|
||||||
} else {
|
children: [
|
||||||
widget.textFieldFocus.requestFocus();
|
if (_recordingState != RecordingState.recording)
|
||||||
}
|
GestureDetector(
|
||||||
});
|
onTap: () {
|
||||||
},
|
setState(() {
|
||||||
child: ColoredBox(
|
_emojiShowing = !_emojiShowing;
|
||||||
color: Colors.transparent,
|
if (_emojiShowing) {
|
||||||
child: Padding(
|
widget.textFieldFocus.unfocus();
|
||||||
padding: const EdgeInsets.only(
|
} else {
|
||||||
top: 8,
|
widget.textFieldFocus.requestFocus();
|
||||||
bottom: 8,
|
}
|
||||||
left: 12,
|
});
|
||||||
right: 8,
|
},
|
||||||
),
|
child: ColoredBox(
|
||||||
child: FaIcon(
|
color: Colors.transparent,
|
||||||
size: 20,
|
child: Padding(
|
||||||
_emojiShowing
|
padding: const EdgeInsets.only(
|
||||||
? FontAwesomeIcons.keyboard
|
top: 8,
|
||||||
: FontAwesomeIcons.faceSmile,
|
bottom: 8,
|
||||||
|
left: 12,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
size: 20,
|
||||||
|
_emojiShowing ? FontAwesomeIcons.keyboard : FontAwesomeIcons.faceSmile,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
TextField(
|
||||||
TextField(
|
controller: _textFieldController,
|
||||||
controller: _textFieldController,
|
focusNode: widget.textFieldFocus,
|
||||||
focusNode: widget.textFieldFocus,
|
keyboardType: TextInputType.multiline,
|
||||||
keyboardType: TextInputType.multiline,
|
showCursor: _recordingState != RecordingState.recording,
|
||||||
showCursor:
|
maxLines: 4,
|
||||||
_recordingState != RecordingState.recording,
|
minLines: 1,
|
||||||
maxLines: 4,
|
onChanged: (value) async {
|
||||||
minLines: 1,
|
setState(() {});
|
||||||
onChanged: (value) async {
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
setState(() {});
|
widget.group.groupId,
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
GroupsCompanion(
|
||||||
widget.group.groupId,
|
draftMessage: Value(
|
||||||
GroupsCompanion(
|
_textFieldController.text,
|
||||||
draftMessage: Value(
|
),
|
||||||
_textFieldController.text,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
onSubmitted: (_) {
|
||||||
onSubmitted: (_) {
|
_sendMessage();
|
||||||
_sendMessage();
|
},
|
||||||
},
|
style: const TextStyle(fontSize: 17),
|
||||||
style: const TextStyle(fontSize: 17),
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
hintText: context.lang.chatListDetailInput,
|
||||||
hintText: context.lang.chatListDetailInput,
|
contentPadding: EdgeInsets.zero,
|
||||||
contentPadding: EdgeInsets.zero,
|
border: InputBorder.none,
|
||||||
border: InputBorder.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_recordingState == RecordingState.recording)
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.color.surfaceContainer,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
),
|
||||||
children: [
|
if (_recordingState == RecordingState.recording)
|
||||||
const Padding(
|
Container(
|
||||||
padding: EdgeInsets.only(
|
decoration: BoxDecoration(
|
||||||
top: 14,
|
color: context.color.surfaceContainer,
|
||||||
bottom: 14,
|
borderRadius: BorderRadius.circular(20),
|
||||||
left: 12,
|
),
|
||||||
right: 8,
|
child: Row(
|
||||||
),
|
children: [
|
||||||
child: FaIcon(
|
const Padding(
|
||||||
FontAwesomeIcons.microphone,
|
padding: EdgeInsets.only(
|
||||||
size: 20,
|
top: 14,
|
||||||
color: Colors.red,
|
bottom: 14,
|
||||||
),
|
left: 12,
|
||||||
),
|
right: 8,
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
Text(
|
child: FaIcon(
|
||||||
formatMsToMinSec(
|
FontAwesomeIcons.microphone,
|
||||||
_currentDuration,
|
size: 20,
|
||||||
),
|
color: Colors.red,
|
||||||
style: TextStyle(
|
),
|
||||||
color: isDarkMode(context)
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_audioRecordingLock) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: (100 - _cancelSlideOffset) % 101,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
context.lang.voiceMessageSlideToCancel,
|
formatMsToMinSec(
|
||||||
|
_currentDuration,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
if (!_audioRecordingLock) ...[
|
||||||
Expanded(
|
SizedBox(
|
||||||
child: Container(),
|
width: (100 - _cancelSlideOffset) % 101,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
Text(
|
||||||
onTap: _cancelAudioRecording,
|
context.lang.voiceMessageSlideToCancel,
|
||||||
child: Text(
|
),
|
||||||
context.lang.voiceMessageCancel,
|
] else ...[
|
||||||
style: const TextStyle(
|
Expanded(
|
||||||
color: Colors.red,
|
child: Container(),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _cancelAudioRecording,
|
||||||
|
child: Text(
|
||||||
|
context.lang.voiceMessageCancel,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 20),
|
||||||
const SizedBox(width: 20),
|
],
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_textFieldController.text == '')
|
|
||||||
IconButton(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return CameraSendToView(widget.group);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_textFieldController.text == '')
|
|
||||||
GestureDetector(
|
|
||||||
onLongPressMoveUpdate: (details) {
|
|
||||||
if (_audioRecordingLock) return;
|
|
||||||
if (_recordingOffset.dy -
|
|
||||||
details.localPosition.dy >=
|
|
||||||
100) {
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
setState(() {
|
|
||||||
_audioRecordingLock = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (_recordingOffset.dx -
|
|
||||||
details.localPosition.dx >=
|
|
||||||
90 &&
|
|
||||||
_recordingState == RecordingState.recording) {
|
|
||||||
_recordingState = RecordingState.none;
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
_cancelAudioRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
final a =
|
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
if (a > 0 && a <= 90) {
|
|
||||||
_cancelSlideOffset =
|
|
||||||
_recordingOffset.dx -
|
|
||||||
details.localPosition.dx;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onLongPressStart: (a) {
|
|
||||||
_recordingOffset = a.localPosition;
|
|
||||||
_startAudioRecording();
|
|
||||||
},
|
|
||||||
onLongPressCancel: _cancelAudioRecording,
|
|
||||||
onLongPressEnd: (a) {
|
|
||||||
if (_recordingState != RecordingState.recording) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_audioRecordingLock) {
|
|
||||||
_stopAudioRecording();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
if (_recordingState == RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
|
||||||
top: -120,
|
|
||||||
left: -5,
|
|
||||||
child: Align(
|
|
||||||
alignment: AlignmentGeometry.topCenter,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.only(top: 13),
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(90),
|
|
||||||
color: isDarkMode(context)
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.lock,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
FaIcon(
|
|
||||||
FontAwesomeIcons.angleUp,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_recordingState == RecordingState.recording &&
|
|
||||||
!_audioRecordingLock)
|
|
||||||
Positioned.fill(
|
|
||||||
top: -20,
|
|
||||||
left: -25,
|
|
||||||
bottom: -20,
|
|
||||||
right: -20,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red,
|
|
||||||
borderRadius: BorderRadius.circular(90),
|
|
||||||
),
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_audioRecordingLock)
|
|
||||||
ColoredBox(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: 8,
|
|
||||||
bottom: 8,
|
|
||||||
left: 8,
|
|
||||||
right: 12,
|
|
||||||
),
|
|
||||||
child: FaIcon(
|
|
||||||
size: 20,
|
|
||||||
color:
|
|
||||||
(_recordingState ==
|
|
||||||
RecordingState.recording)
|
|
||||||
? Colors.white
|
|
||||||
: null,
|
|
||||||
(_recordingState == RecordingState.none)
|
|
||||||
? FontAwesomeIcons.microphone
|
|
||||||
: (_recordingState ==
|
|
||||||
RecordingState.recording)
|
|
||||||
? FontAwesomeIcons.stop
|
|
||||||
: FontAwesomeIcons.play,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (_textFieldController.text == '')
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return CameraSendToView(widget.group);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_textFieldController.text == '')
|
||||||
|
GestureDetector(
|
||||||
|
onLongPressMoveUpdate: (details) {
|
||||||
|
if (_audioRecordingLock) return;
|
||||||
|
if (_recordingOffset.dy - details.localPosition.dy >= 100) {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
setState(() {
|
||||||
|
_audioRecordingLock = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_recordingOffset.dx - details.localPosition.dx >= 90 &&
|
||||||
|
_recordingState == RecordingState.recording) {
|
||||||
|
_recordingState = RecordingState.none;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
|
_cancelAudioRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
final a = _recordingOffset.dx - details.localPosition.dx;
|
||||||
|
if (a > 0 && a <= 90) {
|
||||||
|
_cancelSlideOffset = _recordingOffset.dx - details.localPosition.dx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLongPressStart: (a) {
|
||||||
|
_recordingOffset = a.localPosition;
|
||||||
|
_startAudioRecording();
|
||||||
|
},
|
||||||
|
onLongPressCancel: _cancelAudioRecording,
|
||||||
|
onLongPressEnd: (a) {
|
||||||
|
if (_recordingState != RecordingState.recording) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_audioRecordingLock) {
|
||||||
|
_stopAudioRecording();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
if (_recordingState == RecordingState.recording && !_audioRecordingLock)
|
||||||
|
Positioned.fill(
|
||||||
|
top: -120,
|
||||||
|
left: -5,
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentGeometry.topCenter,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(top: 13),
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(90),
|
||||||
|
color: isDarkMode(context) ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.lock,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.angleUp,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_recordingState == RecordingState.recording && !_audioRecordingLock)
|
||||||
|
Positioned.fill(
|
||||||
|
top: -20,
|
||||||
|
left: -25,
|
||||||
|
bottom: -20,
|
||||||
|
right: -20,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red,
|
||||||
|
borderRadius: BorderRadius.circular(90),
|
||||||
|
),
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_audioRecordingLock)
|
||||||
|
ColoredBox(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
bottom: 8,
|
||||||
|
left: 8,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
size: 20,
|
||||||
|
color: (_recordingState == RecordingState.recording) ? Colors.white : null,
|
||||||
|
(_recordingState == RecordingState.none)
|
||||||
|
? FontAwesomeIcons.microphone
|
||||||
|
: (_recordingState == RecordingState.recording)
|
||||||
|
? FontAwesomeIcons.stop
|
||||||
|
: FontAwesomeIcons.play,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_textFieldController.text != '' || _audioRecordingLock)
|
||||||
if (_textFieldController.text != '' || _audioRecordingLock)
|
IconButton(
|
||||||
IconButton(
|
padding: const EdgeInsets.all(15),
|
||||||
padding: const EdgeInsets.all(15),
|
icon: FaIcon(
|
||||||
icon: FaIcon(
|
color: context.color.primary,
|
||||||
color: context.color.primary,
|
FontAwesomeIcons.solidPaperPlane,
|
||||||
FontAwesomeIcons.solidPaperPlane,
|
),
|
||||||
|
onPressed: _audioRecordingLock ? _stopAudioRecording : _sendMessage,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.plus),
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
onPressed: () => _showAdditionalShareModal(context),
|
||||||
),
|
),
|
||||||
onPressed: _audioRecordingLock
|
],
|
||||||
? _stopAudioRecording
|
),
|
||||||
: _sendMessage,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton(
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.plus),
|
|
||||||
padding: const EdgeInsets.all(15),
|
|
||||||
onPressed: () => _showAdditionalShareModal(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
||||||
|
|
@ -39,10 +40,8 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final messageBox =
|
final messageBox = _message.currentContext?.findRenderObject() as RenderBox?;
|
||||||
_message.currentContext?.findRenderObject() as RenderBox?;
|
final previewBox = _preview.currentContext?.findRenderObject() as RenderBox?;
|
||||||
final previewBox =
|
|
||||||
_preview.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (messageBox == null || previewBox == null) {
|
if (messageBox == null || previewBox == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +64,7 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
||||||
return widget.child!;
|
return widget.child!;
|
||||||
}
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: widget.scrollToMessage == null
|
onTap: widget.scrollToMessage == null ? null : () => widget.scrollToMessage!(widget.msg.quotesMessageId!),
|
||||||
? null
|
|
||||||
: () => widget.scrollToMessage!(widget.msg.quotesMessageId!),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
|
@ -143,16 +140,12 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_message ??= await twonlyDB.messagesDao
|
_message ??= await twonlyDB.messagesDao.getMessageById(widget.messageId!).getSingleOrNull();
|
||||||
.getMessageById(widget.messageId!)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (_message?.mediaId != null) {
|
if (_message?.mediaId != null) {
|
||||||
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
||||||
}
|
}
|
||||||
if (_message?.senderId != null) {
|
if (_message?.senderId != null) {
|
||||||
final contact = await twonlyDB.contactsDao
|
final contact = await twonlyDB.contactsDao.getContactByUserId(_message!.senderId!).getSingleOrNull();
|
||||||
.getContactByUserId(_message!.senderId!)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_username = getContactDisplayName(contact);
|
_username = getContactDisplayName(contact);
|
||||||
}
|
}
|
||||||
|
|
@ -186,6 +179,28 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
subtitle = 'Audio';
|
subtitle = 'Audio';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_message!.type == MessageType.contacts.name) {
|
||||||
|
subtitle = context.lang.contacts;
|
||||||
|
}
|
||||||
|
if (_message!.type == MessageType.restoreFlameCounter.name) {
|
||||||
|
if (_message!.additionalMessageData != null) {
|
||||||
|
try {
|
||||||
|
final data = AdditionalMessageData.fromBuffer(
|
||||||
|
_message!.additionalMessageData!,
|
||||||
|
);
|
||||||
|
subtitle = context.lang.chatEntryFlameRestored(
|
||||||
|
data.restoredFlameCounter.toInt(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
subtitle = context.lang.replyFlameRestored;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subtitle = context.lang.replyFlameRestored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_message!.type == MessageType.askAboutUser.name) {
|
||||||
|
subtitle = context.lang.replyAskAFriend;
|
||||||
|
}
|
||||||
|
|
||||||
if (_message!.senderId == null) {
|
if (_message!.senderId == null) {
|
||||||
_username = context.lang.you;
|
_username = context.lang.you;
|
||||||
|
|
@ -248,8 +263,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_mediaService != null &&
|
if (_mediaService != null && _mediaService!.mediaFile.type != MediaType.audio)
|
||||||
_mediaService!.mediaFile.type != MediaType.audio)
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: widget.showBorder ? 100 : 210,
|
height: widget.showBorder ? 100 : 210,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
|
|
||||||
|
class UnverifiedContactWarningComp extends StatelessWidget {
|
||||||
|
const UnverifiedContactWarningComp({
|
||||||
|
required this.group,
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Group group;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<void>(
|
||||||
|
stream: userService.onUserUpdated,
|
||||||
|
builder: (context, _) {
|
||||||
|
if (!userService.currentUser.securityProfile.showWarningForNonVerifiedContacts) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return StreamBuilder<VerificationStatus>(
|
||||||
|
stream: twonlyDB.keyVerificationDao.watchAllGroupMembersVerified(group.groupId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final status = snapshot.data;
|
||||||
|
if (status == null || status == VerificationStatus.trusted) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(top: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.errorContainer.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(color: context.color.error.withValues(alpha: 0.5)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
VerificationBadgeComp(
|
||||||
|
group: group,
|
||||||
|
size: 24,
|
||||||
|
clickable: false,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
group.isDirectChat
|
||||||
|
? context.lang.unverifiedWarningDirectTitle
|
||||||
|
: context.lang.unverifiedWarningGroupTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onErrorContainer,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onErrorContainer,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
children: formattedText(
|
||||||
|
context,
|
||||||
|
context.lang.unverifiedWarningBody,
|
||||||
|
textColor: context.color.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: FilledButton.tonal(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: context.color.onErrorContainer,
|
||||||
|
foregroundColor: context.color.errorContainer,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
textStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (group.isDirectChat) {
|
||||||
|
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||||
|
} else {
|
||||||
|
await context.push(Routes.profileGroup(group.groupId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(context.lang.unverifiedWarningButton),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart' hide Column;
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
|
@ -6,14 +7,18 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/api/utils.api.dart';
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
|
||||||
|
|
||||||
|
|
@ -57,24 +62,21 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
|
||||||
.watchNewAnnouncedUsersWithRelations()
|
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) {
|
||||||
.listen((update) {
|
if (mounted) {
|
||||||
if (mounted) {
|
setState(() {
|
||||||
setState(() {
|
_newAnnouncedUsers = update;
|
||||||
_newAnnouncedUsers = update;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao
|
}
|
||||||
.watchAllAnnouncedUsersWithRelations()
|
});
|
||||||
.listen((update) {
|
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_allAnnouncedUsers = update;
|
_allAnnouncedUsers = update;
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (widget.username != null) {
|
if (widget.username != null) {
|
||||||
_usernameController.text = widget.username!;
|
_usernameController.text = widget.username!;
|
||||||
|
|
@ -85,6 +87,60 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
twonlyDB.userDiscoveryDao.markAllValidAnnouncedUsersAsShown();
|
twonlyDB.userDiscoveryDao.markAllValidAnnouncedUsersAsShown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _shareProfile() async {
|
||||||
|
final pubKey = await getUserPublicKey();
|
||||||
|
final params = ShareParams(
|
||||||
|
text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
|
||||||
|
);
|
||||||
|
await SharePlus.instance.share(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMyQrCode() {
|
||||||
|
// ignore: inference_failure_on_function_invocation
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: double.infinity),
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.onSurface.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const ProfileQrCodeComp(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
context.lang.addContactQrSheetSubtext,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: context.color.onSurface.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contactsStream.cancel();
|
_contactsStream.cancel();
|
||||||
|
|
@ -134,9 +190,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.publicKey != null &&
|
if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
||||||
mounted &&
|
|
||||||
widget.publicKey!.equals(userdata.publicIdentityKey)) {
|
|
||||||
final markAsVerified = await showAlertDialog(
|
final markAsVerified = await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
context.lang.linkFromUsername(username),
|
context.lang.linkFromUsername(username),
|
||||||
|
|
@ -154,21 +208,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDecoration _getInputDecoration(String hintText) {
|
|
||||||
return InputDecoration(
|
|
||||||
hintText: hintText,
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(9),
|
|
||||||
borderSide: BorderSide(color: context.color.primary),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
borderSide: BorderSide(color: context.color.outline),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -176,77 +215,157 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
title: Text(context.lang.addFriendTitle),
|
title: Text(context.lang.addFriendTitle),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: ListView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
|
children: [
|
||||||
child: Column(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
Padding(
|
child: SearchBar(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
controller: _usernameController,
|
||||||
child: Row(
|
hintText: context.lang.searchUsernameInput,
|
||||||
children: [
|
elevation: const WidgetStatePropertyAll(0),
|
||||||
Expanded(
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
child: TextField(
|
context.color.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
onSubmitted: _requestNewUserByUsername,
|
),
|
||||||
onChanged: (value) {
|
shape: WidgetStatePropertyAll(
|
||||||
_usernameController.text = value.toLowerCase();
|
RoundedRectangleBorder(
|
||||||
_usernameController.selection =
|
borderRadius: BorderRadius.circular(12),
|
||||||
TextSelection.fromPosition(
|
),
|
||||||
TextPosition(
|
),
|
||||||
offset: _usernameController.text.length,
|
padding: const WidgetStatePropertyAll(
|
||||||
),
|
EdgeInsets.symmetric(horizontal: 8),
|
||||||
);
|
),
|
||||||
setState(() {});
|
leading: const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||||
},
|
trailing: [
|
||||||
inputFormatters: [
|
if (_usernameController.text.isNotEmpty) ...[
|
||||||
LengthLimitingTextInputFormatter(12),
|
IconButton(
|
||||||
FilteringTextInputFormatter.allow(
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
RegExp('[a-z0-9A-Z._]'),
|
onPressed: () {
|
||||||
),
|
_usernameController.clear();
|
||||||
],
|
setState(() {});
|
||||||
controller: _usernameController,
|
},
|
||||||
decoration: _getInputDecoration(
|
),
|
||||||
context.lang.searchUsernameInput,
|
if (_isLoading)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: FaIcon(
|
||||||
|
FontAwesomeIcons.magnifyingGlassPlus,
|
||||||
|
size: 20,
|
||||||
|
color: context.color.primary,
|
||||||
|
),
|
||||||
|
onPressed: () => _requestNewUserByUsername(
|
||||||
|
_usernameController.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
],
|
IconButton(
|
||||||
),
|
icon: FaIcon(
|
||||||
),
|
FontAwesomeIcons.camera,
|
||||||
const SizedBox(
|
size: 20,
|
||||||
height: 20,
|
color: context.color.primary,
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
context.push(Routes.settingsPublicProfile),
|
|
||||||
icon: const FaIcon(FontAwesomeIcons.qrcode),
|
|
||||||
label: Text(context.lang.scanQrOrShow),
|
|
||||||
),
|
),
|
||||||
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
|
tooltip: context.lang.scanOtherProfile,
|
||||||
),
|
),
|
||||||
OpenRequestsListComp(
|
|
||||||
contacts: _openRequestsContacts,
|
|
||||||
relations: _allAnnouncedUsers,
|
|
||||||
),
|
|
||||||
FriendSuggestionsComp(_newAnnouncedUsers),
|
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
|
onSubmitted: _requestNewUserByUsername,
|
||||||
|
onChanged: (value) {
|
||||||
|
_usernameController.text = value.toLowerCase();
|
||||||
|
_usernameController.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: _usernameController.text.length),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(
|
||||||
),
|
height: 10,
|
||||||
),
|
),
|
||||||
floatingActionButton: Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 30),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: FloatingActionButton(
|
child: Column(
|
||||||
onPressed: _isLoading || _usernameController.text.isEmpty
|
mainAxisSize: MainAxisSize.min,
|
||||||
? null
|
children: [
|
||||||
: () => _requestNewUserByUsername(_usernameController.text),
|
Row(
|
||||||
child: _isLoading
|
children: [
|
||||||
? const Center(child: CircularProgressIndicator())
|
Expanded(
|
||||||
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
|
child: FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _shareProfile,
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.shareNodes,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
context.lang.shareYourProfile,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: context.color.secondaryContainer,
|
||||||
|
foregroundColor: context.color.onSecondaryContainer,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 10,
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _showMyQrCode,
|
||||||
|
icon: const FaIcon(
|
||||||
|
FontAwesomeIcons.qrcode,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
context.lang.openYourOwnQRcode,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
OpenRequestsListComp(
|
||||||
|
contacts: _openRequestsContacts,
|
||||||
|
relations: _allAnnouncedUsers,
|
||||||
|
),
|
||||||
|
FriendSuggestionsComp(_newAnnouncedUsers),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/api/utils.api.dart';
|
import 'package:twonly/src/services/api/utils.api.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -82,6 +83,87 @@ class FriendSuggestionsComp extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _askFriends(
|
||||||
|
BuildContext context,
|
||||||
|
UserDiscoveryAnnouncedUser user,
|
||||||
|
List<(Contact, DateTime?)> friends,
|
||||||
|
) async {
|
||||||
|
Log.info('Asking friends about user: ${user.announcedUserId}');
|
||||||
|
final selectedFriends = <int>{};
|
||||||
|
final username = user.username ?? '';
|
||||||
|
|
||||||
|
final result = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(context.lang.askFriendsDialogTitle(username)),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(context.lang.askFriendsDialogDescription),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: friends.map((f) {
|
||||||
|
final contact = f.$1;
|
||||||
|
final isSelected =
|
||||||
|
selectedFriends.contains(contact.userId);
|
||||||
|
return CheckboxListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(contact.displayName ?? contact.username),
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
if (val == true) {
|
||||||
|
selectedFriends.add(contact.userId);
|
||||||
|
} else {
|
||||||
|
selectedFriends.remove(contact.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: Text(context.lang.askFriendsDialogCancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: selectedFriends.isEmpty
|
||||||
|
? null
|
||||||
|
: () => Navigator.pop(context, true),
|
||||||
|
child: Text(context.lang.askFriendsDialogConfirm),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true && selectedFriends.isNotEmpty) {
|
||||||
|
for (final contactId in selectedFriends) {
|
||||||
|
await insertAndSendAskAboutUserMessage(contactId, user.announcedUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await twonlyDB.userDiscoveryDao.updateAnnouncedUser(
|
||||||
|
user.announcedUserId,
|
||||||
|
const UserDiscoveryAnnouncedUsersCompanion(
|
||||||
|
wasAskedFriends: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (announcedUsers.isEmpty) return Container();
|
if (announcedUsers.isEmpty) return Container();
|
||||||
|
|
@ -99,71 +181,146 @@ class FriendSuggestionsComp extends StatelessWidget {
|
||||||
|
|
||||||
final friendsList = buildFriendsListText(context, friends);
|
final friendsList = buildFriendsListText(context, friends);
|
||||||
|
|
||||||
return ListTile(
|
return Padding(
|
||||||
key: ValueKey(user.announcedUserId),
|
key: ValueKey(user.announcedUserId),
|
||||||
contentPadding: EdgeInsets.zero,
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
title: Text(substringBy(user.username!, 25)),
|
child: Row(
|
||||||
subtitle: StreamBuilder(
|
|
||||||
stream: twonlyDB.groupsDao.watchNonDirectGroupsForMember(
|
|
||||||
user.announcedUserId,
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
var text = friendsList;
|
|
||||||
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
|
||||||
text += formattedText(
|
|
||||||
context,
|
|
||||||
context.lang.friendSuggestionsGroupMemberIn(
|
|
||||||
joinWithAnd(
|
|
||||||
snapshot.data!.map((g) => '*${g.groupName}*').toList(),
|
|
||||||
context.lang.andWord,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: text,
|
|
||||||
style: const TextStyle(fontSize: 11),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
leading: const AvatarIcon(
|
|
||||||
fontSize: 17,
|
|
||||||
),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const AvatarIcon(
|
||||||
height: 26,
|
fontSize: 17,
|
||||||
child: FilledButton(
|
),
|
||||||
style: FilledButton.styleFrom(
|
const SizedBox(width: 12),
|
||||||
padding: const EdgeInsets.only(right: 8, left: 4),
|
Expanded(
|
||||||
).merge(secondaryGreyButtonStyle(context)),
|
child: Column(
|
||||||
child: Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
const Padding(
|
children: [
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
Text(
|
||||||
child: FaIcon(FontAwesomeIcons.userPlus, size: 12),
|
substringBy(user.username!, 25),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
context.lang.friendSuggestionsRequest,
|
const SizedBox(height: 4),
|
||||||
style: const TextStyle(fontSize: 10),
|
StreamBuilder<List<Group>>(
|
||||||
|
stream: twonlyDB.groupsDao
|
||||||
|
.watchNonDirectGroupsForMember(
|
||||||
|
user.announcedUserId,
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
var text = friendsList;
|
||||||
|
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
|
||||||
|
text += formattedText(
|
||||||
|
context,
|
||||||
|
context.lang.friendSuggestionsGroupMemberIn(
|
||||||
|
joinWithAnd(
|
||||||
|
snapshot.data!
|
||||||
|
.map((g) => '*${g.groupName}*')
|
||||||
|
.toList(),
|
||||||
|
context.lang.andWord,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (!user.wasAskedFriends) ...[
|
||||||
|
SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 8,
|
||||||
|
left: 4,
|
||||||
|
),
|
||||||
|
).merge(secondaryGreyButtonStyle(context)),
|
||||||
|
onPressed: () => _askFriends(context, user, friends),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.circleQuestion,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.lang.friendSuggestionsAskFriend,
|
||||||
|
style: const TextStyle(fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
],
|
||||||
|
SizedBox(
|
||||||
|
height: 26,
|
||||||
|
child: FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 8,
|
||||||
|
left: 4,
|
||||||
|
),
|
||||||
|
).merge(secondaryGreyButtonStyle(context)),
|
||||||
|
onPressed: () =>
|
||||||
|
_requestAnnouncedUser(context, user),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
),
|
||||||
|
child: FaIcon(
|
||||||
|
FontAwesomeIcons.userPlus,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
context.lang.friendSuggestionsRequest,
|
||||||
|
style: const TextStyle(fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () => _requestAnnouncedUser(context, user),
|
IconButton(
|
||||||
),
|
style: IconButton.styleFrom(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
IconButton(
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
style: IconButton.styleFrom(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
constraints: const BoxConstraints(),
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
icon: const Icon(Icons.close, size: 18),
|
||||||
),
|
onPressed: () => _hideAnnouncedUser(user.announcedUserId),
|
||||||
constraints: const BoxConstraints(),
|
),
|
||||||
icon: const Icon(Icons.close, size: 18),
|
],
|
||||||
onPressed: () => _hideAnnouncedUser(user.announcedUserId),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/headline.element.dart';
|
import 'package:twonly/src/visual/elements/headline.element.dart';
|
||||||
|
|
@ -63,8 +64,17 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
const update = ContactsCompanion(blocked: Value(true));
|
final block = await showAlertDialog(
|
||||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
context,
|
||||||
|
context.lang.contactBlockTitle(getContactDisplayName(contact)),
|
||||||
|
context.lang.contactBlockBody,
|
||||||
|
);
|
||||||
|
if (block) {
|
||||||
|
const update = ContactsCompanion(blocked: Value(true));
|
||||||
|
if (context.mounted) {
|
||||||
|
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,9 +189,7 @@ class OpenRequestsListComp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: contact.requested
|
children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact),
|
||||||
? requestedActions(context, contact)
|
|
||||||
: sendRequestActions(context, contact),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,10 @@ import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
@ -19,9 +16,11 @@ import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
|
import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/contact/contact_components/user_discovery_contact_settings.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
|
||||||
|
|
||||||
class ContactView extends StatefulWidget {
|
class ContactView extends StatefulWidget {
|
||||||
const ContactView(this.userId, {super.key});
|
const ContactView(this.userId, {super.key});
|
||||||
|
|
@ -35,13 +34,9 @@ class ContactView extends StatefulWidget {
|
||||||
class _ContactViewState extends State<ContactView> {
|
class _ContactViewState extends State<ContactView> {
|
||||||
Contact? _contact;
|
Contact? _contact;
|
||||||
List<GroupMember> _memberOfGroups = [];
|
List<GroupMember> _memberOfGroups = [];
|
||||||
List<KeyVerification> _keyVerifications = [];
|
|
||||||
List<(Contact, DateTime)> _transferredTrust = [];
|
|
||||||
|
|
||||||
late StreamSubscription<Contact?> _streamContact;
|
late StreamSubscription<Contact?> _streamContact;
|
||||||
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
|
||||||
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -63,30 +58,12 @@ class _ContactViewState extends State<ContactView> {
|
||||||
_memberOfGroups = groups;
|
_memberOfGroups = groups;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
|
||||||
.watchContactVerification(widget.userId)
|
|
||||||
.listen((update) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_keyVerifications = update;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
|
||||||
.watchTransferredTrustVerifications(widget.userId)
|
|
||||||
.listen((update) {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_transferredTrust = update;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamContact.cancel();
|
_streamContact.cancel();
|
||||||
_streamMemberOfGroups.cancel();
|
_streamMemberOfGroups.cancel();
|
||||||
_streamKeyVerifications.cancel();
|
|
||||||
_streamTransferredTrust.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,14 +93,6 @@ class _ContactViewState extends State<ContactView> {
|
||||||
);
|
);
|
||||||
if (remove) {
|
if (remove) {
|
||||||
await twonlyDB.contactsDao.deleteContactByUserId(contact.userId);
|
await twonlyDB.contactsDao.deleteContactByUserId(contact.userId);
|
||||||
// await twonlyDB.contactsDao.updateContact(
|
|
||||||
// contact.userId,
|
|
||||||
// const ContactsCompanion(
|
|
||||||
// accepted: Value(false),
|
|
||||||
// requested: Value(false),
|
|
||||||
// deletedByUser: Value(true),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
Navigator.popUntil(context, (route) => route.isFirst);
|
||||||
}
|
}
|
||||||
|
|
@ -260,131 +229,16 @@ class _ContactViewState extends State<ContactView> {
|
||||||
RestoreFlameComp(
|
RestoreFlameComp(
|
||||||
contactId: widget.userId,
|
contactId: widget.userId,
|
||||||
),
|
),
|
||||||
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty)
|
VerificationExpansionTileComp(
|
||||||
BetterListTile(
|
contact: contact,
|
||||||
leading: VerificationBadgeComp(
|
),
|
||||||
contact: contact,
|
MutualGroupsExpansionTileComp(
|
||||||
size: 20,
|
contact: contact,
|
||||||
),
|
),
|
||||||
text: context.lang.contactVerifyNumberTitle,
|
UserDiscoveryContactSettingsComp(
|
||||||
onTap: () async {
|
contact: contact,
|
||||||
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
),
|
||||||
setState(() {});
|
const Divider(),
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty)
|
|
||||||
ExpansionTile(
|
|
||||||
shape: const RoundedRectangleBorder(),
|
|
||||||
backgroundColor: context.color.surfaceContainer,
|
|
||||||
collapsedShape: const RoundedRectangleBorder(),
|
|
||||||
leading: Padding(
|
|
||||||
padding: const EdgeInsetsGeometry.only(left: 12, right: 12),
|
|
||||||
child: VerificationBadgeComp(
|
|
||||||
contact: contact,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(context.lang.userVerifiedTitle),
|
|
||||||
children: [
|
|
||||||
..._keyVerifications.map(
|
|
||||||
(kv) => ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
|
||||||
trailing: Text(
|
|
||||||
DateFormat.yMd(
|
|
||||||
Localizations.localeOf(context).toString(),
|
|
||||||
).format(kv.createdAt),
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
..._transferredTrust.map(
|
|
||||||
(tt) => ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.lang.contactVerifiedBy(
|
|
||||||
getContactDisplayName(tt.$1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VerificationBadgeComp(
|
|
||||||
contact: tt.$1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
DateFormat.yMd(
|
|
||||||
Localizations.localeOf(context).toString(),
|
|
||||||
).format(tt.$2),
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.color.onSurfaceVariant,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled)
|
|
||||||
if (userService.currentUser.userDiscoveryRequiresManualApproval &&
|
|
||||||
contact.userDiscoveryManualApproved != true)
|
|
||||||
BetterListTile(
|
|
||||||
icon: FontAwesomeIcons.usersViewfinder,
|
|
||||||
text: context.lang.userDiscoverySettingsTitle,
|
|
||||||
subtitle: Text(
|
|
||||||
context.lang.contactUserDiscoveryManualApprovalPending,
|
|
||||||
style: const TextStyle(fontSize: 10),
|
|
||||||
),
|
|
||||||
trailing: TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
contact.userId,
|
|
||||||
const ContactsCompanion(
|
|
||||||
userDiscoveryManualApproved: Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
context.lang.contactUserDiscoveryManualApprovalApprove,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
BetterListTile(
|
|
||||||
icon: FontAwesomeIcons.usersViewfinder,
|
|
||||||
text: context.lang.userDiscoverySettingsTitle,
|
|
||||||
onTap: () => context.navPush(const UserDiscoverySettingsView()),
|
|
||||||
subtitle:
|
|
||||||
!contact.userDiscoveryExcluded &&
|
|
||||||
contact.mediaSendCounter <
|
|
||||||
userService.currentUser.requiredSendImages
|
|
||||||
? Text(
|
|
||||||
context.lang.contactUserDiscoveryImagesLeft(
|
|
||||||
userService.currentUser.requiredSendImages -
|
|
||||||
contact.mediaSendCounter,
|
|
||||||
getContactDisplayName(contact),
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 9),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
trailing: Transform.scale(
|
|
||||||
scale: 0.8,
|
|
||||||
child: Switch(
|
|
||||||
value: !contact.userDiscoveryExcluded,
|
|
||||||
onChanged: (a) async {
|
|
||||||
await UserDiscoveryService.changeExclusionForContact(
|
|
||||||
contact.userId,
|
|
||||||
!a,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.flag,
|
icon: FontAwesomeIcons.flag,
|
||||||
text: context.lang.reportUser,
|
text: context.lang.reportUser,
|
||||||
|
|
@ -408,19 +262,6 @@ class _ContactViewState extends State<ContactView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
|
||||||
return switch (type) {
|
|
||||||
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
|
||||||
VerificationType.secretQrToken =>
|
|
||||||
context.lang.verificationTypeSecretQrToken,
|
|
||||||
VerificationType.link => context.lang.verificationTypeLink,
|
|
||||||
VerificationType.contactSharedByVerified =>
|
|
||||||
context.lang.verificationTypeContactSharedByVerified,
|
|
||||||
VerificationType.migratedFromOldVersion =>
|
|
||||||
context.lang.verificationTypeMigratedFromOldVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> showNicknameChangeDialog(
|
Future<String?> showNicknameChangeDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Contact contact,
|
Contact contact,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
||||||
|
class MutualGroupsExpansionTileComp extends StatefulWidget {
|
||||||
|
const MutualGroupsExpansionTileComp({
|
||||||
|
required this.contact,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MutualGroupsExpansionTileComp> createState() =>
|
||||||
|
_MutualGroupsExpansionTileCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MutualGroupsExpansionTileCompState
|
||||||
|
extends State<MutualGroupsExpansionTileComp> {
|
||||||
|
List<Group> _groups = [];
|
||||||
|
late StreamSubscription<List<Group>> _streamGroups;
|
||||||
|
bool? _hasInitializedExpanded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_streamGroups = twonlyDB.groupsDao
|
||||||
|
.watchNonDirectGroupsForMember(widget.contact.userId)
|
||||||
|
.listen((groupsList) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_groups = groupsList;
|
||||||
|
_groups.sort((a, b) {
|
||||||
|
return b.totalMediaCounter.compareTo(a.totalMediaCounter);
|
||||||
|
});
|
||||||
|
_hasInitializedExpanded ??= true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_streamGroups.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_hasInitializedExpanded == null || _groups.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpansionTile(
|
||||||
|
key: PageStorageKey<String>('mutual_groups_${widget.contact.userId}'),
|
||||||
|
shape: const RoundedRectangleBorder(),
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
collapsedShape: const RoundedRectangleBorder(),
|
||||||
|
initiallyExpanded: _groups.length < 5,
|
||||||
|
onExpansionChanged: (expanded) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: Icon(
|
||||||
|
FontAwesomeIcons.userGroup,
|
||||||
|
size: 16,
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
context.lang.mutualGroupsTitle(_groups.length),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: _groups.map((group) {
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
leading: AvatarIcon(
|
||||||
|
group: group,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
group.groupName,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurface,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.push(Routes.chatsMessages(group.groupId));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -11,8 +10,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb;
|
||||||
as pb;
|
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/subscription.service.dart';
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
|
|
@ -24,9 +22,11 @@ import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
class RestoreFlameComp extends StatefulWidget {
|
class RestoreFlameComp extends StatefulWidget {
|
||||||
const RestoreFlameComp({
|
const RestoreFlameComp({
|
||||||
required this.contactId,
|
required this.contactId,
|
||||||
|
this.flameOnRightSide = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final int contactId;
|
final int contactId;
|
||||||
|
final bool flameOnRightSide;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RestoreFlameComp> createState() => _RestoreFlameCompState();
|
State<RestoreFlameComp> createState() => _RestoreFlameCompState();
|
||||||
|
|
@ -60,21 +60,15 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
final currentPlan = planFromString(
|
final currentPlan = planFromString(
|
||||||
userService.currentUser.subscriptionPlan,
|
userService.currentUser.subscriptionPlan,
|
||||||
);
|
);
|
||||||
if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) &&
|
if (!isUserAllowed(currentPlan, PremiumFeatures.RestoreFlames) && kReleaseMode) {
|
||||||
kReleaseMode) {
|
|
||||||
await context.push(Routes.settingsSubscription);
|
await context.push(Routes.settingsSubscription);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
|
||||||
);
|
);
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
|
||||||
_groupId,
|
await restoreFlames(_groupId);
|
||||||
GroupsCompanion(
|
|
||||||
flameCounter: Value(_group!.maxFlameCounter),
|
|
||||||
lastFlameCounterChange: Value(clock.now()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final addData = AdditionalMessageData(
|
final addData = AdditionalMessageData(
|
||||||
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
|
type: AdditionalMessageData_Type.RESTORED_FLAME_COUNTER,
|
||||||
|
|
@ -116,6 +110,22 @@ class _RestoreFlameCompState extends State<RestoreFlameComp> {
|
||||||
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
|
if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
if (widget.flameOnRightSide) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
onTap: _restoreFlames,
|
||||||
|
title: Text(
|
||||||
|
'Restore your ${_group!.maxFlameCounter} lost flames',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
trailing: const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
child: EmojiAnimationComp(
|
||||||
|
emoji: '🔥',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return BetterListTile(
|
return BetterListTile(
|
||||||
onTap: _restoreFlames,
|
onTap: _restoreFlames,
|
||||||
leading: const SizedBox(
|
leading: const SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
|
||||||
|
|
||||||
|
class UserDiscoveryContactSettingsComp extends StatelessWidget {
|
||||||
|
const UserDiscoveryContactSettingsComp({
|
||||||
|
required this.contact,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!userService.currentUser.isUserDiscoveryEnabled) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userService.currentUser.userDiscoveryRequiresManualApproval &&
|
||||||
|
contact.userDiscoveryManualApproved != true) {
|
||||||
|
return BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.usersViewfinder,
|
||||||
|
text: context.lang.userDiscoverySettingsTitle,
|
||||||
|
subtitle: Text(
|
||||||
|
context.lang.contactUserDiscoveryManualApprovalPending,
|
||||||
|
style: const TextStyle(fontSize: 10),
|
||||||
|
),
|
||||||
|
trailing: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await twonlyDB.contactsDao.updateContact(
|
||||||
|
contact.userId,
|
||||||
|
const ContactsCompanion(
|
||||||
|
userDiscoveryManualApproved: Value(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.lang.contactUserDiscoveryManualApprovalApprove,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.usersViewfinder,
|
||||||
|
text: context.lang.userDiscoverySettingsTitle,
|
||||||
|
onTap: () => context.navPush(const UserDiscoverySettingsView()),
|
||||||
|
subtitle: !contact.userDiscoveryExcluded &&
|
||||||
|
contact.mediaSendCounter <
|
||||||
|
userService.currentUser.requiredSendImages
|
||||||
|
? Text(
|
||||||
|
context.lang.contactUserDiscoveryImagesLeft(
|
||||||
|
userService.currentUser.requiredSendImages -
|
||||||
|
contact.mediaSendCounter,
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 9),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: Transform.scale(
|
||||||
|
scale: 0.8,
|
||||||
|
child: Switch(
|
||||||
|
value: !contact.userDiscoveryExcluded,
|
||||||
|
onChanged: (a) async {
|
||||||
|
await UserDiscoveryService.changeExclusionForContact(
|
||||||
|
contact.userId,
|
||||||
|
!a,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
|
|
||||||
|
class VerificationExpansionTileComp extends StatefulWidget {
|
||||||
|
const VerificationExpansionTileComp({
|
||||||
|
required this.contact,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VerificationExpansionTileComp> createState() =>
|
||||||
|
_VerificationExpansionTileCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerificationExpansionTileCompState
|
||||||
|
extends State<VerificationExpansionTileComp> {
|
||||||
|
List<KeyVerification> _keyVerifications = [];
|
||||||
|
List<(Contact, DateTime)> _transferredTrust = [];
|
||||||
|
|
||||||
|
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||||
|
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
||||||
|
.watchContactVerification(widget.contact.userId)
|
||||||
|
.listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_keyVerifications = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||||
|
.watchTransferredTrustVerifications(widget.contact.userId)
|
||||||
|
.listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_transferredTrust = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_streamKeyVerifications.cancel();
|
||||||
|
_streamTransferredTrust.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _verificationTypeLabel(BuildContext context, VerificationType type) {
|
||||||
|
return switch (type) {
|
||||||
|
VerificationType.qrScanned => context.lang.verificationTypeQrScanned,
|
||||||
|
VerificationType.secretQrToken =>
|
||||||
|
context.lang.verificationTypeSecretQrToken(
|
||||||
|
getContactDisplayName(widget.contact),
|
||||||
|
),
|
||||||
|
VerificationType.link => context.lang.verificationTypeLink,
|
||||||
|
VerificationType.contactSharedByVerified =>
|
||||||
|
context.lang.verificationTypeContactSharedByVerified,
|
||||||
|
VerificationType.migratedFromOldVersion =>
|
||||||
|
context.lang.verificationTypeMigratedFromOldVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) {
|
||||||
|
return BetterListTile(
|
||||||
|
leading: VerificationBadgeComp(
|
||||||
|
contact: widget.contact,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
text: context.lang.contactVerifyNumberTitle,
|
||||||
|
onTap: () async {
|
||||||
|
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpansionTile(
|
||||||
|
shape: const RoundedRectangleBorder(),
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
collapsedShape: const RoundedRectangleBorder(),
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsetsGeometry.only(left: 12, right: 12),
|
||||||
|
child: VerificationBadgeComp(
|
||||||
|
contact: widget.contact,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(context.lang.userVerifiedTitle),
|
||||||
|
children: [
|
||||||
|
..._keyVerifications.map(
|
||||||
|
(kv) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 16),
|
||||||
|
title: Text(_verificationTypeLabel(context, kv.type)),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMd(
|
||||||
|
Localizations.localeOf(context).toString(),
|
||||||
|
).format(kv.createdAt),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
iconSize: 8,
|
||||||
|
icon: Icon(
|
||||||
|
FontAwesomeIcons.trash,
|
||||||
|
size: 8,
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirm = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.deleteVerificationTitle,
|
||||||
|
context.lang.deleteVerificationBody,
|
||||||
|
);
|
||||||
|
if (confirm) {
|
||||||
|
await twonlyDB.keyVerificationDao
|
||||||
|
.deleteKeyVerificationById(
|
||||||
|
kv.verificationId,
|
||||||
|
widget.contact.userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
..._transferredTrust.map(
|
||||||
|
(tt) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.contactVerifiedBy(
|
||||||
|
getContactDisplayName(tt.$1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VerificationBadgeComp(
|
||||||
|
contact: tt.$1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
DateFormat.yMd(
|
||||||
|
Localizations.localeOf(context).toString(),
|
||||||
|
).format(tt.$2),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ class _GroupCreateSelectGroupNameViewState
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.lang.selectGroupName),
|
title: Text(context.lang.selectGroupName),
|
||||||
),
|
),
|
||||||
|
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||||
floatingActionButton: FilledButton.icon(
|
floatingActionButton: FilledButton.icon(
|
||||||
onPressed: (textFieldGroupName.text.isEmpty || _isLoading)
|
onPressed: (textFieldGroupName.text.isEmpty || _isLoading)
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
: context.lang.addMember,
|
: context.lang.addMember,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
floatingActionButtonAnimator: FloatingActionButtonAnimator.noAnimation,
|
||||||
floatingActionButton: FilledButton.icon(
|
floatingActionButton: FilledButton.icon(
|
||||||
onPressed: selectedUsers.isEmpty ? null : submitChanges,
|
onPressed: selectedUsers.isEmpty ? null : submitChanges,
|
||||||
label: Text(
|
label: Text(
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
_selectNotificationSub = selectNotificationStream.stream.listen((
|
_selectNotificationSub = selectNotificationStream.stream.listen((
|
||||||
response,
|
response,
|
||||||
) async {
|
) async {
|
||||||
if (response.payload != null &&
|
if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) {
|
||||||
response.payload!.startsWith(Routes.chats) &&
|
|
||||||
response.payload! != Routes.chats) {
|
|
||||||
await routerProvider.push(response.payload!);
|
await routerProvider.push(response.payload!);
|
||||||
}
|
}
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
|
|
@ -116,40 +114,31 @@ class HomeViewState extends State<HomeView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (widget.initialPage == 1 &&
|
if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) {
|
||||||
!userService.currentUser.startWithCameraOpen ||
|
|
||||||
widget.initialPage == 0) {
|
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAsync() async {
|
Future<void> _initAsync() async {
|
||||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||||
.getNotificationAppLaunchDetails();
|
|
||||||
|
|
||||||
RemoteMessage? initialRemoteMessage;
|
RemoteMessage? initialRemoteMessage;
|
||||||
try {
|
try {
|
||||||
initialRemoteMessage = await FirebaseMessaging.instance
|
initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||||||
.getInitialMessage();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not get initial Firebase message: $e');
|
Log.error('Could not get initial Firebase message: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.initialPage == 0 ||
|
if (widget.initialPage == 0 ||
|
||||||
initialRemoteMessage != null ||
|
initialRemoteMessage != null ||
|
||||||
(notificationAppLaunchDetails != null &&
|
(notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||||
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
|
||||||
if (initialRemoteMessage != null) {
|
if (initialRemoteMessage != null) {
|
||||||
Log.info('App launched from iOS/Remote push notification tap.');
|
Log.info('App launched from iOS/Remote push notification tap.');
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ??
|
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
false) {
|
final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||||
final payload =
|
if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) {
|
||||||
notificationAppLaunchDetails?.notificationResponse?.payload;
|
|
||||||
if (payload != null &&
|
|
||||||
payload.startsWith(Routes.chats) &&
|
|
||||||
payload != Routes.chats) {
|
|
||||||
await routerProvider.push(payload);
|
await routerProvider.push(payload);
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +169,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
_homeViewPageIndexSub?.cancel();
|
_homeViewPageIndexSub?.cancel();
|
||||||
_selectNotificationSub?.cancel();
|
_selectNotificationSub?.cancel();
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
|
_mainCameraController.setState = null;
|
||||||
_mainCameraController.closeCamera();
|
_mainCameraController.closeCamera();
|
||||||
_intentStreamSub?.cancel();
|
_intentStreamSub?.cancel();
|
||||||
_deepLinkSub?.cancel();
|
_deepLinkSub?.cancel();
|
||||||
|
|
@ -190,25 +180,30 @@ class HomeViewState extends State<HomeView> {
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
|
|
||||||
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
||||||
if (_activePageIdx == 2 &&
|
final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent;
|
||||||
notification.metrics.pixels < 100 &&
|
if (!canScroll) {
|
||||||
!_isBottomNavVisible) {
|
if (!_isBottomNavVisible) {
|
||||||
setState(() {
|
|
||||||
_isBottomNavVisible = true;
|
|
||||||
});
|
|
||||||
} else if (notification is ScrollUpdateNotification) {
|
|
||||||
final delta = notification.scrollDelta ?? 0;
|
|
||||||
if (delta > 5 &&
|
|
||||||
_isBottomNavVisible &&
|
|
||||||
(_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
|
||||||
setState(() {
|
|
||||||
_isBottomNavVisible = false;
|
|
||||||
});
|
|
||||||
} else if (delta < -5 && !_isBottomNavVisible) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBottomNavVisible = true;
|
_isBottomNavVisible = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
} else if (notification is ScrollUpdateNotification) {
|
||||||
|
final delta = notification.scrollDelta ?? 0;
|
||||||
|
if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = false;
|
||||||
|
});
|
||||||
|
} else if (delta < -5 && !_isBottomNavVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isBottomNavVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,59 +238,59 @@ class HomeViewState extends State<HomeView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: GestureDetector(
|
body: Stack(
|
||||||
onDoubleTap: _offsetRatio == 0
|
children: <Widget>[
|
||||||
? _mainCameraController.onDoubleTap
|
MainCameraPreview(mainCameraController: _mainCameraController),
|
||||||
: null,
|
Positioned.fill(
|
||||||
onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null,
|
child: Opacity(
|
||||||
child: Stack(
|
opacity: _offsetRatio,
|
||||||
children: <Widget>[
|
child: Container(
|
||||||
MainCameraPreview(mainCameraController: _mainCameraController),
|
color: context.color.surface,
|
||||||
Positioned.fill(
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _offsetRatio,
|
|
||||||
child: Container(
|
|
||||||
color: context.color.surface,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
NotificationListener<ScrollNotification>(
|
),
|
||||||
onNotification: _onPageView,
|
NotificationListener<ScrollNotification>(
|
||||||
child: Positioned.fill(
|
onNotification: _onPageView,
|
||||||
child: PageView(
|
child: Positioned.fill(
|
||||||
controller: _homeViewPageController,
|
child: PageView(
|
||||||
onPageChanged: (index) {
|
controller: _homeViewPageController,
|
||||||
setState(() {
|
onPageChanged: (index) {
|
||||||
_activePageIdx = index;
|
setState(() {
|
||||||
});
|
_activePageIdx = index;
|
||||||
},
|
});
|
||||||
children: [
|
},
|
||||||
const ChatListView(),
|
children: [
|
||||||
Container(),
|
const ChatListView(),
|
||||||
const MemoriesView(),
|
Container(),
|
||||||
],
|
const MemoriesView(),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
left: 0,
|
Positioned.fill(
|
||||||
top: 0,
|
child: _offsetRatio == 0
|
||||||
right: 0,
|
? GestureDetector(
|
||||||
bottom: (_offsetRatio > 0.25)
|
behavior: HitTestBehavior.translucent,
|
||||||
? MediaQuery.sizeOf(context).height * 2
|
onDoubleTap: _mainCameraController.onDoubleTap,
|
||||||
: 0,
|
onTapDown: _mainCameraController.onTapDown,
|
||||||
child: Opacity(
|
)
|
||||||
opacity: 1 - (_offsetRatio * 4) % 1,
|
: const SizedBox.shrink(),
|
||||||
child: CameraPreviewControllerView(
|
),
|
||||||
mainController: _mainCameraController,
|
Positioned(
|
||||||
isVisible:
|
key: const ValueKey('camera_controls'),
|
||||||
((1 - (_offsetRatio * 4) % 1) == 1) &&
|
left: 0,
|
||||||
_activePageIdx == 1,
|
top: 0,
|
||||||
),
|
right: 0,
|
||||||
|
bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 1 - (_offsetRatio * 4) % 1,
|
||||||
|
child: CameraPreviewControllerView(
|
||||||
|
mainController: _mainCameraController,
|
||||||
|
isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: AnimatedSize(
|
bottomNavigationBar: AnimatedSize(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue