diff --git a/README.md b/README.md index b1bf285..9a90d1b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure ## TODOS bevor first beta -- Verify contact view +- Send a picture first to only one person -> Kamera button - Context Menu - Pro Invitation codes diff --git a/lib/src/components/best_friends_selector.dart b/lib/src/components/best_friends_selector.dart index aa10299..d5ea1d6 100644 --- a/lib/src/components/best_friends_selector.dart +++ b/lib/src/components/best_friends_selector.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/components/flame.dart'; @@ -14,11 +15,13 @@ class BestFriendsSelector extends StatelessWidget { final Function(Int64, bool) updateStatus; final HashSet selectedUserIds; final int maxTotalMediaCounter; + final bool isRealTwonly; const BestFriendsSelector({ super.key, required this.users, required this.maxTotalMediaCounter, + required this.isRealTwonly, required this.updateStatus, required this.selectedUserIds, }); @@ -48,6 +51,7 @@ class BestFriendsSelector extends StatelessWidget { .contains(users[firstUserIndex].userId), user: users[firstUserIndex], onChanged: updateStatus, + isRealTwonly: isRealTwonly, maxTotalMediaCounter: maxTotalMediaCounter, ), ), @@ -58,6 +62,7 @@ class BestFriendsSelector extends StatelessWidget { .contains(users[secondUserIndex].userId), user: users[secondUserIndex], onChanged: updateStatus, + isRealTwonly: isRealTwonly, maxTotalMediaCounter: maxTotalMediaCounter), ) : Expanded( @@ -77,6 +82,7 @@ class UserCheckbox extends StatelessWidget { final Contact user; final Function(Int64, bool) onChanged; final bool isChecked; + final bool isRealTwonly; final int maxTotalMediaCounter; const UserCheckbox({ @@ -84,6 +90,7 @@ class UserCheckbox extends StatelessWidget { required this.user, required this.maxTotalMediaCounter, required this.onChanged, + required this.isRealTwonly, required this.isChecked, }); @@ -113,18 +120,36 @@ class UserCheckbox extends StatelessWidget { child: Row( children: [ InitialsAvatar( - fontSize: 15, + fontSize: 12, displayName: user.displayName, ), SizedBox(width: 8), - Text( - user.displayName.length > 10 - ? '${user.displayName.substring(0, 10)}...' - : user.displayName, - overflow: TextOverflow.ellipsis, + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isRealTwonly) + Padding( + padding: EdgeInsets.only(right: 2), + child: VerifiedShield( + user, + size: 12, + )), + Text( + user.displayName.length > 10 + ? '${user.displayName.substring(0, 10)}...' + : user.displayName, + overflow: TextOverflow.ellipsis, + ), + ], + ), + if (flameCounter > 0) + FlameCounterWidget( + user, flameCounter, maxTotalMediaCounter), + ], ), - if (flameCounter > 0) - FlameCounterWidget(user, flameCounter, maxTotalMediaCounter), Expanded(child: Container()), Checkbox( value: isChecked, diff --git a/lib/src/components/flame.dart b/lib/src/components/flame.dart index cc1f723..b041eed 100644 --- a/lib/src/components/flame.dart +++ b/lib/src/components/flame.dart @@ -5,11 +5,13 @@ class FlameCounterWidget extends StatelessWidget { final Contact user; final int maxTotalMediaCounter; final int flameCounter; + final bool prefix; const FlameCounterWidget( this.user, this.flameCounter, this.maxTotalMediaCounter, { + this.prefix = false, super.key, }); @@ -17,9 +19,9 @@ class FlameCounterWidget extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - const SizedBox(width: 5), - Text("•"), - const SizedBox(width: 5), + if (prefix) const SizedBox(width: 5), + if (prefix) Text("•"), + if (prefix) const SizedBox(width: 5), Text( flameCounter.toString(), style: const TextStyle(fontSize: 13), diff --git a/lib/src/components/format_long_string.dart b/lib/src/components/format_long_string.dart new file mode 100644 index 0000000..7b90904 --- /dev/null +++ b/lib/src/components/format_long_string.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class FormattedStringWidget extends StatelessWidget { + final String longString; + + const FormattedStringWidget(this.longString, {super.key}); + + String formatString(String input) { + StringBuffer formattedString = StringBuffer(); + int blockCount = 0; + + for (int i = 0; i < input.length; i += 4) { + String block = + input.substring(i, i + 4 > input.length ? input.length : i + 4); + formattedString.write(block); + blockCount++; + + if (blockCount == 5) { + formattedString.writeln(); + blockCount = 0; + } else { + formattedString.write(' '); + } + } + + return formattedString.toString().trim(); + } + + @override + Widget build(BuildContext context) { + return Text( + formatString(longString), + style: TextStyle(fontSize: 18, color: Colors.black), + textAlign: TextAlign.center, + ); + } +} diff --git a/lib/src/components/user_context_menu.dart b/lib/src/components/user_context_menu.dart index 0523b39..bf68782 100644 --- a/lib/src/components/user_context_menu.dart +++ b/lib/src/components/user_context_menu.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:pie_menu/pie_menu.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/views/contact/contact_verify_view.dart'; class UserContextMenu extends StatefulWidget { final Widget child; @@ -22,9 +23,15 @@ class _UserContextMenuState extends State { PieAction( tooltip: const Text('Verify user'), onSelect: () { - print('Verify user selected'); + Navigator.push(context, MaterialPageRoute( + builder: (context) { + return ContactVerifyView(widget.user); + }, + )); }, - child: const Icon(Icons.gpp_maybe_rounded), // Can be any widget + child: widget.user.verified + ? FaIcon(FontAwesomeIcons.shieldHeart) + : const Icon(Icons.gpp_maybe_rounded), // Can be any widget ), PieAction( tooltip: const Text('Send image'), diff --git a/lib/src/components/verified_shield.dart b/lib/src/components/verified_shield.dart new file mode 100644 index 0000000..b21fabc --- /dev/null +++ b/lib/src/components/verified_shield.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/views/contact/contact_verify_view.dart'; + +class VerifiedShield extends StatelessWidget { + final Contact contact; + final double size; + + const VerifiedShield(this.contact, {super.key, this.size = 18}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute( + builder: (context) { + return ContactVerifyView(contact); + }, + )); + }, + child: Tooltip( + message: contact.verified + ? "You verified this contact" + : "Click here to verify your contact.", + child: FaIcon( + contact.verified + ? FontAwesomeIcons.shieldHeart + : Icons.gpp_maybe_rounded, + color: contact.verified + ? Theme.of(context).colorScheme.primary + : Colors.red, + size: size, + ), + ), + ); + } +} diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 960eae9..c269c02 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -15,6 +15,7 @@ "shareImagedEditorSaveImage": "Save", "shareImagedEditorSavedImage": "Saved", "shareImageAllUsers": "All contacts", + "shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!", "searchUsernameInput": "Username", "searchUsernameTitle": "Search username", "searchUsernameNotFound": "Username not found", @@ -48,6 +49,10 @@ "settingsAccountDeleteAccount": "Delete account", "settingsAccountDeleteModalTitle": "Are you sure?", "settingsAccountDeleteModalBody": "Your account will be deleted. There is no change to restore it.", + "contactVerifyNumberTitle": "Verify safety number", + "contactVerifyNumberMarkAsVerified": "Mark as verified", + "contactVerifyNumberClearVerification": "Clear verification", + "contactVerifyNumberLongDesc": "To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.", "undo": "Undo", "redo": "Redo", "close": "Close", diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index 7d77dfe..fa80cb0 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -2,8 +2,10 @@ import 'package:cv/cv.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/app.dart'; +import 'package:twonly/src/utils/misc.dart'; class Contact { Contact( @@ -11,6 +13,7 @@ class Contact { required this.displayName, required this.accepted, required this.blocked, + required this.verified, required this.totalMediaCounter, required this.requested}); final Int64 userId; @@ -18,6 +21,7 @@ class Contact { final bool accepted; final bool requested; final bool blocked; + final bool verified; final int totalMediaCounter; } @@ -39,6 +43,9 @@ class DbContacts extends CvModelBase { static const columnBlocked = "blocked"; final blocked = CvField(columnBlocked); + static const columnVerified = "verified"; + final verified = CvField(columnVerified); + static const columnTotalMediaCounter = "total_media_counter"; final totalMediaCounter = CvField(columnTotalMediaCounter); @@ -47,18 +54,28 @@ class DbContacts extends CvModelBase { static const nextFlameCounterInSeconds = kDebugMode ? 60 : 60 * 60 * 24; - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnUserId INTEGER NOT NULL PRIMARY KEY, $columnDisplayName TEXT, $columnAccepted INT NOT NULL DEFAULT 0, $columnRequested INT NOT NULL DEFAULT 0, $columnBlocked INT NOT NULL DEFAULT 0, + $columnVerified INTEGER NOT NULL DEFAULT 0, $columnTotalMediaCounter INT NOT NULL DEFAULT 0, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ) """; + await db.execute(createTableString); + + if (!await columnExists(db, tableName, columnVerified)) { + String alterTableString = """ + ALTER TABLE $tableName + ADD COLUMN $columnVerified INTEGER NOT NULL DEFAULT 0 + """; + await db.execute(alterTableString); + } } @override @@ -108,6 +125,7 @@ class DbContacts extends CvModelBase { columnAccepted, columnRequested, columnBlocked, + columnVerified, columnTotalMediaCounter, columnCreatedAt ]); @@ -123,6 +141,7 @@ class DbContacts extends CvModelBase { displayName: users.cast()[i][columnDisplayName], accepted: users[i][columnAccepted] == 1, blocked: users[i][columnBlocked] == 1, + verified: users[i][columnVerified] == 1, requested: users[i][columnRequested] == 1, ), ); @@ -175,6 +194,13 @@ class DbContacts extends CvModelBase { await _update(userId, updates); } + static Future updateVerificationStatus(int userId, bool status) async { + Map updates = { + columnVerified: status ? 1 : 0, + }; + await _update(userId, updates); + } + static Future deleteUser(int userId) async { await dbProvider.db!.delete( tableName, diff --git a/lib/src/model/identity_key_store_model.dart b/lib/src/model/identity_key_store_model.dart index 3a6c395..a8f6d5f 100644 --- a/lib/src/model/identity_key_store_model.dart +++ b/lib/src/model/identity_key_store_model.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:cv/cv.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; class DbSignalIdentityKeyStore extends CvModelBase { static const tableName = "signal_identity_key_store"; @@ -17,8 +18,8 @@ class DbSignalIdentityKeyStore extends CvModelBase { static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnDeviceId INTEGER NOT NULL, $columnName TEXT NOT NULL, @@ -27,6 +28,7 @@ class DbSignalIdentityKeyStore extends CvModelBase { PRIMARY KEY ($columnDeviceId, $columnName) ) """; + await db.execute(createTableString); } @override diff --git a/lib/src/model/messages_model.dart b/lib/src/model/messages_model.dart index 069cf75..db86b3d 100644 --- a/lib/src/model/messages_model.dart +++ b/lib/src/model/messages_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cv/cv.dart'; import 'package:logging/logging.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/app.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; @@ -103,8 +104,8 @@ class DbMessages extends CvModelBase { static const columnUpdatedAt = "updated_at"; final updatedAt = CvField(columnUpdatedAt); - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnMessageId INTEGER NOT NULL PRIMARY KEY, $columnMessageOtherId INTEGER DEFAULT NULL, @@ -118,6 +119,7 @@ class DbMessages extends CvModelBase { $columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ) """; + await db.execute(createTableString); } static Future> getMessageDates(int otherUserId) async { diff --git a/lib/src/model/model_constants.dart b/lib/src/model/model_constants.dart deleted file mode 100644 index ab1dca4..0000000 --- a/lib/src/model/model_constants.dart +++ /dev/null @@ -1,5 +0,0 @@ -const String dbName = 'twonly.db'; - -const int kVersion1 = 3; - -String tableLibSignal = 'LibSignal'; diff --git a/lib/src/model/pre_key_model.dart b/lib/src/model/pre_key_model.dart index cfd8c1f..4280170 100644 --- a/lib/src/model/pre_key_model.dart +++ b/lib/src/model/pre_key_model.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:cv/cv.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; class DbSignalPreKeyStore extends CvModelBase { static const tableName = "signal_pre_key_store"; @@ -13,8 +14,8 @@ class DbSignalPreKeyStore extends CvModelBase { static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnPreKeyId INTEGER NOT NULL, $columnPreKey BLOB NOT NULL, @@ -22,6 +23,7 @@ class DbSignalPreKeyStore extends CvModelBase { PRIMARY KEY ($columnPreKeyId) ) """; + await db.execute(createTableString); } @override diff --git a/lib/src/model/sender_key_store_model.dart b/lib/src/model/sender_key_store_model.dart index 88b8703..ada0d87 100644 --- a/lib/src/model/sender_key_store_model.dart +++ b/lib/src/model/sender_key_store_model.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:cv/cv.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; class DbSignalSenderKeyStore extends CvModelBase { static const tableName = "signal_sender_key_store"; @@ -13,8 +14,8 @@ class DbSignalSenderKeyStore extends CvModelBase { static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnSenderKeyName TEXT NOT NULL, $columnSenderKey BLOB NOT NULL, @@ -22,6 +23,7 @@ class DbSignalSenderKeyStore extends CvModelBase { PRIMARY KEY ($columnSenderKeyName) ) """; + await db.execute(createTableString); } @override diff --git a/lib/src/model/session_store_model.dart b/lib/src/model/session_store_model.dart index db4d142..b21e70c 100644 --- a/lib/src/model/session_store_model.dart +++ b/lib/src/model/session_store_model.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:cv/cv.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; class DbSignalSessionStore extends CvModelBase { static const tableName = "signal_session_store"; @@ -16,8 +17,8 @@ class DbSignalSessionStore extends CvModelBase { static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); - static String getCreateTableString() { - return """ + static Future setupDatabaseTable(Database db) async { + String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( $columnDeviceId INTEGER NOT NULL, $columnName TEXT NOT NULL, @@ -26,6 +27,7 @@ class DbSignalSessionStore extends CvModelBase { PRIMARY KEY ($columnDeviceId, $columnName) ) """; + await db.execute(createTableString); } @override diff --git a/lib/src/providers/db_provider.dart b/lib/src/providers/db_provider.dart index 8954380..05c1bd9 100644 --- a/lib/src/providers/db_provider.dart +++ b/lib/src/providers/db_provider.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/identity_key_store_model.dart'; import 'package:twonly/src/model/messages_model.dart'; -import 'package:twonly/src/model/model_constants.dart'; import 'package:twonly/src/model/pre_key_model.dart'; import 'package:twonly/src/model/sender_key_store_model.dart'; import 'package:twonly/src/model/session_store_model.dart'; @@ -13,6 +12,8 @@ import 'package:twonly/src/utils/misc.dart'; class DbProvider { Database? db; + static const String dbName = 'twonly.db'; + Future openPath(String path) async { // Read the password for the database from the secure storage. If there is no, then create a // new cryptographically secure random password with 32 bytes and store them in the secure storage. @@ -36,14 +37,10 @@ class DbProvider { await storage.write(key: "sqflite_database_password", value: password); } - db = await openDatabase(path, password: password, version: kVersion1, - onCreate: (db, version) async { - await _createDb(db); - }, onUpgrade: (db, oldVersion, newVersion) async { - if (oldVersion < kVersion1) { - //await _createDb(db); - } - }); + db = await openDatabase(path, password: password); + if (db != null) { + await setupDatabaseTable(db!); + } } Future get ready async { @@ -53,13 +50,13 @@ class DbProvider { return db; } - Future _createDb(Database db) async { - await db.execute(DbSignalSessionStore.getCreateTableString()); - await db.execute(DbSignalPreKeyStore.getCreateTableString()); - await db.execute(DbSignalSenderKeyStore.getCreateTableString()); - await db.execute(DbSignalIdentityKeyStore.getCreateTableString()); - await db.execute(DbContacts.getCreateTableString()); - await db.execute(DbMessages.getCreateTableString()); + Future setupDatabaseTable(Database db) async { + await DbSignalSessionStore.setupDatabaseTable(db); + await DbSignalPreKeyStore.setupDatabaseTable(db); + await DbSignalSenderKeyStore.setupDatabaseTable(db); + await DbSignalIdentityKeyStore.setupDatabaseTable(db); + await DbContacts.setupDatabaseTable(db); + await DbMessages.setupDatabaseTable(db); } Future open() async { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index e81f4f8..267a5a1 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -7,6 +7,7 @@ import 'package:gal/gal.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -15,6 +16,18 @@ extension LocalizationExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; } +// Function to check if a column exists +Future columnExists( + Database db, String tableName, String columnName) async { + final result = await db.rawQuery('PRAGMA table_info($tableName)'); + for (var row in result) { + if (row['name'] == columnName) { + return true; // Column exists + } + } + return false; // Column does not exist +} + Future writeLogToFile(LogRecord record) async { final directory = await getApplicationDocumentsDirectory(); final logFile = File('${directory.path}/app.log'); diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index ae915ac..3837444 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -6,9 +6,11 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/signal_identity.dart'; +import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart'; import 'package:twonly/src/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; const int defaultDeviceId = 1; @@ -147,26 +149,30 @@ Future createIfNotExistsSignalIdentity() async { key: "signal_identity", value: jsonEncode(storedSignalIdentity)); } -// Future generateSessionFingerPrint(String target) async { -// try { -// IdentityKey? targetIdentity = await signalStore -// .getIdentity(SignalProtocolAddress(target, defaultDeviceId)); -// if (targetIdentity != null) { -// final generator = NumericFingerprintGenerator(5200); -// final localFingerprint = generator.createFor( -// 1, -// userId, -// (await signalStore.getIdentityKeyPair()).getPublicKey(), -// Uint8List.fromList(utf8.encode(target)), -// targetIdentity, -// ); -// return localFingerprint; -// } -// return null; -// } catch (e) { -// return null; -// } -// } +Future generateSessionFingerPrint(Int64 target) async { + ConnectSignalProtocolStore? signalStore = await getSignalStore(); + UserData? user = await getUser(); + if (signalStore == null || user == null) return null; + try { + IdentityKey? targetIdentity = await signalStore + .getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId)); + if (targetIdentity != null) { + final generator = NumericFingerprintGenerator(5200); + final localFingerprint = generator.createFor( + 1, + Uint8List.fromList([user.userId.toInt()]), + (await signalStore.getIdentityKeyPair()).getPublicKey(), + Uint8List.fromList([target.toInt()]), + targetIdentity, + ); + + return localFingerprint; + } + return null; + } catch (e) { + return null; + } +} Uint8List intToBytes(int value) { final byteData = ByteData(4); diff --git a/lib/src/views/camera_to_share/share_image_view.dart b/lib/src/views/camera_to_share/share_image_view.dart index 88e5f21..d55c547 100644 --- a/lib/src/views/camera_to_share/share_image_view.dart +++ b/lib/src/views/camera_to_share/share_image_view.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/flame.dart'; import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; +import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; @@ -36,6 +37,7 @@ class _ShareImageView extends State { Uint8List? imageBytes; final HashSet _selectedUserIds = HashSet(); final TextEditingController searchUserName = TextEditingController(); + bool showRealTwonlyWarning = false; @override void initState() { @@ -105,6 +107,14 @@ class _ShareImageView extends State { } void updateStatus(Int64 userId, bool checked) { + if (widget.isRealTwonly) { + Contact user = _users.firstWhere((x) => x.userId == userId); + if (!user.verified) { + showRealTwonlyWarning = true; + return; + } + } + showRealTwonlyWarning = false; if (checked) { if (widget.isRealTwonly) { _selectedUserIds.clear(); @@ -126,16 +136,25 @@ class _ShareImageView extends State { child: Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: TextField( - onChanged: _filterUsers, - decoration: getInputDecoration( - context, context.lang.searchUsernameInput))), + padding: EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: _filterUsers, + decoration: getInputDecoration( + context, context.lang.searchUsernameInput), + ), + ), + if (showRealTwonlyWarning) const SizedBox(height: 10), + if (showRealTwonlyWarning) + Text( + context.lang.shareImageAllTwonlyWarning, + style: TextStyle(color: Colors.orange), + ), const SizedBox(height: 10), BestFriendsSelector( users: _bestFriends, selectedUserIds: _selectedUserIds, maxTotalMediaCounter: maxTotalMediaCounter, + isRealTwonly: widget.isRealTwonly, updateStatus: updateStatus, ), const SizedBox(height: 10), @@ -146,6 +165,7 @@ class _ShareImageView extends State { List.from(_otherUsers), maxTotalMediaCounter, selectedUserIds: _selectedUserIds, + isRealTwonly: widget.isRealTwonly, updateStatus: updateStatus, ), ) @@ -206,11 +226,18 @@ class _ShareImageView extends State { } class UserList extends StatelessWidget { - const UserList(this.users, this.maxTotalMediaCounter, - {super.key, required this.selectedUserIds, required this.updateStatus}); + const UserList( + this.users, + this.maxTotalMediaCounter, { + super.key, + required this.selectedUserIds, + required this.updateStatus, + required this.isRealTwonly, + }); final Function(Int64, bool) updateStatus; final List users; final int maxTotalMediaCounter; + final bool isRealTwonly; final HashSet selectedUserIds; @override @@ -228,11 +255,25 @@ class UserList extends StatelessWidget { Contact user = users[i]; int flameCounter = flameCounters[user.userId.toInt()] ?? 0; return ListTile( - title: Row(children: [ - Text(user.displayName), - if (flameCounter > 0) - FlameCounterWidget(user, flameCounter, maxTotalMediaCounter), - ]), + title: Row( + mainAxisAlignment: MainAxisAlignment.start, // Center horizontally + crossAxisAlignment: CrossAxisAlignment.center, // Center vertically + children: [ + if (isRealTwonly) + Padding( + padding: const EdgeInsets.only(right: 1), + child: VerifiedShield(user), + ), + Text(user.displayName), + if (flameCounter >= 0) + FlameCounterWidget( + user, + flameCounter, + maxTotalMediaCounter, + prefix: true, + ), + ], + ), leading: InitialsAvatar( displayName: user.displayName, fontSize: 15, diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 7a29738..2952fad 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; +import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/messages_model.dart'; @@ -205,7 +206,13 @@ class _ChatItemDetailsViewState extends State { Expanded( child: Container( color: Colors.transparent, - child: Text(widget.user.displayName), + child: Row( + children: [ + Text(widget.user.displayName), + SizedBox(width: 10), + VerifiedShield(widget.user), + ], + ), ), ), ], diff --git a/lib/src/views/contact/contact_verify_view.dart b/lib/src/views/contact/contact_verify_view.dart index 8fa89ae..ce103ee 100644 --- a/lib/src/views/contact/contact_verify_view.dart +++ b/lib/src/views/contact/contact_verify_view.dart @@ -1,5 +1,13 @@ +import 'dart:convert'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:twonly/src/components/format_long_string.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/signal.dart'; +import 'package:url_launcher/url_launcher.dart'; class ContactVerifyView extends StatefulWidget { const ContactVerifyView(this.contact, {super.key}); @@ -11,21 +19,125 @@ class ContactVerifyView extends StatefulWidget { } class _ContactVerifyViewState extends State { + Fingerprint? fingerprint; + @override void initState() { super.initState(); + loadAsync(); + } + + Future loadAsync() async { + fingerprint = await generateSessionFingerPrint(widget.contact.userId); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Verify ${widget.contact.displayName}"), + title: Text(context.lang.contactVerifyNumberTitle), ), - body: ListView( - children: [ - SizedBox(height: 50), - ], + body: (fingerprint == null) + ? Center(child: CircularProgressIndicator()) + : ListView( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Container( + padding: EdgeInsets.all(25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white, + ), + child: QrImageView( + data: base64Encode(fingerprint! + .scannableFingerprint.fingerprints), + version: QrVersions.auto, + size: 150.0, + ), + ), + SizedBox(height: 10), + SizedBox( + width: 200, + child: Text( + "QR Code scanning is coming soon. Please compare the numbers manual.", + style: + TextStyle(color: Colors.black, fontSize: 10), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 20), + FormattedStringWidget( + fingerprint!.displayableFingerprint + .getDisplayText(), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Text( + context.lang.contactVerifyNumberLongDesc( + widget.contact.displayName), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: GestureDetector( + onTap: () { + launchUrl(Uri.parse("https://twonly.eu/verify")); + }, + child: Text( + "Read more.", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ) + ], + ), + bottomNavigationBar: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: 60), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.contact.verified + ? OutlinedButton.icon( + onPressed: () { + DbContacts.updateVerificationStatus( + widget.contact.userId.toInt(), false); + }, + label: Text( + context.lang.contactVerifyNumberClearVerification), + ) + : FilledButton.icon( + icon: FaIcon(FontAwesomeIcons.shieldHeart), + onPressed: () { + DbContacts.updateVerificationStatus( + widget.contact.userId.toInt(), true); + }, + label: + Text(context.lang.contactVerifyNumberMarkAsVerified), + ), + ], + ), + ), ), ); } diff --git a/lib/src/views/contact/contact_view.dart b/lib/src/views/contact/contact_view.dart index 1af4aa1..bff2203 100644 --- a/lib/src/views/contact/contact_view.dart +++ b/lib/src/views/contact/contact_view.dart @@ -4,6 +4,7 @@ import 'package:twonly/src/components/alert_dialog.dart'; import 'package:twonly/src/components/better_list_title.dart'; import 'package:twonly/src/components/flame.dart'; import 'package:twonly/src/components/initialsavatar.dart'; +import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; @@ -47,12 +48,20 @@ class _ContactViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + Padding( + padding: EdgeInsets.only(right: 10), + child: VerifiedShield(widget.contact)), Text( widget.contact.displayName, style: TextStyle(fontSize: 20), ), if (flameCounter > 0) - FlameCounterWidget(widget.contact, flameCounter, 110000000), + FlameCounterWidget( + widget.contact, + flameCounter, + 110000000, + prefix: true, + ), ], ), SizedBox(height: 50), diff --git a/lib/src/views/settings/privacy_view_block_users.dart b/lib/src/views/settings/privacy_view_block_users.dart index 0183c0b..406073c 100644 --- a/lib/src/views/settings/privacy_view_block_users.dart +++ b/lib/src/views/settings/privacy_view_block_users.dart @@ -101,7 +101,6 @@ class UserList extends StatelessWidget { itemCount: users.length, itemBuilder: (BuildContext context, int i) { Contact user = users[i]; - print(user.blocked); return ListTile( title: Row(children: [ Text(user.displayName), @@ -113,7 +112,6 @@ class UserList extends StatelessWidget { trailing: Checkbox( value: user.blocked, onChanged: (bool? value) { - print(value); block(value, user.userId.toInt()); }, ), diff --git a/pubspec.lock b/pubspec.lock index 180f210..2f46193 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1045,6 +1045,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" reorderables: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f14e7f1..d44d4c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: pie_menu: ^3.2.7 protobuf: ^2.1.0 provider: ^6.1.2 + qr_flutter: ^4.1.0 reorderables: ^0.6.0 restart_app: ^1.3.2 screenshot: ^3.0.0