fixing multiple use-ability issues #343 #342
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2025-12-21 00:06:10 +01:00
parent 9fe55ab62d
commit 074ead8b4f
15 changed files with 6895 additions and 43 deletions

View file

@ -42,8 +42,15 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.getSingleOrNull(); .getSingleOrNull();
} }
Future<List<Contact>> getContactsByUsername(String username) async { Future<List<Contact>> getContactsByUsername(
return (select(contacts)..where((t) => t.username.equals(username))).get(); String username, {
String username2 = '_______',
}) async {
return (select(contacts)
..where(
(t) => t.username.equals(username) | t.username.equals(username2),
))
.get();
} }
Future<void> deleteContactByUserId(int userId) { Future<void> deleteContactByUserId(int userId) {
@ -58,7 +65,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.write(updatedValues); .write(updatedValues);
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
updatedValues.displayName.present || updatedValues.displayName.present ||
updatedValues.nickName.present) { updatedValues.nickName.present ||
updatedValues.username.present) {
final contact = await getContactByUserId(userId).getSingleOrNull(); final contact = await getContactByUserId(userId).getSingleOrNull();
if (contact != null) { if (contact != null) {
await updatePushUser(contact); await updatePushUser(contact);
@ -118,7 +126,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
Stream<List<Contact>> watchAllAcceptedContacts() { Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts) return (select(contacts)
..where((t) => t.blocked.equals(false) & t.accepted.equals(true))) ..where(
(t) =>
t.blocked.equals(false) &
t.accepted.equals(true) &
t.accountDeleted.equals(false),
))
.watch(); .watch();
} }

View file

@ -386,14 +386,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int getFlameCounterFromGroup(Group? group) { int getFlameCounterFromGroup(Group? group) {
if (group == null) return 0; if (group == null) return 0;
if (group.lastMessageSend == null || group.lastMessageReceived == null) { if (group.lastMessageSend == null ||
group.lastMessageReceived == null ||
group.lastFlameCounterChange == null) {
return 0; return 0;
} }
final now = DateTime.now(); final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day); final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
final oneDayAgo = startOfToday.subtract(const Duration(days: 1));
if (group.lastMessageSend!.isAfter(twoDaysAgo) && if (group.lastMessageSend!.isAfter(twoDaysAgo) &&
group.lastMessageReceived!.isAfter(twoDaysAgo)) { group.lastMessageReceived!.isAfter(twoDaysAgo) ||
group.lastFlameCounterChange!.isAfter(oneDayAgo)) {
return group.flameCounter + 1; return group.flameCounter + 1;
} else { } else {
return 0; return 0;

File diff suppressed because one or more lines are too long

View file

@ -90,8 +90,7 @@ class GroupHistories extends Table {
IntColumn get contactId => IntColumn get contactId =>
integer().nullable().references(Contacts, #userId)(); integer().nullable().references(Contacts, #userId)();
IntColumn get affectedContactId => IntColumn get affectedContactId => integer().nullable()();
integer().nullable().references(Contacts, #userId)();
TextColumn get oldGroupName => text().nullable()(); TextColumn get oldGroupName => text().nullable()();
TextColumn get newGroupName => text().nullable()(); TextColumn get newGroupName => text().nullable()();

View file

@ -67,7 +67,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 3; int get schemaVersion => 4;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -92,6 +92,17 @@ class TwonlyDB extends _$TwonlyDB {
from2To3: (m, schema) async { from2To3: (m, schema) async {
await m.addColumn(schema.groups, schema.groups.draftMessage); await m.addColumn(schema.groups, schema.groups.draftMessage);
}, },
from3To4: (m, schema) async {
await m.alterTable(
TableMigration(
schema.groupHistories,
columnTransformer: {
schema.groupHistories.affectedContactId:
schema.groupHistories.affectedContactId,
},
),
);
},
), ),
); );
} }

View file

@ -1566,9 +1566,390 @@ class Shape17 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_100(String aliasedName) => i1.GeneratedColumn<String> _column_100(String aliasedName) =>
i1.GeneratedColumn<String>('draft_message', aliasedName, true, i1.GeneratedColumn<String>('draft_message', aliasedName, true,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
final class Schema4 extends i0.VersionedSchema {
Schema4({required super.database}) : super(version: 4);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageActions,
groupHistories,
];
late final Shape0 contacts = Shape0(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 groups = Shape17(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id)',
],
columns: [
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_100,
_column_25,
_column_26,
_column_27,
_column_12,
_column_28,
_column_29,
_column_30,
_column_31,
_column_32,
_column_33,
_column_34,
_column_35,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 mediaFiles = Shape2(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(media_id)',
],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 messages = Shape3(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id)',
],
columns: [
_column_50,
_column_51,
_column_52,
_column_37,
_column_53,
_column_54,
_column_55,
_column_56,
_column_46,
_column_57,
_column_58,
_column_59,
_column_60,
_column_12,
_column_61,
_column_62,
_column_63,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 messageHistories = Shape4(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_64,
_column_65,
_column_66,
_column_53,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 reactions = Shape5(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, sender_id, emoji)',
],
columns: [
_column_65,
_column_67,
_column_68,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 groupMembers = Shape6(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id, contact_id)',
],
columns: [
_column_50,
_column_69,
_column_70,
_column_71,
_column_72,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 receipts = Shape7(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_74,
_column_75,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 receivedReceipts = Shape8(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 signalIdentityKeyStores = Shape9(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_83,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 signalPreKeyStores = Shape10(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_86,
_column_87,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 signalSessionStores = Shape12(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_88,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape13 signalContactPreKeys = Shape13(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_74,
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactSignedPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_74,
_column_89,
_column_90,
_column_91,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 messageActions = Shape15(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, contact_id, type)',
],
columns: [
_column_65,
_column_92,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 groupHistories = Shape16(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_history_id)',
],
columns: [
_column_94,
_column_50,
_column_95,
_column_101,
_column_97,
_column_98,
_column_99,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
}
i1.GeneratedColumn<int> _column_101(String aliasedName) =>
i1.GeneratedColumn<int>('affected_contact_id', aliasedName, true,
type: i1.DriftSqlType.int);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -1582,6 +1963,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema); await from2To3(migrator, schema);
return 3; return 3;
case 3:
final schema = Schema4(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -1591,9 +1977,11 @@ i0.MigrationStepWithVersion migrationSteps({
i1.OnUpgrade stepByStep({ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
from2To3: from2To3, from2To3: from2To3,
from3To4: from3To4,
)); ));

View file

@ -352,9 +352,8 @@ class ApiService {
if (contact != null) { if (contact != null) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
ContactsCompanion( const ContactsCompanion(
accountDeleted: const Value(true), accountDeleted: Value(true),
username: Value('${contact.username} (${contact.userId})'),
), ),
); );
} }
@ -496,7 +495,8 @@ class ApiService {
} }
Future<void> checkForDeletedUsernames() async { Future<void> checkForDeletedUsernames() async {
final users = await twonlyDB.contactsDao.getContactsByUsername('[deleted]'); final users = await twonlyDB.contactsDao
.getContactsByUsername('[deleted]', username2: '[Unknown]');
for (final user in users) { for (final user in users) {
final userData = await getUserById(user.userId); final userData = await getUserById(user.userId);
if (userData != null) { if (userData != null) {

View file

@ -117,6 +117,10 @@ Future<void> handleGroupUpdate(
final group = (await twonlyDB.groupsDao.getGroup(groupId))!; final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
switch (actionType) { switch (actionType) {
case GroupActionType.updatedGroupName: case GroupActionType.updatedGroupName:
await twonlyDB.groupsDao.insertGroupAction( await twonlyDB.groupsDao.insertGroupAction(
@ -173,10 +177,6 @@ Future<void> handleGroupUpdate(
case GroupActionType.createdGroup: case GroupActionType.createdGroup:
break; break;
} }
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
} }
Future<bool> handleGroupJoin( Future<bool> handleGroupJoin(

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -118,13 +119,17 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull() == .getSingleOrNull() ==
null) { null) {
final user = await apiService.getUserById(fromUserId);
/// In case the user does not exists, just create a dummy user which was deleted by the user, so the message /// In case the user does not exists, just create a dummy user which was deleted by the user, so the message
/// can be inserted into the receipts database /// can be inserted into the receipts database
await twonlyDB.contactsDao.insertContact( await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(
userId: Value(fromUserId), userId: Value(fromUserId),
deletedByUser: const Value(true), deletedByUser: const Value(true),
username: const Value('[deleted]'), username: Value(
user == null ? '[Unknown]' : utf8.decode(user.username),
),
), ),
); );
} }

View file

@ -820,15 +820,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
...widget.mainCameraController.scannedNewProfiles.values ...widget.mainCameraController.scannedNewProfiles.values
.map( .map(
(c) { (c) {
if (c.isLoading) return Container();
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
if (c.isLoading) return;
c.isLoading = true; c.isLoading = true;
widget.mainCameraController.setState(); widget.mainCameraController.setState();
await addNewContactFromPublicProfile(c.profile); await addNewContactFromPublicProfile(c.profile);
widget.mainCameraController.scannedNewProfiles
.remove(c.profile.userId.toInt());
widget.mainCameraController.setState();
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),

View file

@ -5,6 +5,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
@ -71,6 +72,9 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
); );
return; return;
} }
Log.info(
'Restoring flames from ${_directChat!.flameCounter} to ${_directChat!.maxFlameCounter}',
);
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
_groupId, _groupId,
GroupsCompanion( GroupsCompanion(

View file

@ -53,19 +53,19 @@ class OnboardingView extends StatelessWidget {
), ),
), ),
), ),
PageViewModel( // PageViewModel(
title: context.lang.onboardingSendTwonliesTitle, // title: context.lang.onboardingSendTwonliesTitle,
body: context.lang.onboardingSendTwonliesBody, // body: context.lang.onboardingSendTwonliesBody,
image: Center( // image: Center(
child: Padding( // child: Padding(
padding: const EdgeInsets.only(top: 100), // padding: const EdgeInsets.only(top: 100),
child: Lottie.asset( // child: Lottie.asset(
'assets/animations/twonlies.json', // 'assets/animations/twonlies.json',
repeat: false, // repeat: false,
), // ),
), // ),
), // ),
), // ),
PageViewModel( PageViewModel(
title: context.lang.onboardingNotProductTitle, title: context.lang.onboardingNotProductTitle,
bodyWidget: Column( bodyWidget: Column(

View file

@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/public_profile.view.dart';
import 'package:twonly/src/views/settings/account.view.dart'; import 'package:twonly/src/views/settings/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart';
@ -82,13 +83,22 @@ class _SettingsMainViewState extends State<SettingsMainView> {
), ),
), ),
), ),
// Align( Align(
// alignment: Alignment.centerRight, alignment: Alignment.centerRight,
// child: IconButton( child: IconButton(
// onPressed: () {}, onPressed: () {
// icon: FaIcon(FontAwesomeIcons.qrcode), Navigator.push(
// ), context,
// ) MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
},
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
)
], ],
), ),
), ),

View file

@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1; import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2; import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3; import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v2.DatabaseAtV2(db); return v2.DatabaseAtV2(db);
case 3: case 3:
return v3.DatabaseAtV3(db); return v3.DatabaseAtV3(db);
case 4:
return v4.DatabaseAtV4(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3]; static const versions = const [1, 2, 3, 4];
} }

File diff suppressed because it is too large Load diff