move signal to drift

This commit is contained in:
otsmr 2025-03-16 20:51:38 +01:00
parent 8fbb5b001c
commit 0b084f795a
57 changed files with 2100 additions and 736 deletions

View file

@ -1,9 +1,7 @@
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
late ApiProvider apiProvider;
// uses for background notification
late DbProvider dbProvider;
late TwonlyDatabase twonlyDatabase;

View file

@ -1,12 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:twonly/src/providers/db_provider.dart';
import 'package:twonly/src/providers/hive.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/providers/settings_change_provider.dart';
@ -39,14 +37,9 @@ void main() async {
await initMediaStorage();
await initFCMService();
dbProvider = DbProvider();
await dbProvider.ready;
apiProvider = ApiProvider();
twonlyDatabase = TwonlyDatabase();
FlutterForegroundTask.initCommunicationPort();
runApp(
MultiProvider(
providers: [

View file

@ -1,4 +1,3 @@
import 'package:cv/cv.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
@ -30,8 +29,7 @@ class EmojiAnimation extends StatelessWidget {
// Check if the emoji has a corresponding Lottie animation
if (animatedIcons.containsKey(emoji)) {
return Lottie.asset(
"assets/animated_icons/${animatedIcons.getValue(emoji)}");
return Lottie.asset("assets/animated_icons/${animatedIcons[emoji]}");
} else {
return Text(
emoji,

View file

@ -2,8 +2,8 @@ import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/verified_shield.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/headline.dart';
@ -142,7 +142,8 @@ class UserCheckbox extends StatelessWidget {
],
),
StreamBuilder(
stream: twonlyDatabase.watchFlameCounter(user.userId),
stream: twonlyDatabase.contactsDao
.watchFlameCounter(user.userId),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data! == 0) {
return Container();

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/components/animate_icon.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
class FlameCounterWidget extends StatelessWidget {
final Contact user;

View file

@ -2,9 +2,9 @@ import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/utils/misc.dart';
enum MessageSendState {

View file

@ -1,7 +1,4 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionHandlerView extends StatefulWidget {
@ -48,28 +45,6 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
// }
}
if (Platform.isAndroid) {
// Android 12+, there are restrictions on starting a foreground service.
//
// To restart the service on device reboot or unexpected problem, you need to allow below permission.
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
}
// Use this utility only if you provide services that require long-term survival,
// such as exact alarm service, healthcare service, or Bluetooth communication.
//
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
// Using this permission may make app distribution difficult due to Google policy.
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
// When you call this function, will be gone to the settings page.
// So you need to explain to the user why set it.
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
// }
}
/*{Permission.camera: PermissionStatus.granted, Permission.storage: PermissionStatus.granted}*/
return statuses;
}

View file

@ -2,7 +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:provider/provider.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_item_details_view.dart';

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
class VerifiedShield extends StatelessWidget {
final Contact contact;

View file

@ -1,145 +1,15 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
part 'database.g.dart';
part 'contacts_dao.g.dart';
// You can then create a database class that includes this table
@DriftDatabase(tables: [Contacts, Messages])
class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'twonly_main_db',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
// ------------
Stream<List<Message>> watchMessageNotOpened(int contactId) {
return (select(messages)
..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
return (select(messages)
..where((t) =>
t.openedAt.isNull() &
t.contactId.equals(contactId) &
t.kind.equals(MessageKind.media.name))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Stream<List<Message>> watchLastMessage(int contactId) {
return (select(messages)
..where((t) => t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
..limit(1))
.watch();
}
Stream<List<Message>> watchAllMessagesFrom(int contactId) {
return (select(messages)
..where((t) =>
t.contactId.equals(contactId) &
t.contentJson.isNotNull() &
(t.openedAt.isNull() |
t.openedAt.isBiggerThanValue(
DateTime.now().subtract(Duration(days: 1)))))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Future removeOldMessages() {
return (update(messages)
..where((t) =>
t.openedAt.isSmallerThanValue(
DateTime.now().subtract(Duration(days: 1))) &
t.kind.equals(MessageKind.textMessage.name)))
.write(MessagesCompanion(contentJson: Value(null)));
}
Future<List<Message>> getAllMessagesPendingDownloading() {
return (select(messages)
..where(
(t) =>
t.downloadState.equals(DownloadState.downloaded.index).not() &
t.messageOtherId.isNotNull() &
t.kind.equals(MessageKind.media.name),
))
.get();
}
Future<List<Message>> getAllMessagesForRetransmitting() {
return (select(messages)..where((t) => t.acknowledgeByServer.equals(false)))
.get();
}
Future openedAllTextMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages)
..where((t) =>
t.contactId.equals(contactId) &
t.openedAt.isNull() &
t.kind.equals(MessageKind.textMessage.name)))
.write(updates);
}
Future updateMessageByOtherUser(
int userId, int messageId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
c.contactId.equals(userId) & c.messageId.equals(messageId)))
.write(updatedValues);
}
Future updateMessageByOtherMessageId(
int userId, int messageOtherId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
c.contactId.equals(userId) &
c.messageOtherId.equals(messageOtherId)))
.write(updatedValues);
}
Future updateMessageByMessageId(
int messageId, MessagesCompanion updatedValues) {
return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
}
Future<int?> insertMessage(MessagesCompanion message) async {
try {
return await into(messages).insert(message);
} catch (e) {
Logger("twonlyDatabase").shout("Error while inserting message: $e");
return null;
}
}
Future deleteMessageById(int messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
SingleOrNullSelectable<Message> getMessageByMessageId(int messageId) {
return select(messages)..where((t) => t.messageId.equals(messageId));
}
// ------------
@DriftAccessor(tables: [Contacts])
class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
with _$ContactsDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
ContactsDao(TwonlyDatabase db) : super(db);
Future<int> insertContact(ContactsCompanion contact) async {
try {

View file

@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'contacts_dao.dart';
// ignore_for_file: type=lint
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDatabase> {
$ContactsTable get contacts => attachedDatabase.contacts;
}

View file

@ -0,0 +1,127 @@
import 'package:drift/drift.dart';
import 'package:logging/logging.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/json_models/message.dart';
part 'messages_dao.g.dart';
@DriftAccessor(tables: [Messages])
class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
with _$MessagesDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
MessagesDao(TwonlyDatabase db) : super(db);
Stream<List<Message>> watchMessageNotOpened(int contactId) {
return (select(messages)
..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
return (select(messages)
..where((t) =>
t.openedAt.isNull() &
t.contactId.equals(contactId) &
t.kind.equals(MessageKind.media.name))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Stream<List<Message>> watchLastMessage(int contactId) {
return (select(messages)
..where((t) => t.contactId.equals(contactId))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)])
..limit(1))
.watch();
}
Stream<List<Message>> watchAllMessagesFrom(int contactId) {
return (select(messages)
..where((t) =>
t.contactId.equals(contactId) &
t.contentJson.isNotNull() &
(t.openedAt.isNull() |
t.openedAt.isBiggerThanValue(
DateTime.now().subtract(Duration(days: 1)))))
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch();
}
Future removeOldMessages() {
return (update(messages)
..where((t) =>
t.openedAt.isSmallerThanValue(
DateTime.now().subtract(Duration(days: 1))) &
t.kind.equals(MessageKind.textMessage.name)))
.write(MessagesCompanion(contentJson: Value(null)));
}
Future<List<Message>> getAllMessagesPendingDownloading() {
return (select(messages)
..where(
(t) =>
t.downloadState.equals(DownloadState.downloaded.index).not() &
t.messageOtherId.isNotNull() &
t.kind.equals(MessageKind.media.name),
))
.get();
}
Future<List<Message>> getAllMessagesForRetransmitting() {
return (select(messages)..where((t) => t.acknowledgeByServer.equals(false)))
.get();
}
Future openedAllTextMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages)
..where((t) =>
t.contactId.equals(contactId) &
t.openedAt.isNull() &
t.kind.equals(MessageKind.textMessage.name)))
.write(updates);
}
Future updateMessageByOtherUser(
int userId, int messageId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
c.contactId.equals(userId) & c.messageId.equals(messageId)))
.write(updatedValues);
}
Future updateMessageByOtherMessageId(
int userId, int messageOtherId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
c.contactId.equals(userId) &
c.messageOtherId.equals(messageOtherId)))
.write(updatedValues);
}
Future updateMessageByMessageId(
int messageId, MessagesCompanion updatedValues) {
return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
}
Future<int?> insertMessage(MessagesCompanion message) async {
try {
return await into(messages).insert(message);
} catch (e) {
Logger("twonlyDatabase").shout("Error while inserting message: $e");
return null;
}
}
Future deleteMessageById(int messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
SingleOrNullSelectable<Message> getMessageByMessageId(int messageId) {
return select(messages)..where((t) => t.messageId.equals(messageId));
}
}

View file

@ -0,0 +1,9 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'messages_dao.dart';
// ignore_for_file: type=lint
mixin _$MessagesDaoMixin on DatabaseAccessor<TwonlyDatabase> {
$ContactsTable get contacts => attachedDatabase.contacts;
$MessagesTable get messages => attachedDatabase.messages;
}

View file

@ -1,5 +1,5 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
class Contacts extends Table {
IntColumn get userId => integer()();

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/json_models/message.dart';
enum DownloadState {
pending,
@ -8,6 +8,7 @@ enum DownloadState {
downloaded,
}
@DataClassName('Message')
class Messages extends Table {
IntColumn get contactId => integer().references(Contacts, #userId)();

View file

@ -0,0 +1,12 @@
import 'package:drift/drift.dart';
@DataClassName('SignalIdentityKeyStore')
class SignalIdentityKeyStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get identityKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -0,0 +1,11 @@
import 'package:drift/drift.dart';
@DataClassName('SignalPreKeyStore')
class SignalPreKeyStores extends Table {
IntColumn get preKeyId => integer()();
BlobColumn get preKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {preKeyId};
}

View file

@ -0,0 +1,10 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSenderKeyStore')
class SignalSenderKeyStores extends Table {
TextColumn get senderKeyName => text()();
BlobColumn get senderKey => blob()();
@override
Set<Column> get primaryKey => {senderKeyName};
}

View file

@ -0,0 +1,12 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSessionStore')
class SignalSessionStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get sessionRecord => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -0,0 +1,42 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/daos/messages_dao.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store_table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store_table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store_table.dart';
import 'package:twonly/src/database/tables/signal_session_store_table.dart';
import 'package:twonly/src/json_models/message.dart';
part 'twonly_database.g.dart';
// You can then create a database class that includes this table
@DriftDatabase(tables: [
Contacts,
Messages,
SignalIdentityKeyStores,
SignalPreKeyStores,
SignalSenderKeyStores,
SignalSessionStores
], daos: [
MessagesDao,
ContactsDao
])
class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'twonly_database',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
}

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_data.dart';
part of 'userdata.dart';
// **************************************************************************
// JsonSerializableGenerator

View file

@ -1,36 +0,0 @@
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";
static const columnDeviceId = "device_id";
final deviceId = CvField<int>(columnDeviceId);
static const columnName = "name";
final name = CvField<String>(columnName);
static const columnIdentityKey = "identity_key";
final identityKey = CvField<Uint8List>(columnIdentityKey);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnDeviceId INTEGER NOT NULL,
$columnName TEXT NOT NULL,
$columnIdentityKey BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ($columnDeviceId, $columnName)
)
""";
await db.execute(createTableString);
}
@override
List<CvField> get fields => [deviceId, name, identityKey, createdAt];
}

View file

@ -1,31 +0,0 @@
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";
static const columnPreKeyId = "pre_key_id";
final preKeyId = CvField<int>(columnPreKeyId);
static const columnPreKey = "pre_key";
final preKey = CvField<Uint8List>(columnPreKey);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnPreKeyId INTEGER NOT NULL,
$columnPreKey BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ($columnPreKeyId)
)
""";
await db.execute(createTableString);
}
@override
List<CvField> get fields => [preKeyId, preKey, createdAt];
}

View file

@ -1,31 +0,0 @@
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";
static const columnSenderKeyName = "sender_key_name";
final senderKeyName = CvField<String>(columnSenderKeyName);
static const columnSenderKey = "sender_key";
final senderKey = CvField<Uint8List>(columnSenderKey);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnSenderKeyName TEXT NOT NULL,
$columnSenderKey BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ($columnSenderKeyName)
)
""";
await db.execute(createTableString);
}
@override
List<CvField> get fields => [senderKeyName, senderKey, createdAt];
}

View file

@ -1,35 +0,0 @@
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";
static const columnDeviceId = "device_id";
final deviceId = CvField<int>(columnDeviceId);
static const columnName = "name";
final name = CvField<String>(columnName);
static const columnSessionRecord = "session_record";
final sessionRecord = CvField<Uint8List>(columnSessionRecord);
static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt);
static Future setupDatabaseTable(Database db) async {
String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName (
$columnDeviceId INTEGER NOT NULL,
$columnName TEXT NOT NULL,
$columnSessionRecord BLOB NOT NULL,
$columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ($columnDeviceId, $columnName)
)
""";
await db.execute(createTableString);
}
@override
List<CvField> get fields => [deviceId, name, sessionRecord, createdAt];
}

View file

@ -3,9 +3,9 @@ import 'package:drift/drift.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/providers/hive.dart';
@ -70,7 +70,7 @@ Future<Result> encryptAndSendMessage(
if (resp.isSuccess) {
if (messageId != null) {
await twonlyDatabase.updateMessageByMessageId(
await twonlyDatabase.messagesDao.updateMessageByMessageId(
messageId,
MessagesCompanion(acknowledgeByServer: Value(true)),
);
@ -86,7 +86,7 @@ Future sendTextMessage(int target, String message) async {
DateTime messageSendAt = DateTime.now();
int? messageId = await twonlyDatabase.insertMessage(
int? messageId = await twonlyDatabase.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(target),
kind: Value(MessageKind.textMessage),

View file

@ -5,9 +5,9 @@ import 'package:drift/drift.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/proto/api/server_to_client.pb.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
@ -19,7 +19,7 @@ Future tryDownloadAllMediaFiles() async {
return;
}
List<Message> messages =
await twonlyDatabase.getAllMessagesPendingDownloading();
await twonlyDatabase.messagesDao.getAllMessagesPendingDownloading();
for (Message message in messages) {
MessageContent? content =
@ -163,7 +163,7 @@ class ImageUploader {
final downloadToken = uploadState.downloadTokens.removeLast();
twonlyDatabase.incFlameCounter(
twonlyDatabase.contactsDao.incFlameCounter(
targetUserId,
false,
metadata.messageSendAt,
@ -215,7 +215,7 @@ Future sendImage(
// at this point it is safe inform the user about the process of sending the image..
for (final userId in metadata.userIds) {
int? messageId = await twonlyDatabase.insertMessage(
int? messageId = await twonlyDatabase.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(userId),
kind: Value(MessageKind.media),
@ -284,7 +284,7 @@ Future tryDownloadMedia(
box.put("${content.downloadToken!}_messageId", messageId);
await twonlyDatabase.updateMessageByOtherUser(
await twonlyDatabase.messagesDao.updateMessageByOtherUser(
fromUserId,
messageId,
MessagesCompanion(
@ -307,7 +307,7 @@ Future<Uint8List?> getDownloadedMedia(
// await userOpenedOtherMessage(otherUserId, messageOtherId);
notifyContactAboutOpeningMessage(message.contactId, message.messageOtherId!);
twonlyDatabase.updateMessageByMessageId(
twonlyDatabase.messagesDao.updateMessageByMessageId(
message.messageId, MessagesCompanion(openedAt: Value(DateTime.now())));
box.delete(downloadToken.toString());

View file

@ -7,9 +7,9 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
@ -76,7 +76,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
if (data.fin && data.data.isEmpty) {
// media file was deleted by the server. remove the media from device
await twonlyDatabase.deleteMessageById(messageId);
await twonlyDatabase.messagesDao.deleteMessageById(messageId);
box.delete(boxId);
box.delete("${data.downloadToken}_downloaded");
var ok = client.Response_Ok()..none = true;
@ -111,8 +111,9 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
// Uint8List? rawBytes =
// await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
Message? msg =
await twonlyDatabase.getMessageByMessageId(messageId).getSingleOrNull();
Message? msg = await twonlyDatabase.messagesDao
.getMessageByMessageId(messageId)
.getSingleOrNull();
if (msg == null) {
Logger("server_messages")
.info("messageId not found in database. Ignoring download");
@ -141,13 +142,13 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
} catch (e) {
Logger("server_messages").info("Decryption error: $e");
// deleting message as this is an invalid image
await twonlyDatabase.deleteMessageById(messageId);
await twonlyDatabase.messagesDao.deleteMessageById(messageId);
// answers with ok, so the server will delete the message
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
await twonlyDatabase.updateMessageByOtherUser(
await twonlyDatabase.messagesDao.updateMessageByOtherUser(
msg.contactId,
messageId,
MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
@ -168,7 +169,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (username.isSuccess) {
Uint8List name = username.value.userdata.username;
int added = await twonlyDatabase.insertContact(ContactsCompanion(
int added =
await twonlyDatabase.contactsDao.insertContact(ContactsCompanion(
username: Value(utf8.decode(name)),
userId: Value(fromUserId),
requested: Value(true),
@ -184,23 +186,23 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
break;
case MessageKind.opened:
final update = MessagesCompanion(openedAt: Value(message.timestamp));
await twonlyDatabase.updateMessageByOtherUser(
await twonlyDatabase.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageId!,
update,
);
break;
case MessageKind.rejectRequest:
await twonlyDatabase.deleteContactByUserId(fromUserId);
await twonlyDatabase.contactsDao.deleteContactByUserId(fromUserId);
break;
case MessageKind.acceptRequest:
final update = ContactsCompanion(accepted: Value(true));
twonlyDatabase.updateContact(fromUserId, update);
twonlyDatabase.contactsDao.updateContact(fromUserId, update);
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
break;
case MessageKind.ack:
final update = MessagesCompanion(acknowledgeByUser: Value(true));
await twonlyDatabase.updateMessageByOtherUser(
await twonlyDatabase.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageId!,
update,
@ -226,7 +228,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
sendAt: Value(message.timestamp),
);
final messageId = await twonlyDatabase.insertMessage(
final messageId = await twonlyDatabase.messagesDao.insertMessage(
update,
);
@ -246,7 +248,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
);
if (message.kind == MessageKind.media) {
twonlyDatabase.incFlameCounter(
twonlyDatabase.contactsDao.incFlameCounter(
fromUserId,
true,
message.timestamp,

View file

@ -1,72 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:twonly/src/model/identity_key_store_model.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';
import 'package:sqflite_sqlcipher/sqflite.dart';
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.
// So if someone dumps the app for example using a tool like Cellebrite including the apps
// content he is not able to access the databases content.
// (https://signal.org/blog/cellebrite-and-clickbait/)
//
// CHECK: Does this actually improve the security or is this just security through obscurity and
// can be removed as it does increase complexity?
// Current thoughts: As the database password is at some point in the memory the attacker could
// just dump it. Questions here: What capabilities must the attacker have to do that?
final storage = getSecureStorage();
var password = await storage.read(key: "sqflite_database_password");
if (password == null) {
var secureRandom = Random.secure();
password = "";
for (var i = 0; i < 32; i++) {
password = "$password${String.fromCharCode(secureRandom.nextInt(256))}";
}
await storage.write(key: "sqflite_database_password", value: password);
}
db = await openDatabase(path, password: password);
if (db != null) {
await setupDatabaseTable(db!);
}
}
Future<Database?> get ready async {
if (db == null) {
await open();
}
return db;
}
Future setupDatabaseTable(Database db) async {
await DbSignalSessionStore.setupDatabaseTable(db);
await DbSignalPreKeyStore.setupDatabaseTable(db);
await DbSignalSenderKeyStore.setupDatabaseTable(db);
await DbSignalIdentityKeyStore.setupDatabaseTable(db);
}
Future open() async {
await openPath(await fixPath(dbName));
}
Future remove() async {
await deleteDatabase(await fixPath(dbName));
// await _createDb(db!);
}
Future<String> fixPath(String path) async => path;
Future close() async {
await db!.close();
}
}

View file

@ -3,7 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/utils/misc.dart';

View file

@ -4,9 +4,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/model/json/message.dart' as my;
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/json_models/message.dart' as my;
/// Streams are created so that app can respond to notification-related events
/// since the plugin is initialized in the `main` function
@ -167,8 +167,9 @@ String getPushNotificationText(String key, String userName) {
Future localPushNotificationNewMessage(
int fromUserId, my.MessageJson message, int messageId) async {
Contact? user =
await twonlyDatabase.getContactByUserId(fromUserId).getSingleOrNull();
Contact? user = await twonlyDatabase.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) return;

View file

@ -1,14 +1,8 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/identity_key_store_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
bool eq<E>(List<E>? list1, List<E>? list2) =>
ListEquality<E>().equals(list1, list2);
// make it easier to read
typedef DB = DbSignalIdentityKeyStore;
import 'package:twonly/src/database/twonly_database.dart';
class ConnectIdentityKeyStore extends IdentityKeyStore {
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
@ -18,15 +12,15 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
var dbIdentityKey = (await dbProvider.db!.query(DB.tableName,
columns: [DB.columnIdentityKey],
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]));
if (dbIdentityKey.isEmpty) {
return null;
}
Uint8List identityKey = dbIdentityKey.first.cast()[DB.columnIdentityKey];
return IdentityKey.fromBytes(identityKey, 0);
SignalIdentityKeyStore? identity =
await (twonlyDatabase.select(twonlyDatabase.signalIdentityKeyStores)
..where((t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName())))
.getSingleOrNull();
if (identity == null) return null;
return IdentityKey.fromBytes(identity.identityKey, 0);
}
@override
@ -42,7 +36,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
if (identityKey == null) {
return false;
}
return trusted == null || eq(trusted.serialize(), identityKey.serialize());
return trusted == null ||
ListEquality().equals(trusted.serialize(), identityKey.serialize());
}
@override
@ -52,16 +47,23 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false;
}
if (await getIdentity(address) == null) {
await dbProvider.db!.insert(DB.tableName, {
DB.columnDeviceId: address.getDeviceId(),
DB.columnName: address.getName(),
DB.columnIdentityKey: identityKey.serialize()
});
await twonlyDatabase.into(twonlyDatabase.signalIdentityKeyStores).insert(
SignalIdentityKeyStoresCompanion(
deviceId: Value(address.getDeviceId()),
name: Value(address.getName()),
identityKey: Value(identityKey.serialize()),
),
);
} else {
await dbProvider.db!.update(
DB.tableName, {DB.columnIdentityKey: identityKey.serialize()},
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]);
await (twonlyDatabase.update(twonlyDatabase.signalIdentityKeyStores)
..where((t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName())))
.write(
SignalIdentityKeyStoresCompanion(
identityKey: Value(identityKey.serialize()),
),
);
}
return true;
}

View file

@ -1,49 +1,52 @@
import 'dart:typed_data';
import 'package:drift/drift.dart';
import 'package:logging/logging.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/pre_key_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
typedef DB = DbSignalPreKeyStore;
import 'package:twonly/src/database/twonly_database.dart';
class ConnectPreKeyStore extends PreKeyStore {
@override
Future<bool> containsPreKey(int preKeyId) async {
final dbPreKey = await dbProvider.db!.query(DB.tableName,
columns: [DB.columnPreKey],
where: '${DB.columnPreKeyId} = ?',
whereArgs: <Object?>[preKeyId]);
return dbPreKey.isNotEmpty;
final preKeyRecord =
await (twonlyDatabase.select(twonlyDatabase.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.get();
return preKeyRecord.isNotEmpty;
}
@override
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
final dbPreKey = await dbProvider.db!.query(DB.tableName,
columns: [DB.columnPreKey],
where: '${DB.columnPreKeyId} = ?',
whereArgs: <Object?>[preKeyId]);
if (dbPreKey.isEmpty) {
final preKeyRecord =
await (twonlyDatabase.select(twonlyDatabase.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.get();
if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException('No such preKey record! - $preKeyId');
}
Uint8List preKey = dbPreKey.first.cast()[DB.columnPreKey];
Uint8List preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey);
}
@override
Future<void> removePreKey(int preKeyId) async {
await dbProvider.db!.delete(DB.tableName,
where: '${DB.columnPreKeyId} = ?',
whereArgs: <Object?>[DB.columnPreKeyId]);
await (twonlyDatabase.delete(twonlyDatabase.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.go();
}
@override
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
if (!await containsPreKey(preKeyId)) {
await dbProvider.db!.insert(DB.tableName,
{DB.columnPreKeyId: preKeyId, DB.columnPreKey: record.serialize()});
} else {
await dbProvider.db!.update(
DB.tableName, {DB.columnPreKey: record.serialize()},
where: '${DB.columnPreKeyId} = ?', whereArgs: <Object?>[preKeyId]);
final preKeyCompanion = SignalPreKeyStoresCompanion(
preKeyId: Value(preKeyId),
preKey: Value(record.serialize()),
);
try {
await twonlyDatabase
.into(twonlyDatabase.signalPreKeyStores)
.insert(preKeyCompanion);
} catch (e) {
Logger("pre_key_store").shout("$e");
}
}
}

View file

@ -1,31 +1,30 @@
import 'dart:typed_data';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/sender_key_store_model.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
typedef DB = DbSignalSenderKeyStore;
class ConnectSenderKeyStore extends SenderKeyStore {
@override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final dbSenderKey = await dbProvider.db!.query(DB.tableName,
columns: [DB.columnSenderKey],
where: '${DB.columnSenderKeyName} = ?',
whereArgs: <Object?>[senderKeyName.serialize()]);
if (dbSenderKey.isEmpty) {
SignalSenderKeyStore? identity =
await (twonlyDatabase.select(twonlyDatabase.signalSenderKeyStores)
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
.getSingleOrNull();
if (identity == null) {
throw InvalidKeyIdException(
'No such sender key record! - $senderKeyName');
}
Uint8List preKey = dbSenderKey.first.cast()[DB.columnSenderKey];
return SenderKeyRecord.fromSerialized(preKey);
return SenderKeyRecord.fromSerialized(identity.senderKey);
}
@override
Future<void> storeSenderKey(
SenderKeyName senderKeyName, SenderKeyRecord record) async {
await dbProvider.db!.insert(DB.tableName, {
DB.columnSenderKeyName: senderKeyName.serialize(),
DB.columnSenderKey: record.serialize()
});
await twonlyDatabase.into(twonlyDatabase.signalSenderKeyStores).insert(
SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()),
senderKeyName: Value(senderKeyName.serialize()),
),
);
}
}

View file

@ -1,73 +1,81 @@
import 'dart:typed_data';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/session_store_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
// make it easier to read
typedef DB = DbSignalSessionStore;
import 'package:twonly/src/database/twonly_database.dart';
class ConnectSessionStore extends SessionStore {
ConnectSessionStore();
@override
Future<bool> containsSession(SignalProtocolAddress address) async {
var list = (await dbProvider.db!.query(DB.tableName,
columns: [],
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]));
return list.isNotEmpty;
final sessions =
await (twonlyDatabase.select(twonlyDatabase.signalSessionStores)
..where((tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName())))
.get();
return sessions.isNotEmpty;
}
@override
Future<void> deleteAllSessions(String name) async {
await dbProvider.db!.delete(DB.tableName,
where: '${DB.columnName} = ?', whereArgs: <Object?>[name]);
await (twonlyDatabase.delete(twonlyDatabase.signalSessionStores)
..where((tbl) => tbl.name.equals(name)))
.go();
}
@override
Future<void> deleteSession(SignalProtocolAddress address) async {
await dbProvider.db!.delete(DB.tableName,
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]);
await (twonlyDatabase.delete(twonlyDatabase.signalSessionStores)
..where((tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName())))
.go();
}
@override
Future<List<int>> getSubDeviceSessions(String name) async {
var deviceIds = (await dbProvider.db!.query(DB.tableName,
columns: [DB.columnDeviceId],
where: '${DB.columnDeviceId} != 1 AND ${DB.columnName} = ?',
whereArgs: <Object?>[name]));
return deviceIds.cast();
final deviceIds = await (twonlyDatabase
.select(twonlyDatabase.signalSessionStores)
..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name)))
.get();
return deviceIds.map((row) => row.deviceId).toList();
}
@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
var dbSession = (await dbProvider.db!.query(DB.tableName,
columns: [DB.columnSessionRecord],
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]));
final dbSession =
await (twonlyDatabase.select(twonlyDatabase.signalSessionStores)
..where((tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName())))
.get();
if (dbSession.isEmpty) {
return SessionRecord();
}
Uint8List session = dbSession.first.cast()[DB.columnSessionRecord];
Uint8List session = dbSession.first.sessionRecord;
return SessionRecord.fromSerialized(session);
}
@override
Future<void> storeSession(
SignalProtocolAddress address, SessionRecord record) async {
final sessionCompanion = SignalSessionStoresCompanion(
deviceId: Value(address.getDeviceId()),
name: Value(address.getName()),
sessionRecord: Value(record.serialize()),
);
if (!await containsSession(address)) {
await dbProvider.db!.insert(DB.tableName, {
DB.columnDeviceId: address.getDeviceId(),
DB.columnName: address.getName(),
DB.columnSessionRecord: record.serialize()
});
await twonlyDatabase
.into(twonlyDatabase.signalSessionStores)
.insert(sessionCompanion);
} else {
await dbProvider.db!.update(
DB.tableName, {DB.columnSessionRecord: record.serialize()},
where: '${DB.columnDeviceId} = ? AND ${DB.columnName} = ?',
whereArgs: <Object?>[address.getDeviceId(), address.getName()]);
await (twonlyDatabase.update(twonlyDatabase.signalSessionStores)
..where((tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName())))
.write(sessionCompanion);
}
}
}

View file

@ -10,8 +10,7 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:provider/provider.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -20,18 +19,6 @@ extension ShortCutsExtension on BuildContext {
TwonlyDatabase get db => Provider.of<TwonlyDatabase>(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

@ -3,9 +3,9 @@ import 'dart:io';
import 'dart:typed_data';
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/json_models/message.dart';
import 'package:twonly/src/json_models/signal_identity.dart';
import 'package:twonly/src/json_models/userdata.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';

View file

@ -1,8 +1,7 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/json_models/userdata.dart';
import 'package:twonly/src/utils/misc.dart';
Future<bool> isUserCreated() async {
@ -15,7 +14,7 @@ Future<bool> isUserCreated() async {
Future<UserData?> getUser() async {
final storage = getSecureStorage();
String? userJson = await storage.read(key: "user_data");
String? userJson = await storage.read(key: "userData");
if (userJson == null) {
return null;
}
@ -30,16 +29,11 @@ Future<UserData?> getUser() async {
}
Future<bool> deleteLocalUserData() async {
final storage = getSecureStorage();
var password = await storage.read(key: "sqflite_database_password");
await dbProvider.remove();
final appDir = await getApplicationSupportDirectory();
if (appDir.existsSync()) {
appDir.deleteSync(recursive: true);
}
await storage.write(key: "sqflite_database_password", value: password);
final storage = getSecureStorage();
await storage.deleteAll();
return true;
}

View file

@ -5,8 +5,8 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/components/image_editor/action_button.dart';
import 'package:twonly/src/components/media_view_sizing.dart';
import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart';
@ -55,8 +55,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Future updateAsync(int userId) async {
if (sendNextMediaToUserName != null) return;
Contact? contact =
await twonlyDatabase.getContactByUserId(userId).getSingleOrNull();
Contact? contact = await twonlyDatabase.contactsDao
.getContactByUserId(userId)
.getSingleOrNull();
if (contact != null) {
sendNextMediaToUserName = getContactDisplayName(contact);
}

View file

@ -10,8 +10,8 @@ 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/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart';
@ -54,7 +54,7 @@ class _ShareImageView extends State<ShareImageView> {
}
Stream<List<Contact>> allContacts =
twonlyDatabase.watchContactsForChatList();
twonlyDatabase.contactsDao.watchContactsForChatList();
contactSub = allContacts.listen((allContacts) {
setState(() {

View file

@ -9,10 +9,10 @@ import 'package:twonly/src/components/animate_icon.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/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
@ -158,8 +158,9 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
}
Future initStreams() async {
await twonlyDatabase.removeOldMessages();
Stream<Contact> contact = twonlyDatabase.watchContact(widget.userid);
await twonlyDatabase.messagesDao.removeOldMessages();
Stream<Contact> contact =
twonlyDatabase.contactsDao.watchContact(widget.userid);
userSub = contact.listen((contact) {
setState(() {
user = contact;
@ -167,7 +168,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
});
Stream<List<Message>> msgStream =
twonlyDatabase.watchAllMessagesFrom(widget.userid);
twonlyDatabase.messagesDao.watchAllMessagesFrom(widget.userid);
messageSub = msgStream.listen((msgs) {
if (!context.mounted) return;
var updated = false;
@ -181,7 +182,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
}
}
if (updated) {
twonlyDatabase.openedAllTextMessages(widget.userid);
twonlyDatabase.messagesDao.openedAllTextMessages(widget.userid);
} else {
// The stream should be get an update, so only update the UI when all are opened
setState(() {

View file

@ -9,10 +9,10 @@ import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart';
@ -50,7 +50,7 @@ class _ChatListViewState extends State<ChatListView> {
// title:
actions: [
StreamBuilder(
stream: twonlyDatabase.watchContactsRequested(),
stream: twonlyDatabase.contactsDao.watchContactsRequested(),
builder: (context, snapshot) {
var count = 0;
if (snapshot.hasData && snapshot.data != null) {
@ -86,7 +86,7 @@ class _ChatListViewState extends State<ChatListView> {
],
),
body: StreamBuilder(
stream: twonlyDatabase.watchContactsForChatList(),
stream: twonlyDatabase.contactsDao.watchContactsForChatList(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
@ -190,7 +190,8 @@ class _UserListItem extends State<UserListItem> {
child: ListTile(
title: Text(getContactDisplayName(widget.user)),
subtitle: StreamBuilder(
stream: twonlyDatabase.watchLastMessage(widget.user.userId),
stream:
twonlyDatabase.messagesDao.watchLastMessage(widget.user.userId),
builder: (context, lastMessageSnapshot) {
if (!lastMessageSnapshot.hasData) {
return Container();
@ -200,7 +201,8 @@ class _UserListItem extends State<UserListItem> {
}
final lastMessage = lastMessageSnapshot.data!.first;
return StreamBuilder(
stream: twonlyDatabase.watchMessageNotOpened(widget.user.userId),
stream: twonlyDatabase.messagesDao
.watchMessageNotOpened(widget.user.userId),
builder: (context, notOpenedMessagesSnapshot) {
if (!lastMessageSnapshot.hasData) {
return Container();

View file

@ -9,9 +9,9 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/animate_icon.dart';
import 'package:twonly/src/components/media_view_sizing.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/messages_db.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart';
@ -57,7 +57,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Future asyncLoadNextMedia(bool firstRun) async {
Stream<List<Message>> messages =
twonlyDatabase.watchMediaMessageNotOpened(widget.userId);
twonlyDatabase.messagesDao.watchMediaMessageNotOpened(widget.userId);
_subscription = messages.listen((messages) {
for (Message msg in messages) {

View file

@ -2,13 +2,13 @@ import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/json_models/message.dart';
import 'package:twonly/src/providers/api/api.dart';
// ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
@ -49,7 +49,8 @@ class _SearchUsernameView extends State<SearchUsernameView> {
return;
}
int added = await twonlyDatabase.insertContact(ContactsCompanion(
int added =
await twonlyDatabase.contactsDao.insertContact(ContactsCompanion(
username: Value(searchUserName.text),
userId: Value(res.value.userdata.userId.toInt()),
requested: Value(false),
@ -97,7 +98,8 @@ class _SearchUsernameView extends State<SearchUsernameView> {
);
}
Stream<List<Contact>> contacts = twonlyDatabase.watchNotAcceptedContacts();
Stream<List<Contact>> contacts =
twonlyDatabase.contactsDao.watchNotAcceptedContacts();
return Scaffold(
appBar: AppBar(
@ -195,8 +197,8 @@ class _ContactsListViewState extends State<ContactsListView> {
color: const Color.fromARGB(164, 244, 67, 54)),
onPressed: () async {
final update = ContactsCompanion(blocked: Value(true));
await twonlyDatabase.updateContact(
contact.userId, update);
await twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
},
),
),
@ -205,7 +207,7 @@ class _ContactsListViewState extends State<ContactsListView> {
child: IconButton(
icon: Icon(Icons.close, color: Colors.red),
onPressed: () async {
await twonlyDatabase
await twonlyDatabase.contactsDao
.deleteContactByUserId(contact.userId);
encryptAndSendMessage(
null,
@ -223,7 +225,8 @@ class _ContactsListViewState extends State<ContactsListView> {
icon: Icon(Icons.check, color: Colors.green),
onPressed: () async {
final update = ContactsCompanion(accepted: Value(true));
await twonlyDatabase.updateContact(contact.userId, update);
await twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
encryptAndSendMessage(
null,
contact.userId,

View file

@ -6,8 +6,8 @@ import 'package:qr_flutter/qr_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/format_long_string.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/signal.dart';
import 'package:url_launcher/url_launcher.dart';
@ -37,7 +37,7 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
@override
Widget build(BuildContext context) {
Stream<Contact?> contact = twonlyDatabase
Stream<Contact?> contact = twonlyDatabase.contactsDao
.getContactByUserId(widget.contact.userId)
.watchSingleOrNull();
@ -145,7 +145,8 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
onPressed: () {
final update =
ContactsCompanion(verified: Value(false));
twonlyDatabase.updateContact(contact.userId, update);
twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
},
label: Text(
context.lang.contactVerifyNumberClearVerification),
@ -155,7 +156,8 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
icon: FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () {
final update = ContactsCompanion(verified: Value(true));
twonlyDatabase.updateContact(contact.userId, update);
twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
},
label: Text(context.lang.contactVerifyNumberMarkAsVerified),
);

View file

@ -7,8 +7,8 @@ import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/verified_shield.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_verify_view.dart';
@ -24,8 +24,9 @@ class ContactView extends StatefulWidget {
class _ContactViewState extends State<ContactView> {
@override
Widget build(BuildContext context) {
Stream<Contact?> contact =
twonlyDatabase.getContactByUserId(widget.userId).watchSingleOrNull();
Stream<Contact?> contact = twonlyDatabase.contactsDao
.getContactByUserId(widget.userId)
.watchSingleOrNull();
return Scaffold(
appBar: AppBar(
@ -79,7 +80,8 @@ class _ContactViewState extends State<ContactView> {
if (context.mounted && nickName != null && nickName != "") {
final update = ContactsCompanion(nickName: Value(nickName));
twonlyDatabase.updateContact(contact.userId, update);
twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
}
},
),
@ -109,8 +111,8 @@ class _ContactViewState extends State<ContactView> {
if (block) {
final update = ContactsCompanion(blocked: Value(true));
if (context.mounted) {
await twonlyDatabase.updateContact(
contact.userId, update);
await twonlyDatabase.contactsDao
.updateContact(contact.userId, update);
}
if (context.mounted) {
Navigator.popUntil(context, (route) => route.isFirst);

View file

@ -5,7 +5,7 @@ import 'package:twonly/globals.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/json_models/userdata.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/signal.dart';
@ -43,7 +43,7 @@ class _RegisterViewState extends State<RegisterView> {
username: username,
displayName: username,
);
storage.write(key: "user_data", value: jsonEncode(userData));
storage.write(key: "userData", value: jsonEncode(userData));
}
setState(() {

View file

@ -27,7 +27,7 @@ class _PrivacyViewState extends State<PrivacyView> {
ListTile(
title: Text(context.lang.settingsPrivacyBlockUsers),
subtitle: StreamBuilder(
stream: twonlyDatabase.watchContactsBlocked(),
stream: twonlyDatabase.contactsDao.watchContactsBlocked(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Text(

View file

@ -2,8 +2,8 @@ import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/database/contacts_db.dart';
import 'package:twonly/src/database/database.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
class PrivacyViewBlockUsers extends StatefulWidget {
@ -21,7 +21,7 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
@override
void initState() {
super.initState();
allUsers = twonlyDatabase.watchAllContacts();
allUsers = twonlyDatabase.contactsDao.watchAllContacts();
loadAsync();
}
@ -105,7 +105,7 @@ class UserList extends StatelessWidget {
Future block(BuildContext context, int userId, bool? value) async {
if (value != null) {
final update = ContactsCompanion(blocked: Value(!value));
await twonlyDatabase.updateContact(userId, update);
await twonlyDatabase.contactsDao.updateContact(userId, update);
}
}

View file

@ -1,7 +1,7 @@
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/better_list_title.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/json_models/userdata.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';

View file

@ -265,14 +265,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.7.1"
cv:
dependency: "direct main"
description:
name: cv
sha256: b7ad2d39ebd8d0a610c69becbd9b0131c1b1544d42186fa025710f8ebff038a0
url: "https://pub.dev"
source: hosted
version: "1.1.3"
dart_style:
dependency: transitive
description:
@ -410,7 +402,7 @@ packages:
source: hosted
version: "3.10.2"
fixnum:
dependency: "direct main"
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
@ -422,14 +414,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_foreground_task:
dependency: "direct main"
description:
name: flutter_foreground_task
sha256: "206017ee1bf864f34b8d7bce664a172717caa21af8da23f55866470dfe316644"
url: "https://pub.dev"
source: hosted
version: "8.17.0"
flutter_image_compress:
dependency: "direct main"
description:
@ -669,14 +653,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
url: "https://pub.dev"
source: hosted
version: "6.2.1"
graphs:
dependency: transitive
description:
@ -1189,14 +1165,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
reorderables:
dependency: "direct main"
description:
name: reorderables
sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
restart_app:
dependency: "direct main"
description:
@ -1221,62 +1189,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
@ -1330,22 +1242,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_sqlcipher:
dependency: "direct main"
description:
name: sqflite_sqlcipher
sha256: "16033fde6c7d7bd657b71a2bc42332ab02bc8001c3212f502d2e02714e735ec9"
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
sqlite3:
dependency: transitive
description:
@ -1402,14 +1298,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
term_glyph:
dependency: transitive
description:

View file

@ -15,16 +15,13 @@ dependencies:
# path: ../CamerAwesome
collection: ^1.18.0
connectivity_plus: ^6.1.2
cv: ^1.1.3
drift: ^2.25.1
drift_flutter: ^0.2.4
exif: ^3.3.0
firebase_core: ^3.11.0
firebase_messaging: ^15.2.2
fixnum: ^1.1.1
flutter:
sdk: flutter
flutter_foreground_task: ^8.17.0
flutter_image_compress: ^2.4.0
flutter_local_notifications: ^18.0.1
flutter_localizations:
@ -32,7 +29,6 @@ dependencies:
flutter_secure_storage: ^10.0.0-beta.4
font_awesome_flutter: ^10.8.0
gal: ^2.3.1
google_fonts: ^6.2.1
hand_signature: ^3.0.3
hive: ^2.2.3
image: ^4.3.0
@ -54,10 +50,8 @@ dependencies:
cryptography_flutter_plus: ^2.3.2
provider: ^6.1.2
qr_flutter: ^4.1.0
reorderables: ^0.6.0
restart_app: ^1.3.2
screenshot: ^3.0.0
sqflite_sqlcipher: ^3.1.0+1
url_launcher: ^6.3.1
web_socket_channel: ^3.0.1