mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
verify other users
This commit is contained in:
parent
e7a4b59379
commit
ee2dcd788a
24 changed files with 433 additions and 88 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
37
lib/src/components/format_long_string.dart
Normal file
37
lib/src/components/format_long_string.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
38
lib/src/components/verified_shield.dart
Normal file
38
lib/src/components/verified_shield.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
const String dbName = 'twonly.db';
|
||||
|
||||
const int kVersion1 = 3;
|
||||
|
||||
String tableLibSignal = 'LibSignal';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
),
|
||||
|
|
|
|||
16
pubspec.lock
16
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue