verify other users

This commit is contained in:
otsmr 2025-02-08 21:29:23 +01:00
parent e7a4b59379
commit ee2dcd788a
24 changed files with 433 additions and 88 deletions

View file

@ -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

View file

@ -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<Int64> 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,

View file

@ -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),

View file

@ -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,
);
}
}

View file

@ -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<UserContextMenu> {
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'),

View file

@ -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,
),
),
);
}
}

View file

@ -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",

View file

@ -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<int>(columnBlocked);
static const columnVerified = "verified";
final verified = CvField<int>(columnVerified);
static const columnTotalMediaCounter = "total_media_counter";
final totalMediaCounter = CvField<int>(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<String, dynamic> updates = {
columnVerified: status ? 1 : 0,
};
await _update(userId, updates);
}
static Future deleteUser(int userId) async {
await dbProvider.db!.delete(
tableName,

View file

@ -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<DateTime>(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

View file

@ -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<DateTime>(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<List<(DateTime, int?)>> getMessageDates(int otherUserId) async {

View file

@ -1,5 +0,0 @@
const String dbName = 'twonly.db';
const int kVersion1 = 3;
String tableLibSignal = 'LibSignal';

View file

@ -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<DateTime>(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

View file

@ -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<DateTime>(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

View file

@ -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<DateTime>(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

View file

@ -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<Database?> 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 {

View file

@ -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<bool> 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<void> writeLogToFile(LogRecord record) async {
final directory = await getApplicationDocumentsDirectory();
final logFile = File('${directory.path}/app.log');

View file

@ -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<Fingerprint?> 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<Fingerprint?> 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);

View file

@ -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<ShareImageView> {
Uint8List? imageBytes;
final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
final TextEditingController searchUserName = TextEditingController();
bool showRealTwonlyWarning = false;
@override
void initState() {
@ -105,6 +107,14 @@ class _ShareImageView extends State<ShareImageView> {
}
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<ShareImageView> {
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<ShareImageView> {
List.from(_otherUsers),
maxTotalMediaCounter,
selectedUserIds: _selectedUserIds,
isRealTwonly: widget.isRealTwonly,
updateStatus: updateStatus,
),
)
@ -206,11 +226,18 @@ class _ShareImageView extends State<ShareImageView> {
}
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<Contact> users;
final int maxTotalMediaCounter;
final bool isRealTwonly;
final HashSet<Int64> 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,

View file

@ -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<ChatItemDetailsView> {
Expanded(
child: Container(
color: Colors.transparent,
child: Text(widget.user.displayName),
child: Row(
children: [
Text(widget.user.displayName),
SizedBox(width: 10),
VerifiedShield(widget.user),
],
),
),
),
],

View file

@ -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<ContactVerifyView> {
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),
),
],
),
),
),
);
}

View file

@ -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<ContactView> {
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),

View file

@ -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());
},
),

View file

@ -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:

View file

@ -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