message and media sending does work

This commit is contained in:
otsmr 2025-10-26 01:14:46 +02:00
parent 4cb7f0ab01
commit 5ae943bcf3
54 changed files with 1712 additions and 429 deletions

View file

@ -1,9 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
@ -160,14 +157,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
_showDatabaseMigration = File(
join(
(await getApplicationSupportDirectory()).path,
'twonly_database.sqlite',
),
).existsSync();
_isUserCreated = await isUserCreated(); _isUserCreated = await isUserCreated();
if (_isUserCreated) {
if (gUser.appVersion < 62) {
_showDatabaseMigration = true;
}
}
setState(() { setState(() {
_isLoaded = true; _isLoaded = true;
}); });

View file

@ -1,6 +1,9 @@
import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -35,6 +38,15 @@ void main() async {
gCameras = await availableCameras(); gCameras = await availableCameras();
// try {
// File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite'))
// .deleteSync();
// } catch (e) {}
// await updateUserdata((u) {
// u.appVersion = 0;
// return u;
// });
apiService = ApiService(); apiService = ApiService();
twonlyDB = TwonlyDB(); twonlyDB = TwonlyDB();

View file

@ -1,6 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/database/twonly_database_old.dart' as old;
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
part 'contacts.dao.g.dart'; part 'contacts.dao.g.dart';
@ -111,6 +112,22 @@ String getContactDisplayName(Contact user) {
return name; return name;
} }
String getContactDisplayNameOld(old.Contact user) {
var name = user.username;
if (user.nickName != null && user.nickName != '') {
name = user.nickName!;
} else if (user.displayName != null) {
name = user.displayName!;
}
if (user.deleted) {
name = applyStrikethrough(name);
}
if (name.length > 12) {
return '${name.substring(0, 12)}...';
}
return name;
}
String applyStrikethrough(String text) { String applyStrikethrough(String text) {
return text.split('').map((char) => '$char\u0336').join(); return text.split('').map((char) => '$char\u0336').join();
} }

View file

@ -1,7 +1,10 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
part 'groups.dao.g.dart'; part 'groups.dao.g.dart';
@ -33,12 +36,46 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.get(); .get();
} }
Future<void> insertGroup(GroupsCompanion group) async { Future<Group?> createNewGroup(GroupsCompanion group) async {
await into(groups).insert( final insertGroup = group.copyWith(
group.copyWith(
groupId: Value(uuid.v4()), groupId: Value(uuid.v4()),
), isGroupAdmin: const Value(true),
); );
return _insertGroup(insertGroup);
}
Future<Group?> createNewDirectChat(
int contactId,
GroupsCompanion group,
) async {
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId);
final insertGroup = group.copyWith(
groupId: Value(groupIdDirectChat),
isDirectChat: const Value(true),
isGroupAdmin: const Value(true),
);
final result = await _insertGroup(insertGroup);
if (result != null) {
await into(groupMembers).insert(GroupMembersCompanion(
groupId: Value(result.groupId),
contactId: Value(
contactId,
),
));
}
return result;
}
Future<Group?> _insertGroup(GroupsCompanion group) async {
try {
final rowId = await into(groups).insert(group);
return await (select(groups)..where((t) => t.rowId.equals(rowId)))
.getSingle();
} catch (e) {
Log.error('Could not insert group: $e');
return null;
}
} }
Future<List<Contact>> getGroupContact(String groupId) async { Future<List<Contact>> getGroupContact(String groupId) async {

View file

@ -16,11 +16,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async { Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
try { try {
final rowId = await into(mediaFiles).insert( var insertMediaFile = mediaFile;
mediaFile.copyWith(
if (insertMediaFile.mediaId == const Value.absent()) {
insertMediaFile = mediaFile.copyWith(
mediaId: Value(uuid.v7()), mediaId: Value(uuid.v7()),
),
); );
}
final rowId = await into(mediaFiles).insert(insertMediaFile);
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId))) return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
.getSingle(); .getSingle();
@ -72,11 +76,22 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async { Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
return (select(mediaFiles) return (select(mediaFiles)
..where((t) => t.downloadState.equals(DownloadState.pending.name))) ..where(
(t) =>
t.downloadState.equals(DownloadState.pending.name) |
t.downloadState.equals(DownloadState.downloading.name),
))
.get(); .get();
} }
Stream<List<MediaFile>> watchAllStoredMediaFiles() { Stream<List<MediaFile>> watchAllStoredMediaFiles() {
return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch(); return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch();
} }
Stream<List<MediaFile>> watchNewestMediaFiles() {
return (select(mediaFiles)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(100))
.watch();
}
} }

View file

@ -38,24 +38,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Stream<List<Message>> watchMediaNotOpened(String groupId) { Stream<List<Message>> watchMediaNotOpened(String groupId) {
return (select(messages) final query = select(messages).join([
leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)),
])
..where( ..where(
(t) => mediaFiles.downloadState
t.openedAt.isNull() & .equals(DownloadState.reuploadRequested.name)
t.groupId.equals(groupId) & .not() &
t.senderId.isNotNull() & messages.openedAt.isNull() &
t.type.equals(MessageType.media.name), messages.groupId.equals(groupId) &
) messages.mediaId.isNotNull() &
..orderBy([(t) => OrderingTerm.asc(t.createdAt)])) messages.senderId.isNotNull() &
.watch(); messages.type.equals(MessageType.media.name),
);
return query.map((row) => row.readTable(messages)).watch();
} }
Stream<List<Message>> watchLastMessage(String groupId) { Stream<Message?> watchLastMessage(String groupId) {
return (select(messages) return (select(messages)
..where((t) => t.groupId.equals(groupId)) ..where((t) => t.groupId.equals(groupId))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(1)) ..limit(1))
.watch(); .watchSingleOrNull();
} }
Stream<List<Message>> watchByGroupId(String groupId) { Stream<List<Message>> watchByGroupId(String groupId) {
@ -64,6 +68,16 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.watch(); .watch();
} }
Stream<List<MessageAction>> watchMessageActionChanges(String messageId) {
return (select(messageActions)..where((t) => t.messageId.equals(messageId)))
.watch();
}
Stream<Message?> watchMessageById(String messageId) {
return (select(messages)..where((t) => t.messageId.equals(messageId)))
.watchSingleOrNull();
}
// Future<void> removeOldMessages() { // Future<void> removeOldMessages() {
// return (update(messages) // return (update(messages)
// ..where( // ..where(
@ -206,14 +220,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
await into(messageActions).insert( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
contactId: Value(contactId), contactId: Value(contactId),
type: const Value(MessageActionType.ackByUserAt), type: const Value(MessageActionType.openedAt),
actionAt: Value(timestamp), actionAt: Value(timestamp),
), ),
); );
if (await haveAllMembers(messageId, MessageActionType.openedAt)) {
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(openedAt: Value(DateTime.now())),
);
}
} }
Future<void> handleMessageAckByServer( Future<void> handleMessageAckByServer(
@ -221,7 +241,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
await into(messageActions).insert( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
contactId: Value(contactId), contactId: Value(contactId),
@ -229,14 +249,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
actionAt: Value(timestamp), actionAt: Value(timestamp),
), ),
); );
if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) {
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(ackByServer: Value(DateTime.now())),
);
}
} }
Future<bool> haveAllMembers( Future<bool> haveAllMembers(
String groupId,
String messageId, String messageId,
MessageActionType action, MessageActionType action,
) async { ) async {
final members = await twonlyDB.groupsDao.getGroupMembers(groupId); final message =
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
if (message == null) return true;
final members = await twonlyDB.groupsDao.getGroupMembers(message.groupId);
final actions = await (select(messageActions) final actions = await (select(messageActions)
..where( ..where(
@ -291,11 +319,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
Future<Message?> insertMessage(MessagesCompanion message) async { Future<Message?> insertMessage(MessagesCompanion message) async {
try { try {
final rowId = await into(messages).insert( var insertMessage = message;
message.copyWith(
if (message.messageId == const Value.absent()) {
insertMessage = message.copyWith(
messageId: Value(uuid.v7()), messageId: Value(uuid.v7()),
),
); );
}
final rowId = await into(messages).insert(insertMessage);
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
message.groupId.value, message.groupId.value,
@ -323,19 +355,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.getSingleOrNull(); .getSingleOrNull();
} }
Future<void> reopenedMedia(String messageId) async {
await (delete(messageActions)
..where(
(t) =>
t.messageId.equals(messageId) &
t.contactId.isNull() &
t.type.equals(
MessageActionType.openedAt.name,
),
))
.go();
}
// Future<void> deleteMessagesByContactId(int contactId) { // Future<void> deleteMessagesByContactId(int contactId) {
// return (delete(messages) // return (delete(messages)
// ..where( // ..where(

View file

@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart'; part 'receipts.dao.g.dart';
@DriftAccessor(tables: [Receipts, Messages, MessageActions]) @DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts])
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin { class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
@ -52,11 +52,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async { Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
try { try {
final id = await into(receipts).insert( var insertEntry = entry;
entry.copyWith( if (entry.receiptId == const Value.absent()) {
insertEntry = entry.copyWith(
receiptId: Value(uuid.v4()), receiptId: Value(uuid.v4()),
),
); );
}
final id = await into(receipts).insert(insertEntry);
return await (select(receipts)..where((t) => t.rowId.equals(id))) return await (select(receipts)..where((t) => t.rowId.equals(id)))
.getSingle(); .getSingle();
} catch (e) { } catch (e) {
@ -97,4 +99,26 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
await (update(receipts)..where((c) => c.receiptId.equals(receiptId))) await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
.write(updates); .write(updates);
} }
Future<bool> isDuplicated(String receiptId) async {
return await (select(receivedReceipts)
..where((t) => t.receiptId.equals(receiptId)))
.getSingleOrNull() !=
null;
// try {
// return await (select()
// ..where(
// (t) => t.receiptId.equals(receiptId),
// ))
// .getSingleOrNull();
// } catch (e) {
// Log.error(e);
// return null;
// }
}
Future<void> gotReceipt(String receiptId) async {
await into(receivedReceipts)
.insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
}
} }

View file

@ -10,4 +10,6 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor<TwonlyDB> {
$MessagesTable get messages => attachedDatabase.messages; $MessagesTable get messages => attachedDatabase.messages;
$ReceiptsTable get receipts => attachedDatabase.receipts; $ReceiptsTable get receipts => attachedDatabase.receipts;
$MessageActionsTable get messageActions => attachedDatabase.messageActions; $MessageActionsTable get messageActions => attachedDatabase.messageActions;
$ReceivedReceiptsTable get receivedReceipts =>
attachedDatabase.receivedReceipts;
} }

View file

@ -57,7 +57,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
tbl.preKeyId.equals(preKey.preKeyId), tbl.preKeyId.equals(preKey.preKeyId),
)) ))
.go(); .go();
Log.info('Using prekey ${preKey.preKeyId} for $contactId'); Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId');
return preKey; return preKey;
} }
return null; return null;
@ -68,6 +68,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
List<SignalContactPreKeysCompanion> preKeys, List<SignalContactPreKeysCompanion> preKeys,
) async { ) async {
for (final preKey in preKeys) { for (final preKey in preKeys) {
Log.info('[PREKEY] Inserting others ${preKey.preKeyId}');
try { try {
await into(signalContactPreKeys).insert(preKey); await into(signalContactPreKeys).insert(preKey);
} catch (e) { } catch (e) {

View file

@ -19,15 +19,17 @@ class ConnectPreKeyStore extends PreKeyStore {
..where((tbl) => tbl.preKeyId.equals(preKeyId))) ..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.get(); .get();
if (preKeyRecord.isEmpty) { if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException('No such preKey record! - $preKeyId'); throw InvalidKeyIdException(
'[PREKEY] No such preKey record! - $preKeyId');
} }
Log.info('Contact used preKey $preKeyId'); Log.info('[PREKEY] Contact used my preKey $preKeyId');
final preKey = preKeyRecord.first.preKey; final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey); return PreKeyRecord.fromBuffer(preKey);
} }
@override @override
Future<void> removePreKey(int preKeyId) async { Future<void> removePreKey(int preKeyId) async {
Log.info('[PREKEY] Removing $preKeyId from my own storage.');
await (twonlyDB.delete(twonlyDB.signalPreKeyStores) await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId))) ..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.go(); .go();
@ -40,6 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
preKey: Value(record.serialize()), preKey: Value(record.serialize()),
); );
Log.info('[PREKEY] Storing $preKeyId from my own storage.');
try { try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
} catch (e) { } catch (e) {

View file

@ -35,6 +35,8 @@ class Messages extends Table {
DateTimeColumn get openedAt => dateTime().nullable()(); DateTimeColumn get openedAt => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get modifiedAt => dateTime().nullable()(); DateTimeColumn get modifiedAt => dateTime().nullable()();
DateTimeColumn get ackByUser => dateTime().nullable()();
DateTimeColumn get ackByServer => dateTime().nullable()();
@override @override
Set<Column> get primaryKey => {messageId}; Set<Column> get primaryKey => {messageId};

View file

@ -30,3 +30,13 @@ class Receipts extends Table {
@override @override
Set<Column> get primaryKey => {receiptId}; Set<Column> get primaryKey => {receiptId};
} }
@DataClassName('ReceivedReceipt')
class ReceivedReceipts extends Table {
TextColumn get receiptId => text()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {receiptId};
}

View file

@ -36,6 +36,7 @@ part 'twonly.db.g.dart';
Groups, Groups,
GroupMembers, GroupMembers,
Receipts, Receipts,
ReceivedReceipts,
SignalIdentityKeyStores, SignalIdentityKeyStores,
SignalPreKeyStores, SignalPreKeyStores,
SignalSenderKeyStores, SignalSenderKeyStores,

View file

@ -2299,6 +2299,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>( late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>(
'modified_at', aliasedName, true, 'modified_at', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false); type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _ackByUserMeta =
const VerificationMeta('ackByUser');
@override
late final GeneratedColumn<DateTime> ackByUser = GeneratedColumn<DateTime>(
'ack_by_user', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _ackByServerMeta =
const VerificationMeta('ackByServer');
@override
late final GeneratedColumn<DateTime> ackByServer = GeneratedColumn<DateTime>(
'ack_by_server', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
groupId, groupId,
@ -2313,7 +2325,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
isDeletedFromSender, isDeletedFromSender,
openedAt, openedAt,
createdAt, createdAt,
modifiedAt modifiedAt,
ackByUser,
ackByServer
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@ -2387,6 +2401,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
modifiedAt.isAcceptableOrUnknown( modifiedAt.isAcceptableOrUnknown(
data['modified_at']!, _modifiedAtMeta)); data['modified_at']!, _modifiedAtMeta));
} }
if (data.containsKey('ack_by_user')) {
context.handle(
_ackByUserMeta,
ackByUser.isAcceptableOrUnknown(
data['ack_by_user']!, _ackByUserMeta));
}
if (data.containsKey('ack_by_server')) {
context.handle(
_ackByServerMeta,
ackByServer.isAcceptableOrUnknown(
data['ack_by_server']!, _ackByServerMeta));
}
return context; return context;
} }
@ -2422,6 +2448,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
modifiedAt: attachedDatabase.typeMapping modifiedAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']), .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']),
ackByUser: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_user']),
ackByServer: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server']),
); );
} }
@ -2448,6 +2478,8 @@ class Message extends DataClass implements Insertable<Message> {
final DateTime? openedAt; final DateTime? openedAt;
final DateTime createdAt; final DateTime createdAt;
final DateTime? modifiedAt; final DateTime? modifiedAt;
final DateTime? ackByUser;
final DateTime? ackByServer;
const Message( const Message(
{required this.groupId, {required this.groupId,
required this.messageId, required this.messageId,
@ -2461,7 +2493,9 @@ class Message extends DataClass implements Insertable<Message> {
required this.isDeletedFromSender, required this.isDeletedFromSender,
this.openedAt, this.openedAt,
required this.createdAt, required this.createdAt,
this.modifiedAt}); this.modifiedAt,
this.ackByUser,
this.ackByServer});
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
@ -2494,6 +2528,12 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || modifiedAt != null) { if (!nullToAbsent || modifiedAt != null) {
map['modified_at'] = Variable<DateTime>(modifiedAt); map['modified_at'] = Variable<DateTime>(modifiedAt);
} }
if (!nullToAbsent || ackByUser != null) {
map['ack_by_user'] = Variable<DateTime>(ackByUser);
}
if (!nullToAbsent || ackByServer != null) {
map['ack_by_server'] = Variable<DateTime>(ackByServer);
}
return map; return map;
} }
@ -2526,6 +2566,12 @@ class Message extends DataClass implements Insertable<Message> {
modifiedAt: modifiedAt == null && nullToAbsent modifiedAt: modifiedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(modifiedAt), : Value(modifiedAt),
ackByUser: ackByUser == null && nullToAbsent
? const Value.absent()
: Value(ackByUser),
ackByServer: ackByServer == null && nullToAbsent
? const Value.absent()
: Value(ackByServer),
); );
} }
@ -2548,6 +2594,8 @@ class Message extends DataClass implements Insertable<Message> {
openedAt: serializer.fromJson<DateTime?>(json['openedAt']), openedAt: serializer.fromJson<DateTime?>(json['openedAt']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']), modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']),
ackByUser: serializer.fromJson<DateTime?>(json['ackByUser']),
ackByServer: serializer.fromJson<DateTime?>(json['ackByServer']),
); );
} }
@override @override
@ -2568,6 +2616,8 @@ class Message extends DataClass implements Insertable<Message> {
'openedAt': serializer.toJson<DateTime?>(openedAt), 'openedAt': serializer.toJson<DateTime?>(openedAt),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'modifiedAt': serializer.toJson<DateTime?>(modifiedAt), 'modifiedAt': serializer.toJson<DateTime?>(modifiedAt),
'ackByUser': serializer.toJson<DateTime?>(ackByUser),
'ackByServer': serializer.toJson<DateTime?>(ackByServer),
}; };
} }
@ -2584,7 +2634,9 @@ class Message extends DataClass implements Insertable<Message> {
bool? isDeletedFromSender, bool? isDeletedFromSender,
Value<DateTime?> openedAt = const Value.absent(), Value<DateTime?> openedAt = const Value.absent(),
DateTime? createdAt, DateTime? createdAt,
Value<DateTime?> modifiedAt = const Value.absent()}) => Value<DateTime?> modifiedAt = const Value.absent(),
Value<DateTime?> ackByUser = const Value.absent(),
Value<DateTime?> ackByServer = const Value.absent()}) =>
Message( Message(
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
messageId: messageId ?? this.messageId, messageId: messageId ?? this.messageId,
@ -2602,6 +2654,8 @@ class Message extends DataClass implements Insertable<Message> {
openedAt: openedAt.present ? openedAt.value : this.openedAt, openedAt: openedAt.present ? openedAt.value : this.openedAt,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt, modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt,
ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser,
ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer,
); );
Message copyWithCompanion(MessagesCompanion data) { Message copyWithCompanion(MessagesCompanion data) {
return Message( return Message(
@ -2626,6 +2680,9 @@ class Message extends DataClass implements Insertable<Message> {
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
modifiedAt: modifiedAt:
data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt,
ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser,
ackByServer:
data.ackByServer.present ? data.ackByServer.value : this.ackByServer,
); );
} }
@ -2644,7 +2701,9 @@ class Message extends DataClass implements Insertable<Message> {
..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isDeletedFromSender: $isDeletedFromSender, ')
..write('openedAt: $openedAt, ') ..write('openedAt: $openedAt, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('modifiedAt: $modifiedAt') ..write('modifiedAt: $modifiedAt, ')
..write('ackByUser: $ackByUser, ')
..write('ackByServer: $ackByServer')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -2663,7 +2722,9 @@ class Message extends DataClass implements Insertable<Message> {
isDeletedFromSender, isDeletedFromSender,
openedAt, openedAt,
createdAt, createdAt,
modifiedAt); modifiedAt,
ackByUser,
ackByServer);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -2680,7 +2741,9 @@ class Message extends DataClass implements Insertable<Message> {
other.isDeletedFromSender == this.isDeletedFromSender && other.isDeletedFromSender == this.isDeletedFromSender &&
other.openedAt == this.openedAt && other.openedAt == this.openedAt &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt &&
other.modifiedAt == this.modifiedAt); other.modifiedAt == this.modifiedAt &&
other.ackByUser == this.ackByUser &&
other.ackByServer == this.ackByServer);
} }
class MessagesCompanion extends UpdateCompanion<Message> { class MessagesCompanion extends UpdateCompanion<Message> {
@ -2697,6 +2760,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<DateTime?> openedAt; final Value<DateTime?> openedAt;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<DateTime?> modifiedAt; final Value<DateTime?> modifiedAt;
final Value<DateTime?> ackByUser;
final Value<DateTime?> ackByServer;
final Value<int> rowid; final Value<int> rowid;
const MessagesCompanion({ const MessagesCompanion({
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
@ -2712,6 +2777,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.openedAt = const Value.absent(), this.openedAt = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.modifiedAt = const Value.absent(), this.modifiedAt = const Value.absent(),
this.ackByUser = const Value.absent(),
this.ackByServer = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
MessagesCompanion.insert({ MessagesCompanion.insert({
@ -2728,6 +2795,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.openedAt = const Value.absent(), this.openedAt = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.modifiedAt = const Value.absent(), this.modifiedAt = const Value.absent(),
this.ackByUser = const Value.absent(),
this.ackByServer = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : groupId = Value(groupId), }) : groupId = Value(groupId),
messageId = Value(messageId), messageId = Value(messageId),
@ -2746,6 +2815,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Expression<DateTime>? openedAt, Expression<DateTime>? openedAt,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<DateTime>? modifiedAt, Expression<DateTime>? modifiedAt,
Expression<DateTime>? ackByUser,
Expression<DateTime>? ackByServer,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
@ -2763,6 +2834,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (openedAt != null) 'opened_at': openedAt, if (openedAt != null) 'opened_at': openedAt,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (modifiedAt != null) 'modified_at': modifiedAt, if (modifiedAt != null) 'modified_at': modifiedAt,
if (ackByUser != null) 'ack_by_user': ackByUser,
if (ackByServer != null) 'ack_by_server': ackByServer,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@ -2781,6 +2854,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Value<DateTime?>? openedAt, Value<DateTime?>? openedAt,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<DateTime?>? modifiedAt, Value<DateTime?>? modifiedAt,
Value<DateTime?>? ackByUser,
Value<DateTime?>? ackByServer,
Value<int>? rowid}) { Value<int>? rowid}) {
return MessagesCompanion( return MessagesCompanion(
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
@ -2796,6 +2871,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
openedAt: openedAt ?? this.openedAt, openedAt: openedAt ?? this.openedAt,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
modifiedAt: modifiedAt ?? this.modifiedAt, modifiedAt: modifiedAt ?? this.modifiedAt,
ackByUser: ackByUser ?? this.ackByUser,
ackByServer: ackByServer ?? this.ackByServer,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@ -2843,6 +2920,12 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (modifiedAt.present) { if (modifiedAt.present) {
map['modified_at'] = Variable<DateTime>(modifiedAt.value); map['modified_at'] = Variable<DateTime>(modifiedAt.value);
} }
if (ackByUser.present) {
map['ack_by_user'] = Variable<DateTime>(ackByUser.value);
}
if (ackByServer.present) {
map['ack_by_server'] = Variable<DateTime>(ackByServer.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@ -2865,6 +2948,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('openedAt: $openedAt, ') ..write('openedAt: $openedAt, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('modifiedAt: $modifiedAt, ') ..write('modifiedAt: $modifiedAt, ')
..write('ackByUser: $ackByUser, ')
..write('ackByServer: $ackByServer, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@ -4243,6 +4328,200 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
} }
} }
class $ReceivedReceiptsTable extends ReceivedReceipts
with TableInfo<$ReceivedReceiptsTable, ReceivedReceipt> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$ReceivedReceiptsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _receiptIdMeta =
const VerificationMeta('receiptId');
@override
late final GeneratedColumn<String> receiptId = GeneratedColumn<String>(
'receipt_id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: currentDateAndTime);
@override
List<GeneratedColumn> get $columns => [receiptId, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'received_receipts';
@override
VerificationContext validateIntegrity(Insertable<ReceivedReceipt> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('receipt_id')) {
context.handle(_receiptIdMeta,
receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta));
} else if (isInserting) {
context.missing(_receiptIdMeta);
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {receiptId};
@override
ReceivedReceipt map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ReceivedReceipt(
receiptId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!,
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$ReceivedReceiptsTable createAlias(String alias) {
return $ReceivedReceiptsTable(attachedDatabase, alias);
}
}
class ReceivedReceipt extends DataClass implements Insertable<ReceivedReceipt> {
final String receiptId;
final DateTime createdAt;
const ReceivedReceipt({required this.receiptId, required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['receipt_id'] = Variable<String>(receiptId);
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) {
return ReceivedReceiptsCompanion(
receiptId: Value(receiptId),
createdAt: Value(createdAt),
);
}
factory ReceivedReceipt.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return ReceivedReceipt(
receiptId: serializer.fromJson<String>(json['receiptId']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'receiptId': serializer.toJson<String>(receiptId),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
ReceivedReceipt copyWith({String? receiptId, DateTime? createdAt}) =>
ReceivedReceipt(
receiptId: receiptId ?? this.receiptId,
createdAt: createdAt ?? this.createdAt,
);
ReceivedReceipt copyWithCompanion(ReceivedReceiptsCompanion data) {
return ReceivedReceipt(
receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('ReceivedReceipt(')
..write('receiptId: $receiptId, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(receiptId, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ReceivedReceipt &&
other.receiptId == this.receiptId &&
other.createdAt == this.createdAt);
}
class ReceivedReceiptsCompanion extends UpdateCompanion<ReceivedReceipt> {
final Value<String> receiptId;
final Value<DateTime> createdAt;
final Value<int> rowid;
const ReceivedReceiptsCompanion({
this.receiptId = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
});
ReceivedReceiptsCompanion.insert({
required String receiptId,
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
}) : receiptId = Value(receiptId);
static Insertable<ReceivedReceipt> custom({
Expression<String>? receiptId,
Expression<DateTime>? createdAt,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (receiptId != null) 'receipt_id': receiptId,
if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid,
});
}
ReceivedReceiptsCompanion copyWith(
{Value<String>? receiptId,
Value<DateTime>? createdAt,
Value<int>? rowid}) {
return ReceivedReceiptsCompanion(
receiptId: receiptId ?? this.receiptId,
createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (receiptId.present) {
map['receipt_id'] = Variable<String>(receiptId.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ReceivedReceiptsCompanion(')
..write('receiptId: $receiptId, ')
..write('createdAt: $createdAt, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores
with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> { with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> {
@override @override
@ -6130,6 +6409,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
late final $ReactionsTable reactions = $ReactionsTable(this); late final $ReactionsTable reactions = $ReactionsTable(this);
late final $GroupMembersTable groupMembers = $GroupMembersTable(this); late final $GroupMembersTable groupMembers = $GroupMembersTable(this);
late final $ReceiptsTable receipts = $ReceiptsTable(this); late final $ReceiptsTable receipts = $ReceiptsTable(this);
late final $ReceivedReceiptsTable receivedReceipts =
$ReceivedReceiptsTable(this);
late final $SignalIdentityKeyStoresTable signalIdentityKeyStores = late final $SignalIdentityKeyStoresTable signalIdentityKeyStores =
$SignalIdentityKeyStoresTable(this); $SignalIdentityKeyStoresTable(this);
late final $SignalPreKeyStoresTable signalPreKeyStores = late final $SignalPreKeyStoresTable signalPreKeyStores =
@ -6163,6 +6444,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
reactions, reactions,
groupMembers, groupMembers,
receipts, receipts,
receivedReceipts,
signalIdentityKeyStores, signalIdentityKeyStores,
signalPreKeyStores, signalPreKeyStores,
signalSenderKeyStores, signalSenderKeyStores,
@ -7854,6 +8136,8 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
Value<DateTime?> openedAt, Value<DateTime?> openedAt,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<DateTime?> modifiedAt, Value<DateTime?> modifiedAt,
Value<DateTime?> ackByUser,
Value<DateTime?> ackByServer,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
@ -7870,6 +8154,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<DateTime?> openedAt, Value<DateTime?> openedAt,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<DateTime?> modifiedAt, Value<DateTime?> modifiedAt,
Value<DateTime?> ackByUser,
Value<DateTime?> ackByServer,
Value<int> rowid, Value<int> rowid,
}); });
@ -8042,6 +8328,12 @@ class $$MessagesTableFilterComposer
ColumnFilters<DateTime> get modifiedAt => $composableBuilder( ColumnFilters<DateTime> get modifiedAt => $composableBuilder(
column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); column: $table.modifiedAt, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get ackByUser => $composableBuilder(
column: $table.ackByUser, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get ackByServer => $composableBuilder(
column: $table.ackByServer, builder: (column) => ColumnFilters(column));
$$GroupsTableFilterComposer get groupId { $$GroupsTableFilterComposer get groupId {
final $$GroupsTableFilterComposer composer = $composerBuilder( final $$GroupsTableFilterComposer composer = $composerBuilder(
composer: this, composer: this,
@ -8245,6 +8537,12 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<DateTime> get modifiedAt => $composableBuilder( ColumnOrderings<DateTime> get modifiedAt => $composableBuilder(
column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); column: $table.modifiedAt, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get ackByUser => $composableBuilder(
column: $table.ackByUser, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get ackByServer => $composableBuilder(
column: $table.ackByServer, builder: (column) => ColumnOrderings(column));
$$GroupsTableOrderingComposer get groupId { $$GroupsTableOrderingComposer get groupId {
final $$GroupsTableOrderingComposer composer = $composerBuilder( final $$GroupsTableOrderingComposer composer = $composerBuilder(
composer: this, composer: this,
@ -8362,6 +8660,12 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<DateTime> get modifiedAt => $composableBuilder( GeneratedColumn<DateTime> get modifiedAt => $composableBuilder(
column: $table.modifiedAt, builder: (column) => column); column: $table.modifiedAt, builder: (column) => column);
GeneratedColumn<DateTime> get ackByUser =>
$composableBuilder(column: $table.ackByUser, builder: (column) => column);
GeneratedColumn<DateTime> get ackByServer => $composableBuilder(
column: $table.ackByServer, builder: (column) => column);
$$GroupsTableAnnotationComposer get groupId { $$GroupsTableAnnotationComposer get groupId {
final $$GroupsTableAnnotationComposer composer = $composerBuilder( final $$GroupsTableAnnotationComposer composer = $composerBuilder(
composer: this, composer: this,
@ -8571,6 +8875,8 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<DateTime?> openedAt = const Value.absent(), Value<DateTime?> openedAt = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<DateTime?> modifiedAt = const Value.absent(), Value<DateTime?> modifiedAt = const Value.absent(),
Value<DateTime?> ackByUser = const Value.absent(),
Value<DateTime?> ackByServer = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
MessagesCompanion( MessagesCompanion(
@ -8587,6 +8893,8 @@ class $$MessagesTableTableManager extends RootTableManager<
openedAt: openedAt, openedAt: openedAt,
createdAt: createdAt, createdAt: createdAt,
modifiedAt: modifiedAt, modifiedAt: modifiedAt,
ackByUser: ackByUser,
ackByServer: ackByServer,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
@ -8603,6 +8911,8 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<DateTime?> openedAt = const Value.absent(), Value<DateTime?> openedAt = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<DateTime?> modifiedAt = const Value.absent(), Value<DateTime?> modifiedAt = const Value.absent(),
Value<DateTime?> ackByUser = const Value.absent(),
Value<DateTime?> ackByServer = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
MessagesCompanion.insert( MessagesCompanion.insert(
@ -8619,6 +8929,8 @@ class $$MessagesTableTableManager extends RootTableManager<
openedAt: openedAt, openedAt: openedAt,
createdAt: createdAt, createdAt: createdAt,
modifiedAt: modifiedAt, modifiedAt: modifiedAt,
ackByUser: ackByUser,
ackByServer: ackByServer,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
@ -10060,6 +10372,135 @@ typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager<
(Receipt, $$ReceiptsTableReferences), (Receipt, $$ReceiptsTableReferences),
Receipt, Receipt,
PrefetchHooks Function({bool contactId, bool messageId})>; PrefetchHooks Function({bool contactId, bool messageId})>;
typedef $$ReceivedReceiptsTableCreateCompanionBuilder
= ReceivedReceiptsCompanion Function({
required String receiptId,
Value<DateTime> createdAt,
Value<int> rowid,
});
typedef $$ReceivedReceiptsTableUpdateCompanionBuilder
= ReceivedReceiptsCompanion Function({
Value<String> receiptId,
Value<DateTime> createdAt,
Value<int> rowid,
});
class $$ReceivedReceiptsTableFilterComposer
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
$$ReceivedReceiptsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get receiptId => $composableBuilder(
column: $table.receiptId, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnFilters(column));
}
class $$ReceivedReceiptsTableOrderingComposer
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
$$ReceivedReceiptsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get receiptId => $composableBuilder(
column: $table.receiptId, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
}
class $$ReceivedReceiptsTableAnnotationComposer
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
$$ReceivedReceiptsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get receiptId =>
$composableBuilder(column: $table.receiptId, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
}
class $$ReceivedReceiptsTableTableManager extends RootTableManager<
_$TwonlyDB,
$ReceivedReceiptsTable,
ReceivedReceipt,
$$ReceivedReceiptsTableFilterComposer,
$$ReceivedReceiptsTableOrderingComposer,
$$ReceivedReceiptsTableAnnotationComposer,
$$ReceivedReceiptsTableCreateCompanionBuilder,
$$ReceivedReceiptsTableUpdateCompanionBuilder,
(
ReceivedReceipt,
BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt>
),
ReceivedReceipt,
PrefetchHooks Function()> {
$$ReceivedReceiptsTableTableManager(
_$TwonlyDB db, $ReceivedReceiptsTable table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$ReceivedReceiptsTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$ReceivedReceiptsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$ReceivedReceiptsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> receiptId = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ReceivedReceiptsCompanion(
receiptId: receiptId,
createdAt: createdAt,
rowid: rowid,
),
createCompanionCallback: ({
required String receiptId,
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ReceivedReceiptsCompanion.insert(
receiptId: receiptId,
createdAt: createdAt,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$ReceivedReceiptsTableProcessedTableManager = ProcessedTableManager<
_$TwonlyDB,
$ReceivedReceiptsTable,
ReceivedReceipt,
$$ReceivedReceiptsTableFilterComposer,
$$ReceivedReceiptsTableOrderingComposer,
$$ReceivedReceiptsTableAnnotationComposer,
$$ReceivedReceiptsTableCreateCompanionBuilder,
$$ReceivedReceiptsTableUpdateCompanionBuilder,
(
ReceivedReceipt,
BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt>
),
ReceivedReceipt,
PrefetchHooks Function()>;
typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder
= SignalIdentityKeyStoresCompanion Function({ = SignalIdentityKeyStoresCompanion Function({
required int deviceId, required int deviceId,
@ -11497,6 +11938,8 @@ class $TwonlyDBManager {
$$GroupMembersTableTableManager(_db, _db.groupMembers); $$GroupMembersTableTableManager(_db, _db.groupMembers);
$$ReceiptsTableTableManager get receipts => $$ReceiptsTableTableManager get receipts =>
$$ReceiptsTableTableManager(_db, _db.receipts); $$ReceiptsTableTableManager(_db, _db.receipts);
$$ReceivedReceiptsTableTableManager get receivedReceipts =>
$$ReceivedReceiptsTableTableManager(_db, _db.receivedReceipts);
$$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores => $$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores =>
$$SignalIdentityKeyStoresTableTableManager( $$SignalIdentityKeyStoresTableTableManager(
_db, _db.signalIdentityKeyStores); _db, _db.signalIdentityKeyStores);

View file

@ -22,6 +22,9 @@ class UserData {
String? avatarSvg; String? avatarSvg;
String? avatarJson; String? avatarJson;
@JsonKey(defaultValue: 0)
int appVersion = 0;
@JsonKey(defaultValue: 0) @JsonKey(defaultValue: 0)
int avatarCounter = 0; int avatarCounter = 0;

View file

@ -14,6 +14,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
) )
..avatarSvg = json['avatarSvg'] as String? ..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String? ..avatarJson = json['avatarJson'] as String?
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..isDeveloper = json['isDeveloper'] as bool? ?? false ..isDeveloper = json['isDeveloper'] as bool? ?? false
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
@ -77,6 +78,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'displayName': instance.displayName, 'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg, 'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson, 'avatarJson': instance.avatarJson,
'appVersion': instance.appVersion,
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'isDeveloper': instance.isDeveloper, 'isDeveloper': instance.isDeveloper,
'deviceId': instance.deviceId, 'deviceId': instance.deviceId,

View file

@ -19,6 +19,10 @@ class MemoryItem {
final mediaService = await MediaFileService.fromMediaId(message.mediaId!); final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
if (mediaService == null) continue; if (mediaService == null) continue;
if (!mediaService.imagePreviewAvailable) {
continue;
}
items items
.putIfAbsent( .putIfAbsent(
message.mediaId!, message.mediaId!,

View file

@ -388,7 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
factory EncryptedContent_MessageUpdate({ factory EncryptedContent_MessageUpdate({
EncryptedContent_MessageUpdate_Type? type, EncryptedContent_MessageUpdate_Type? type,
$core.String? senderMessageId, $core.String? senderMessageId,
$core.Iterable<$core.String>? multipleSenderMessageIds, $core.Iterable<$core.String>? multipleTargetMessageIds,
$core.String? text, $core.String? text,
$fixnum.Int64? timestamp, $fixnum.Int64? timestamp,
}) { }) {
@ -399,8 +399,8 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
if (senderMessageId != null) { if (senderMessageId != null) {
$result.senderMessageId = senderMessageId; $result.senderMessageId = senderMessageId;
} }
if (multipleSenderMessageIds != null) { if (multipleTargetMessageIds != null) {
$result.multipleSenderMessageIds.addAll(multipleSenderMessageIds); $result.multipleTargetMessageIds.addAll(multipleTargetMessageIds);
} }
if (text != null) { if (text != null) {
$result.text = text; $result.text = text;
@ -417,7 +417,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create)
..e<EncryptedContent_MessageUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values) ..e<EncryptedContent_MessageUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values)
..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId') ..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds') ..pPS(3, _omitFieldNames ? '' : 'multipleTargetMessageIds', protoName: 'multipleTargetMessageIds')
..aOS(4, _omitFieldNames ? '' : 'text') ..aOS(4, _omitFieldNames ? '' : 'text')
..aInt64(5, _omitFieldNames ? '' : 'timestamp') ..aInt64(5, _omitFieldNames ? '' : 'timestamp')
..hasRequiredFields = false ..hasRequiredFields = false
@ -463,7 +463,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
void clearSenderMessageId() => clearField(2); void clearSenderMessageId() => clearField(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.List<$core.String> get multipleSenderMessageIds => $_getList(2); $core.List<$core.String> get multipleTargetMessageIds => $_getList(2);
@$pb.TagNumber(4) @$pb.TagNumber(4)
$core.String get text => $_getSZ(3); $core.String get text => $_getSZ(3);
@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage { class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
factory EncryptedContent_MediaUpdate({ factory EncryptedContent_MediaUpdate({
EncryptedContent_MediaUpdate_Type? type, EncryptedContent_MediaUpdate_Type? type,
$core.String? targetMediaId, $core.String? targetMessageId,
}) { }) {
final $result = create(); final $result = create();
if (type != null) { if (type != null) {
$result.type = type; $result.type = type;
} }
if (targetMediaId != null) { if (targetMessageId != null) {
$result.targetMediaId = targetMediaId; $result.targetMessageId = targetMessageId;
} }
return $result; return $result;
} }
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create)
..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values) ..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values)
..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId') ..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
void clearType() => clearField(1); void clearType() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.String get targetMediaId => $_getSZ(1); $core.String get targetMessageId => $_getSZ(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set targetMediaId($core.String v) { $_setString(1, v); } set targetMessageId($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasTargetMediaId() => $_has(1); $core.bool hasTargetMessageId() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearTargetMediaId() => clearField(2); void clearTargetMessageId() => clearField(2);
} }
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage { class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {

View file

@ -158,7 +158,7 @@ const EncryptedContent_MessageUpdate$json = {
'2': [ '2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'}, {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'},
{'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true}, {'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true},
{'1': 'multipleSenderMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleSenderMessageIds'}, {'1': 'multipleTargetMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleTargetMessageIds'},
{'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true}, {'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true},
{'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'}, {'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
], ],
@ -221,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
'1': 'MediaUpdate', '1': 'MediaUpdate',
'2': [ '2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'}, {'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'},
{'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'}, {'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'},
], ],
'4': [EncryptedContent_MediaUpdate_Type$json], '4': [EncryptedContent_MediaUpdate_Type$json],
}; };
@ -338,8 +338,8 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3' 'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu' 'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3' 'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11' 'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11'
'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX' 'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ' 'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg' 'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu' '9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
@ -353,24 +353,24 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ' 'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX' 'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu' 'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR' 'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn' 'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn'
'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg' 'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA'
'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw' 'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK'
'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg' 'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG'
'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw' 'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa0gEKDUNvbnRh'
'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG' 'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS'
'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt' '5UeXBlUgR0eXBlEiEKCWF2YXRhclN2ZxgCIAEoCUgAUglhdmF0YXJTdmeIAQESJQoLZGlzcGxh'
'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE' 'eU5hbWUYAyABKAlIAVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVV'
'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB' 'BEQVRFEAFCDAoKX2F2YXRhclN2Z0IOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5'
'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA' 'cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SW'
'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo' 'QYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQY'
'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2' 'BCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQg'
'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl' 'gKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNv'
'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm' 'dW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgAS'
'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK' 'gDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmll'
'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg' 'bmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZX'
'5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk' 'JCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFj'
'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm' 'dFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCw'
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl'); 'oJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZQ==');

View file

@ -66,7 +66,7 @@ message EncryptedContent {
} }
Type type = 1; Type type = 1;
optional string senderMessageId = 2; optional string senderMessageId = 2;
repeated string multipleSenderMessageIds = 3; repeated string multipleTargetMessageIds = 3;
optional string text = 4; optional string text = 4;
int64 timestamp = 5; int64 timestamp = 5;
} }
@ -99,7 +99,7 @@ message EncryptedContent {
DECRYPTION_ERROR = 2; DECRYPTION_ERROR = 2;
} }
Type type = 1; Type type = 1;
string targetMediaId = 2; string targetMessageId = 2;
} }
message ContactRequest { message ContactRequest {

View file

@ -156,11 +156,6 @@ class ApiService {
} }
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
reconnectionTimer = null; reconnectionTimer = null;
final user = await getUser();
if (user != null) {
globalCallbackConnectionState(isConnected: true);
return false;
}
return lockConnecting.protect<bool>(() async { return lockConnecting.protect<bool>(() async {
if (_channel != null) { if (_channel != null) {
return true; return true;

View file

@ -95,6 +95,7 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
} }
if (failed) { if (failed) {
Log.error('Background media upload failed: ${update.status}');
await requestMediaReupload(mediaId); await requestMediaReupload(mediaId);
} else { } else {
await handleEncryptedFile(mediaId); await handleEncryptedFile(mediaId);
@ -194,6 +195,9 @@ Future<void> downloadFileFast(
if (response.statusCode == 404 || if (response.statusCode == 404 ||
response.statusCode == 403 || response.statusCode == 403 ||
response.statusCode == 400) { response.statusCode == 400) {
Log.error(
'Got ${response.statusCode} from server. Requesting upload again',
);
// Message was deleted from the server. Requesting it again from the sender to upload it again... // Message was deleted from the server. Requesting it again from the sender to upload it again...
await requestMediaReupload(media.mediaId); await requestMediaReupload(media.mediaId);
return; return;
@ -217,7 +221,7 @@ Future<void> requestMediaReupload(String mediaId) async {
EncryptedContent( EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate( mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMediaId: mediaId, targetMessageId: messages.first.messageId,
), ),
), ),
); );

View file

@ -58,6 +58,12 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
final mediaId = update.task.taskId.replaceAll('upload_', ''); final mediaId = update.task.taskId.replaceAll('upload_', '');
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
if (update.status == TaskStatus.enqueued ||
update.status == TaskStatus.running) {
// Ignore these updates
return;
}
if (media == null) { if (media == null) {
Log.error( Log.error(
'Got an upload task but no upload media in the media upload database', 'Got an upload task but no upload media in the media upload database',
@ -115,7 +121,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
final mediaService = await MediaFileService.fromMedia(media); final mediaService = await MediaFileService.fromMedia(media);
await mediaService.setUploadState(UploadState.uploading); await mediaService.setUploadState(UploadState.uploaded);
// In all other cases just try the upload again... // In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService); await startBackgroundMediaUpload(mediaService);
} }

View file

@ -6,9 +6,11 @@ import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
@ -47,6 +49,7 @@ Future<void> insertMediaFileInMessagesTable(
MessagesCompanion( MessagesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaService.mediaFile.mediaId), mediaId: Value(mediaService.mediaFile.mediaId),
type: const Value(MessageType.media),
), ),
); );
if (message != null) { if (message != null) {
@ -147,12 +150,13 @@ Future<void> _createUploadRequest(MediaFileService media) async {
} }
final notEncryptedContent = EncryptedContent( final notEncryptedContent = EncryptedContent(
groupId: message.groupId,
media: EncryptedContent_Media( media: EncryptedContent_Media(
senderMessageId: message.messageId, senderMessageId: message.messageId,
type: type, type: type,
requiresAuthentication: media.mediaFile.requiresAuthentication, requiresAuthentication: media.mediaFile.requiresAuthentication,
timestamp: Int64(message.createdAt.millisecondsSinceEpoch), timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
downloadToken: media.mediaFile.downloadToken, downloadToken: downloadToken.toList(),
encryptionKey: media.mediaFile.encryptionKey, encryptionKey: media.mediaFile.encryptionKey,
encryptionNonce: media.mediaFile.encryptionNonce, encryptionNonce: media.mediaFile.encryptionNonce,
encryptionMac: media.mediaFile.encryptionMac, encryptionMac: media.mediaFile.encryptionMac,
@ -202,12 +206,25 @@ Future<void> _createUploadRequest(MediaFileService media) async {
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
} }
Mutex protectUpload = Mutex();
Future<void> _uploadUploadRequest(MediaFileService media) async { Future<void> _uploadUploadRequest(MediaFileService media) async {
await protectUpload.protect(() async {
final currentMedia =
await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId);
if (currentMedia == null ||
currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) {
Log.info('Download for ${media.mediaFile.mediaId} already started.');
return null;
}
final apiAuthTokenRaw = await const FlutterSecureStorage() final apiAuthTokenRaw = await const FlutterSecureStorage()
.read(key: SecureStorageKeys.apiAuthToken); .read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {
Log.error('api auth token not defined.'); Log.error('api auth token not defined.');
return; return null;
} }
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
@ -234,4 +251,5 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
await media.setUploadState(UploadState.backgroundUploadTaskStarted); await media.setUploadState(UploadState.backgroundUploadTaskStarted);
});
} }

View file

@ -4,6 +4,7 @@ import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
@ -35,7 +36,6 @@ Future<void> tryTransmitMessages() async {
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
String? receiptId, String? receiptId,
Receipt? receipt, Receipt? receipt,
bool reupload = false,
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
}) async { }) async {
try { try {
@ -49,15 +49,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
} }
receiptId = receipt.receiptId; receiptId = receipt.receiptId;
if (reupload) {
await twonlyDB.receiptsDao.updateReceipt(
receiptId,
const ReceiptsCompanion(
ackByServerAt: Value(null),
),
);
}
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
Log.error('$receiptId message already uploaded!'); Log.error('$receiptId message already uploaded!');
return null; return null;
@ -71,12 +62,18 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
final encryptedContent = final encryptedContent =
pb.EncryptedContent.fromBuffer(message.encryptedContent); pb.EncryptedContent.fromBuffer(message.encryptedContent);
var pushData = await getPushDataFromEncryptedContent( final pushNotification = await getPushNotificationFromEncryptedContent(
receipt.contactId, receipt.contactId,
receipt.messageId, receipt.messageId,
encryptedContent, encryptedContent,
); );
Uint8List? pushData;
if (pushNotification != null) {
pushData =
await encryptPushNotification(receipt.contactId, pushNotification);
}
if (message.type == pb.Message_Type.TEST_NOTIFICATION) { if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
pushData = (PushNotification()..kind = PushKind.testNotification) pushData = (PushNotification()..kind = PushKind.testNotification)
.writeToBuffer(); .writeToBuffer();
@ -167,6 +164,7 @@ Future<void> insertAndSendTextMessage(
MessagesCompanion( MessagesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
content: Value(textMessage), content: Value(textMessage),
type: const Value(MessageType.text),
quotesMessageId: Value(quotesMessageId), quotesMessageId: Value(quotesMessageId),
), ),
); );
@ -187,26 +185,24 @@ Future<void> insertAndSendTextMessage(
encryptedContent.textMessage.quoteMessageId = quotesMessageId; encryptedContent.textMessage.quoteMessageId = quotesMessageId;
} }
await sendCipherTextToGroup(groupId, encryptedContent); await sendCipherTextToGroup(groupId, encryptedContent, message.messageId);
} }
Future<void> sendCipherTextToGroup( Future<void> sendCipherTextToGroup(
String groupId, String groupId,
pb.EncryptedContent encryptedContent, pb.EncryptedContent encryptedContent,
String? messageId,
) async { ) async {
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId); final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) return;
encryptedContent encryptedContent.groupId = groupId;
..groupId = groupId
..isDirectChat = group.isDirectChat;
for (final groupMember in groupMembers) { for (final groupMember in groupMembers) {
unawaited( unawaited(
sendCipherText( sendCipherText(
groupMember.contactId, groupMember.contactId,
encryptedContent, encryptedContent,
messageId: messageId,
), ),
); );
} }
@ -216,6 +212,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
int contactId, int contactId,
pb.EncryptedContent encryptedContent, { pb.EncryptedContent encryptedContent, {
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
String? messageId,
}) async { }) async {
final response = pb.Message() final response = pb.Message()
..type = pb.Message_Type.CIPHERTEXT ..type = pb.Message_Type.CIPHERTEXT
@ -225,6 +222,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
ReceiptsCompanion( ReceiptsCompanion(
contactId: Value(contactId), contactId: Value(contactId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
messageId: Value(messageId),
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
), ),
); );
@ -249,15 +247,23 @@ Future<void> notifyContactAboutOpeningMessage(
biggestMessageId = messageOtherId; biggestMessageId = messageOtherId;
} }
} }
Log.info('Opened messages: $messageOtherIds');
await sendCipherText( await sendCipherText(
contactId, contactId,
pb.EncryptedContent( pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate( messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.OPENED, type: pb.EncryptedContent_MessageUpdate_Type.OPENED,
multipleSenderMessageIds: messageOtherIds, multipleTargetMessageIds: messageOtherIds,
), ),
), ),
); );
for (final messageId in messageOtherIds) {
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(openedAt: Value(DateTime.now())),
);
}
await updateLastMessageId(contactId, biggestMessageId); await updateLastMessageId(contactId, biggestMessageId);
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/database/twonly.db.dart' hide Message;
@ -50,10 +51,20 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
Mutex protectReceiptCheck = Mutex();
Future<void> handleNewMessage(int fromUserId, Uint8List body) async { Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
final message = Message.fromBuffer(body); final message = Message.fromBuffer(body);
final receiptId = message.receiptId; final receiptId = message.receiptId;
await protectReceiptCheck.protect(() async {
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
Log.error('Got duplicated message from the server. Ignoring it.');
return;
}
await twonlyDB.receiptsDao.gotReceipt(receiptId);
});
switch (message.type) { switch (message.type) {
case Message_Type.SENDER_DELIVERY_RECEIPT: case Message_Type.SENDER_DELIVERY_RECEIPT:
Log.info('Got delivery receipt for $receiptId!'); Log.info('Got delivery receipt for $receiptId!');
@ -65,7 +76,15 @@ Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
Log.info( Log.info(
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId', 'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
); );
await tryToSendCompleteMessage(receiptId: receiptId, reupload: true); final newReceiptId = uuid.v4();
await twonlyDB.receiptsDao.updateReceipt(
receiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
ackByServerAt: const Value(null),
),
);
await tryToSendCompleteMessage(receiptId: newReceiptId);
} }
case Message_Type.CIPHERTEXT: case Message_Type.CIPHERTEXT:
@ -112,7 +131,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
final (content, decryptionErrorType) = await signalDecryptMessage( final (content, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
messageType as int, messageType.value,
); );
if (content == null) { if (content == null) {
@ -147,7 +166,24 @@ Future<PlaintextContent?> handleEncryptedMessage(
return null; return null;
} }
if (content.hasMessageUpdate()) {
await handleMessageUpdate(
fromUserId,
content.messageUpdate,
);
return null;
}
if (content.hasMediaUpdate()) {
await handleMediaUpdate(
fromUserId,
content.mediaUpdate,
);
return null;
}
if (!content.hasGroupId()) { if (!content.hasGroupId()) {
Log.error('Messages should have a groupId $fromUserId.');
return null; return null;
} }
@ -157,14 +193,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
return null; return null;
} }
if (content.hasMessageUpdate()) {
await handleMessageUpdate(
fromUserId,
content.messageUpdate,
);
return null;
}
if (content.hasTextMessage()) { if (content.hasTextMessage()) {
await handleTextMessage( await handleTextMessage(
fromUserId, fromUserId,
@ -192,14 +220,5 @@ Future<PlaintextContent?> handleEncryptedMessage(
return null; return null;
} }
if (content.hasMediaUpdate()) {
await handleMediaUpdate(
fromUserId,
content.groupId,
content.mediaUpdate,
);
return null;
}
return null; return null;
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
@ -92,6 +93,7 @@ Future<void> handleMedia(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
mediaId: Value(mediaFile.mediaId), mediaId: Value(mediaFile.mediaId),
type: const Value(MessageType.media),
quotesMessageId: Value( quotesMessageId: Value(
media.hasQuoteMessageId() ? media.quoteMessageId : null, media.hasQuoteMessageId() ? media.quoteMessageId : null,
), ),
@ -112,16 +114,17 @@ Future<void> handleMedia(
Future<void> handleMediaUpdate( Future<void> handleMediaUpdate(
int fromUserId, int fromUserId,
String groupId,
EncryptedContent_MediaUpdate mediaUpdate, EncryptedContent_MediaUpdate mediaUpdate,
) async { ) async {
final messages = await twonlyDB.messagesDao final message = await twonlyDB.messagesDao
.getMessagesByMediaId(mediaUpdate.targetMediaId); .getMessageById(mediaUpdate.targetMessageId)
if (messages.length != 1) return; .getSingleOrNull();
final message = messages.first; if (message == null) {
if (message.senderId != fromUserId) return; Log.error(
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.');
}
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!);
if (mediaFile == null) { if (mediaFile == null) {
Log.info( Log.info(
'Got media file update, but media file was not found ${message.mediaId}', 'Got media file update, but media file was not found ${message.mediaId}',
@ -140,15 +143,8 @@ Future<void> handleMediaUpdate(
); );
case EncryptedContent_MediaUpdate_Type.STORED: case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('Got media file stored ${mediaFile.mediaId}'); Log.info('Got media file stored ${mediaFile.mediaId}');
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
stored: Value(true),
),
);
final mediaService = await MediaFileService.fromMedia(mediaFile); final mediaService = await MediaFileService.fromMedia(mediaFile);
unawaited(mediaService.createThumbnail()); await mediaService.storeMediaFile();
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}'); Log.info('Got media file decryption error ${mediaFile.mediaId}');

View file

@ -9,17 +9,20 @@ Future<void> handleMessageUpdate(
) async { ) async {
switch (messageUpdate.type) { switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED: case EncryptedContent_MessageUpdate_Type.OPENED:
for (final targetMessageId in messageUpdate.multipleTargetMessageIds) {
Log.info( Log.info(
'Opened message ${messageUpdate.multipleSenderMessageIds.length}', 'Opened message $targetMessageId',
); );
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
await twonlyDB.messagesDao.handleMessageOpened( await twonlyDB.messagesDao.handleMessageOpened(
contactId, contactId,
senderMessageId, targetMessageId,
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
} }
case EncryptedContent_MessageUpdate_Type.DELETE: case EncryptedContent_MessageUpdate_Type.DELETE:
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
return;
}
Log.info('Delete message ${messageUpdate.senderMessageId}'); Log.info('Delete message ${messageUpdate.senderMessageId}');
await twonlyDB.messagesDao.handleMessageDeletion( await twonlyDB.messagesDao.handleMessageDeletion(
contactId, contactId,
@ -27,6 +30,9 @@ Future<void> handleMessageUpdate(
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
return;
}
Log.info('Edit message ${messageUpdate.senderMessageId}'); Log.info('Edit message ${messageUpdate.senderMessageId}');
await twonlyDB.messagesDao.handleTextEdit( await twonlyDB.messagesDao.handleTextEdit(
contactId, contactId,
@ -36,3 +42,14 @@ Future<void> handleMessageUpdate(
); );
} }
} }
Future<bool> isSender(int fromUserId, String messageId) async {
final message =
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
if (message == null) return false;
if (message.senderId == fromUserId) {
return true;
}
Log.error('Contact $fromUserId tried to modify the message $messageId');
return false;
}

View file

@ -1,5 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
@ -20,6 +21,7 @@ Future<void> handleTextMessage(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
content: Value(textMessage.text), content: Value(textMessage.text),
type: const Value(MessageType.text),
quotesMessageId: Value( quotesMessageId: Value(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
), ),

View file

@ -26,7 +26,7 @@ class Result<T, E> {
} }
DateTime fromTimestamp(Int64 timeStamp) { DateTime fromTimestamp(Int64 timeStamp) {
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt() * 1000); return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt());
} }
// ignore: strict_raw_type // ignore: strict_raw_type
@ -88,7 +88,7 @@ Future<void> handleMediaError(MediaFile media) async {
EncryptedContent( EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate( mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMediaId: message.mediaId, targetMessageId: message.messageId,
), ),
), ),
); );

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -129,6 +130,9 @@ class MediaFileService {
} }
} }
bool get imagePreviewAvailable =>
thumbnailPath.existsSync() || storedPath.existsSync();
Future<void> storeMediaFile() async { Future<void> storeMediaFile() async {
Log.info('Storing media file ${mediaFile.mediaId}'); Log.info('Storing media file ${mediaFile.mediaId}');
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
@ -137,7 +141,25 @@ class MediaFileService {
stored: Value(true), stored: Value(true),
), ),
); );
await twonlyDB.messagesDao.updateMessagesByMediaId(
mediaFile.mediaId,
const MessagesCompanion(
mediaStored: Value(true),
),
);
if (originalPath.existsSync()) {
await originalPath.copy(tempPath.path);
await compressMedia();
}
if (tempPath.existsSync()) {
await tempPath.copy(storedPath.path); await tempPath.copy(storedPath.path);
} else {
Log.error(
'Could not store image neither tempPath nor originalPath exists.',
);
}
unawaited(createThumbnail());
await updateFromDB(); await updateFromDB();
} }

View file

@ -195,12 +195,12 @@ Future<void> updateLastMessageId(int fromUserId, String messageId) async {
} }
} }
Future<Uint8List?> getPushDataFromEncryptedContent( Future<PushNotification?> getPushNotificationFromEncryptedContent(
int toUserId, int toUserId,
String? messageId, String? messageId,
EncryptedContent content, EncryptedContent content,
) async { ) async {
late PushKind kind; PushKind? kind;
String? reactionContent; String? reactionContent;
if (content.hasReaction()) { if (content.hasReaction()) {
@ -270,6 +270,8 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
} }
} }
if (kind == null) return null;
final pushNotification = PushNotification()..kind = kind; final pushNotification = PushNotification()..kind = kind;
if (reactionContent != null) { if (reactionContent != null) {
pushNotification.reactionContent = reactionContent; pushNotification.reactionContent = reactionContent;
@ -277,7 +279,7 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
if (messageId != null) { if (messageId != null) {
pushNotification.messageId = messageId; pushNotification.messageId = messageId;
} }
return encryptPushNotification(toUserId, pushNotification); return pushNotification;
} }
/// this will trigger a push notification /// this will trigger a push notification

View file

@ -31,13 +31,13 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
.isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { .isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) {
return; return;
} }
Log.info('Requesting new PREKEYS for $contactId'); Log.info('[PREKEY] Requesting new PREKEYS for $contactId');
lastPreKeyRequest = DateTime.now(); lastPreKeyRequest = DateTime.now();
await requestNewKeys.protect(() async { await requestNewKeys.protect(() async {
final otherKeys = await apiService.getPreKeysByUserId(contactId); final otherKeys = await apiService.getPreKeysByUserId(contactId);
if (otherKeys != null) { if (otherKeys != null) {
Log.info( Log.info(
'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!', '[PREKEY] Got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!',
); );
final preKeys = otherKeys.preKeys final preKeys = otherKeys.preKeys
.map( .map(
@ -50,7 +50,8 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
.toList(); .toList();
await twonlyDB.signalDao.insertPreKeys(preKeys); await twonlyDB.signalDao.insertPreKeys(preKeys);
} else { } else {
Log.error('could not load new pre keys for user $contactId'); // 104400
Log.error('[PREKEY] Could not load new pre keys for user $contactId');
} }
}); });
} }

View file

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -246,11 +247,14 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
} }
bool isUUIDNewer(String uuid1, String uuid2) { bool isUUIDNewer(String uuid1, String uuid2) {
try {
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
print(timestamp1);
print(timestamp2);
return timestamp1 > timestamp2; return timestamp1 > timestamp2;
} catch (e) {
Log.error(e);
return true;
}
} }
String uint8ListToHex(List<int> bytes) { String uint8ListToHex(List<int> bytes) {
@ -313,3 +317,34 @@ Color getMessageColorFromType(
} }
return color; return color;
} }
String getUUIDforDirectChat(int a, int b) {
if (a < 0 || b < 0) {
throw ArgumentError('Inputs must be non-negative integers.');
}
if (a > integerMax || b > integerMax) {
throw ArgumentError('Inputs must be <= 0x7fffffff.');
}
// Mask to 64 bits in case inputs exceed 64 bits
final mask64 = (BigInt.one << 64) - BigInt.one;
final ai = BigInt.from(a) & mask64;
final bi = BigInt.from(b) & mask64;
// Ensure the bigger integer is in front (high 64 bits)
final hi = ai >= bi ? ai : bi;
final lo = ai >= bi ? bi : ai;
final combined = (hi << 64) | lo;
final hex = combined.toRadixString(16).padLeft(32, '0');
final parts = [
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20, 32),
];
return parts.join('-');
}

View file

@ -1,20 +1,20 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
const SaveToGalleryButton({ const SaveToGalleryButton({
required this.getMergedImage, required this.storeImageAsOriginal,
required this.isLoading, required this.isLoading,
required this.displayButtonLabel, required this.displayButtonLabel,
required this.mediaService, required this.mediaService,
super.key, super.key,
}); });
final Future<Uint8List?> Function() getMergedImage; final Future<bool> Function() storeImageAsOriginal;
final bool displayButtonLabel; final bool displayButtonLabel;
final MediaFileService mediaService; final MediaFileService mediaService;
final bool isLoading; final bool isLoading;
@ -45,6 +45,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
_imageSaving = true; _imageSaving = true;
}); });
if (widget.mediaService.mediaFile.type == MediaType.image) {
await widget.storeImageAsOriginal();
}
String? res; String? res;
final storedMediaPath = widget.mediaService.storedPath; final storedMediaPath = widget.mediaService.storedPath;

View file

@ -171,20 +171,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
NotificationBadge( NotificationBadge(
count: (media.type != MediaType.video) count: (media.type == MediaType.video)
? '0' ? '0'
: media.displayLimitInMilliseconds == null : media.displayLimitInMilliseconds == null
? '' ? ''
: media.displayLimitInMilliseconds.toString(), : media.displayLimitInMilliseconds.toString(),
child: ActionButton( child: ActionButton(
(media.type != MediaType.video) (media.type == MediaType.video)
? media.displayLimitInMilliseconds == null ? media.displayLimitInMilliseconds == null
? Icons.repeat_rounded ? Icons.repeat_rounded
: Icons.repeat_one_rounded : Icons.repeat_one_rounded
: Icons.timer_outlined, : Icons.timer_outlined,
tooltipText: context.lang.protectAsARealTwonly, tooltipText: context.lang.protectAsARealTwonly,
onPressed: () async { onPressed: () async {
if (media.type != MediaType.video) { if (media.type == MediaType.video) {
await mediaService.setDisplayLimit( await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null, (media.displayLimitInMilliseconds == null) ? 0 : null,
); );
@ -311,7 +311,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
} }
if (layers.length > 1 || media.type != MediaType.video) { if (layers.length > 1 || media.type == MediaType.video) {
for (final x in layers) { for (final x in layers) {
x.showCustomButtons = false; x.showCustomButtons = false;
} }
@ -434,7 +434,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SaveToGalleryButton( SaveToGalleryButton(
getMergedImage: getEditedImageBytes, storeImageAsOriginal: storeImageAsOriginal,
mediaService: mediaService, mediaService: mediaService,
displayButtonLabel: widget.sendToGroup == null, displayButtonLabel: widget.sendToGroup == null,
isLoading: loadingImage, isLoading: loadingImage,

View file

@ -209,6 +209,12 @@ class _ChatListViewState extends State<ChatListView> {
child: isConnected ? Container() : const ConnectionInfo(), child: isConnected ? Container() : const ConnectionInfo(),
), ),
Positioned.fill( Positioned.fill(
child: RefreshIndicator(
onRefresh: () async {
await apiService.close(() {});
await apiService.connect(force: true);
await Future.delayed(const Duration(seconds: 1));
},
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
? Center( ? Center(
child: Padding( child: Padding(
@ -223,17 +229,12 @@ class _ChatListViewState extends State<ChatListView> {
), ),
); );
}, },
label: Text(context.lang.chatListViewSearchUserNameBtn), label:
Text(context.lang.chatListViewSearchUserNameBtn),
), ),
), ),
) )
: RefreshIndicator( : ListView.builder(
onRefresh: () async {
await apiService.close(() {});
await apiService.connect(force: true);
await Future.delayed(const Duration(seconds: 1));
},
child: ListView.builder(
itemCount: _groupsPinned.length + itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) + (_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length + _groupsNotPinned.length +

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
@ -36,10 +37,12 @@ class _UserListItem extends State<GroupListItem> {
List<Message> messagesNotOpened = []; List<Message> messagesNotOpened = [];
late StreamSubscription<List<Message>> messagesNotOpenedStream; late StreamSubscription<List<Message>> messagesNotOpenedStream;
List<Message> lastMessages = []; Message? lastMessage;
late StreamSubscription<List<Message>> lastMessageStream; late StreamSubscription<Message?> lastMessageStream;
late StreamSubscription<List<MediaFile>> lastMediaFilesStream;
List<Message> previewMessages = []; List<Message> previewMessages = [];
List<MediaFile> previewMediaFiles = [];
bool hasNonOpenedMediaFile = false; bool hasNonOpenedMediaFile = false;
@override @override
@ -52,6 +55,7 @@ class _UserListItem extends State<GroupListItem> {
void dispose() { void dispose() {
messagesNotOpenedStream.cancel(); messagesNotOpenedStream.cancel();
lastMessageStream.cancel(); lastMessageStream.cancel();
lastMediaFilesStream.cancel();
super.dispose(); super.dispose();
} }
@ -59,30 +63,44 @@ class _UserListItem extends State<GroupListItem> {
lastMessageStream = twonlyDB.messagesDao lastMessageStream = twonlyDB.messagesDao
.watchLastMessage(widget.group.groupId) .watchLastMessage(widget.group.groupId)
.listen((update) { .listen((update) {
updateState(update, messagesNotOpened); protectUpdateState.protect(() async {
await updateState(update, messagesNotOpened);
});
}); });
messagesNotOpenedStream = twonlyDB.messagesDao messagesNotOpenedStream = twonlyDB.messagesDao
.watchMessageNotOpened(widget.group.groupId) .watchMessageNotOpened(widget.group.groupId)
.listen((update) { .listen((update) {
updateState(lastMessages, update); protectUpdateState.protect(() async {
await updateState(lastMessage, update);
});
});
lastMediaFilesStream =
twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
for (final mediaFile in mediaFiles) {
final index =
previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId);
if (index >= 0) {
previewMediaFiles[index] = mediaFile;
}
}
setState(() {});
}); });
} }
void updateState( Mutex protectUpdateState = Mutex();
List<Message> newLastMessages,
Future<void> updateState(
Message? newLastMessage,
List<Message> newMessagesNotOpened, List<Message> newMessagesNotOpened,
) { ) async {
if (newLastMessages.isEmpty) { if (newLastMessage == null) {
// there are no messages at all // there are no messages at all
currentMessage = null; currentMessage = null;
previewMessages = []; previewMessages = [];
} else if (newMessagesNotOpened.isEmpty) { } else if (newMessagesNotOpened.isNotEmpty) {
// there are no not opened messages show just the last message in the table // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
currentMessage = newLastMessages.last;
previewMessages = newLastMessages;
} else {
// filter first for received messages
final receivedMessages = final receivedMessages =
newMessagesNotOpened.where((x) => x.senderId != null).toList(); newMessagesNotOpened.where((x) => x.senderId != null).toList();
@ -93,6 +111,10 @@ class _UserListItem extends State<GroupListItem> {
previewMessages = newMessagesNotOpened; previewMessages = newMessagesNotOpened;
currentMessage = newMessagesNotOpened.first; currentMessage = newMessagesNotOpened.first;
} }
} else {
// there are no not opened messages show just the last message in the table
currentMessage = newLastMessage;
previewMessages = [newLastMessage];
} }
final msgs = final msgs =
@ -106,7 +128,18 @@ class _UserListItem extends State<GroupListItem> {
hasNonOpenedMediaFile = false; hasNonOpenedMediaFile = false;
} }
lastMessages = newLastMessages; for (final message in previewMessages) {
if (message.mediaId != null &&
!previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (mediaFile != null) {
previewMediaFiles.add(mediaFile);
}
}
}
lastMessage = newLastMessage;
messagesNotOpened = newMessagesNotOpened; messagesNotOpened = newMessagesNotOpened;
setState(() { setState(() {
// sets lastMessages, messagesNotOpened and currentMessage // sets lastMessages, messagesNotOpened and currentMessage
@ -136,7 +169,7 @@ class _UserListItem extends State<GroupListItem> {
await startDownloadMedia(mediaFile, true); await startDownloadMedia(mediaFile, true);
return; return;
} }
if (mediaFile.downloadState! == DownloadState.downloaded) { if (mediaFile.downloadState! == DownloadState.ready) {
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(
context, context,
@ -184,7 +217,7 @@ class _UserListItem extends State<GroupListItem> {
? Text(context.lang.chatsTapToSend) ? Text(context.lang.chatsTapToSend)
: Row( : Row(
children: [ children: [
MessageSendStateIcon(previewMessages), MessageSendStateIcon(previewMessages, previewMediaFiles),
const Text(''), const Text(''),
const SizedBox(width: 5), const SizedBox(width: 5),
if (currentMessage != null) if (currentMessage != null)

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -94,6 +95,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
super.dispose(); super.dispose();
} }
Mutex protectMessageUpdating = Mutex();
Future<void> initStreams() async { Future<void> initStreams() async {
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId); final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
userSub = groupStream.listen((newGroup) { userSub = groupStream.listen((newGroup) {
@ -105,6 +108,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
messageSub = msgStream.listen((newMessages) async { messageSub = msgStream.listen((newMessages) async {
/// In case a message is not open yet the message is updated, which will trigger this watch to be called again.
/// So as long as the Mutex is locked just return...
if (protectMessageUpdating.isLocked) {
return;
}
await protectMessageUpdating.protect(() async {
await flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
final chatItems = <ChatItem>[]; final chatItems = <ChatItem>[];
@ -118,6 +127,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (msg.type == MessageType.text && if (msg.type == MessageType.text &&
msg.senderId != null && msg.senderId != null &&
msg.openedAt == null) { msg.openedAt == null) {
if (openedMessages[msg.senderId!] == null) {
openedMessages[msg.senderId!] = [];
}
openedMessages[msg.senderId!]!.add(msg.messageId); openedMessages[msg.senderId!]!.add(msg.messageId);
} }
@ -145,8 +157,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
); );
} }
await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId); if (!mounted) return;
setState(() { setState(() {
messages = chatItems.reversed.toList(); messages = chatItems.reversed.toList();
}); });
@ -155,6 +166,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
galleryItems = items.values.toList(); galleryItems = items.values.toList();
setState(() {}); setState(() {});
}); });
});
} }
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
@ -396,18 +408,8 @@ bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
if (index <= 0) { if (index <= 0) {
return true; // If there is no previous message, return true return true; // If there is no previous message, return true
} }
return (messages[index - 1].message?.senderId ==
final lastMessage = messages[index - 1]; messages[index].message?.senderId);
final currentMessage = messages[index];
if (lastMessage.isMessage && currentMessage.isMessage) {
// Check if both messages have the same quotesMessageId (or both are null)
return (lastMessage.message!.quotesMessageId == null &&
currentMessage.message!.quotesMessageId == null) ||
(lastMessage.message!.quotesMessageId != null &&
currentMessage.message!.quotesMessageId != null);
}
return false;
} }
double calculateNumberOfLines(String text, double width, double fontSize) { double calculateNumberOfLines(String text, double width, double fontSize) {

View file

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart' import 'package:twonly/src/database/tables/messages.table.dart'
hide MessageActions; hide MessageActions;
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -35,14 +38,31 @@ class ChatListEntry extends StatefulWidget {
class _ChatListEntryState extends State<ChatListEntry> { class _ChatListEntryState extends State<ChatListEntry> {
MediaFileService? mediaService; MediaFileService? mediaService;
StreamSubscription<MediaFile?>? mediaFileSub;
@override @override
void initState() { void initState() {
initAsync(); initAsync();
super.initState(); super.initState();
} }
@override
void dispose() {
mediaFileSub?.cancel();
super.dispose();
}
Future<void> initAsync() async { Future<void> initAsync() async {
mediaService = await MediaFileService.fromMediaId(widget.message.messageId); if (widget.message.mediaId != null) {
final mediaFileStream =
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!);
mediaFileSub = mediaFileStream.listen((mediaFiles) async {
if (mediaFiles != null) {
mediaService = await MediaFileService.fromMedia(mediaFiles);
if (mounted) setState(() {});
}
});
}
setState(() {}); setState(() {});
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -47,17 +48,20 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
EncryptedContent( EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate( mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.REOPENED, type: EncryptedContent_MediaUpdate_Type.REOPENED,
targetMediaId: widget.message.mediaId, targetMessageId: widget.message.messageId,
), ),
), ),
null,
);
await twonlyDB.messagesDao.updateMessageId(
widget.message.messageId,
const MessagesCompanion(openedAt: Value(null)),
); );
await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId);
} }
} }
Future<void> onTap() async { Future<void> onTap() async {
if (widget.mediaService.mediaFile.downloadState == if (widget.mediaService.mediaFile.downloadState == DownloadState.ready &&
DownloadState.downloaded &&
widget.message.openedAt == null) { widget.message.openedAt == null) {
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(
@ -91,7 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
onTap: (widget.message.type == MessageType.media) ? onTap : null, onTap: (widget.message.type == MessageType.media) ? onTap : null,
child: SizedBox( child: SizedBox(
width: 150, width: 150,
height: widget.message.mediaStored ? 271 : null, height: (widget.message.mediaStored &&
widget.mediaService.imagePreviewAvailable)
? 271
: null,
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ClipRRect( child: ClipRRect(

View file

@ -58,7 +58,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
bool loadIndex() { bool loadIndex() {
if (widget.message.mediaStored) { if (widget.message.mediaStored) {
final index = widget.galleryItems.indexWhere( final index = widget.galleryItems.indexWhere(
(x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId), (x) => x.mediaService.mediaFile.mediaId == (widget.message.mediaId),
); );
if (index != -1) { if (index != -1) {
galleryItemIndex = index; galleryItemIndex = index;
@ -112,7 +112,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!widget.message.mediaStored) { if (!widget.message.mediaStored ||
!widget.mediaService.imagePreviewAvailable) {
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minHeight: 39, minHeight: 39,
@ -130,6 +131,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
), ),
child: MessageSendStateIcon( child: MessageSendStateIcon(
[widget.message], [widget.message],
[widget.mediaService.mediaFile],
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
canBeReopened: widget.canBeReopened, canBeReopened: widget.canBeReopened,
), ),

View file

@ -64,6 +64,7 @@ class MessageContextMenu extends StatelessWidget {
remove: false, remove: false,
), ),
), ),
null,
); );
}, },
child: const FaIcon(FontAwesomeIcons.faceLaugh), child: const FaIcon(FontAwesomeIcons.faceLaugh),

View file

@ -1,11 +1,10 @@
import 'dart:collection'; import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.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';
@ -18,49 +17,37 @@ enum MessageSendState {
sending, sending,
} }
Future<MessageSendState> messageSendStateFromMessage(Message msg) async { MessageSendState messageSendStateFromMessage(Message msg) {
MessageSendState state;
final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
msg.groupId,
msg.messageId,
MessageActionType.ackByServerAt,
);
if (!ackByServer) {
if (msg.senderId == null) { if (msg.senderId == null) {
state = MessageSendState.sending; /// messages was send by me, look up if every messages was received by the server...
} else { if (msg.ackByServer == null) {
state = MessageSendState.receiving; return MessageSendState.sending;
} }
if (msg.openedAt != null) {
return MessageSendState.sendOpened;
} else { } else {
if (msg.senderId == null) { return MessageSendState.send;
// message send
if (msg.openedAt == null) {
state = MessageSendState.send;
} else {
state = MessageSendState.sendOpened;
} }
} else { }
// message received // message received
if (msg.openedAt == null) { if (msg.openedAt == null) {
state = MessageSendState.received; return MessageSendState.received;
} else { } else {
state = MessageSendState.receivedOpened; return MessageSendState.receivedOpened;
} }
} }
}
return state;
}
class MessageSendStateIcon extends StatefulWidget { class MessageSendStateIcon extends StatefulWidget {
const MessageSendStateIcon( const MessageSendStateIcon(
this.messages, { this.messages,
this.mediaFiles, {
super.key, super.key,
this.canBeReopened = false, this.canBeReopened = false,
this.mainAxisAlignment = MainAxisAlignment.end, this.mainAxisAlignment = MainAxisAlignment.end,
}); });
final List<Message> messages; final List<Message> messages;
final List<MediaFile> mediaFiles;
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
final bool canBeReopened; final bool canBeReopened;
@ -69,17 +56,30 @@ class MessageSendStateIcon extends StatefulWidget {
} }
class _MessageSendStateIconState extends State<MessageSendStateIcon> { class _MessageSendStateIconState extends State<MessageSendStateIcon> {
List<Widget> icons = <Widget>[];
String text = '';
Widget? textWidget;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initAsync();
} }
Future<void> initAsync() async { Widget getLoaderIcon(Color color) {
return Row(
children: [
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1, color: color),
),
const SizedBox(width: 2),
],
);
}
@override
Widget build(BuildContext context) {
final icons = <Widget>[];
var text = '';
Widget? textWidget;
textWidget = null;
final kindsAlreadyShown = HashSet<MessageType>(); final kindsAlreadyShown = HashSet<MessageType>();
for (final message in widget.messages) { for (final message in widget.messages) {
@ -87,16 +87,14 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
if (kindsAlreadyShown.contains(message.type)) continue; if (kindsAlreadyShown.contains(message.type)) continue;
kindsAlreadyShown.add(message.type); kindsAlreadyShown.add(message.type);
final state = await messageSendStateFromMessage(message); final state = messageSendStateFromMessage(message);
final mediaFile = message.mediaId == null final mediaFile = message.mediaId == null
? null ? null
: await MediaFileService.fromMediaId(message.mediaId!); : widget.mediaFiles
.firstWhereOrNull((t) => t.mediaId == message.mediaId);
if (!mounted) return; final color = getMessageColorFromType(message, mediaFile, context);
final color =
getMessageColorFromType(message, mediaFile?.mediaFile, context);
Widget icon = const Placeholder(); Widget icon = const Placeholder();
textWidget = null; textWidget = null;
@ -126,11 +124,10 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
icon = Icon(Icons.square_rounded, size: 14, color: color); icon = Icon(Icons.square_rounded, size: 14, color: color);
text = context.lang.messageSendState_Received; text = context.lang.messageSendState_Received;
if (message.type == MessageType.media) { if (message.type == MessageType.media) {
if (mediaFile!.mediaFile.downloadState == DownloadState.pending) { if (mediaFile!.downloadState == DownloadState.pending) {
text = context.lang.messageSendState_TapToLoad; text = context.lang.messageSendState_TapToLoad;
} }
if (mediaFile.mediaFile.downloadState == if (mediaFile.downloadState == DownloadState.downloading) {
DownloadState.downloading) {
text = context.lang.messageSendState_Loading; text = context.lang.messageSendState_Loading;
icon = getLoaderIcon(color); icon = getLoaderIcon(color);
} }
@ -153,12 +150,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
if (mediaFile != null) { if (mediaFile != null) {
if (mediaFile.mediaFile.stored) { if (mediaFile.reopenByContact) {
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color); icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
text = context.lang.messageReopened; text = context.lang.messageReopened;
} }
if (mediaFile.mediaFile.reuploadRequestedBy != null) { if (mediaFile.downloadState == DownloadState.reuploadRequested) {
icon = icon =
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color); FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
textWidget = Text( textWidget = Text(
@ -175,24 +172,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
} }
} }
setState(() {});
}
Widget getLoaderIcon(Color color) {
return Row(
children: [
SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1, color: color),
),
const SizedBox(width: 2),
],
);
}
@override
Widget build(BuildContext context) {
if (icons.isEmpty) return Container(); if (icons.isEmpty) return Container();
var icon = icons[0]; var icon = icons[0];
@ -201,18 +180,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
icon = Stack( icon = Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
// First icon (bottom icon) Transform.scale(
icons[0], scale: 1.3,
Transform(
transform: Matrix4.identity()
..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half
..translateByDouble(3, 5, 0, 1),
// Move down by 10 pixels (adjust as needed)
alignment: Alignment.center,
child: icons[1], child: icons[1],
), ),
// Second icon (top icon, slightly offset) icons[0],
], ],
); );
} }
@ -223,7 +195,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
icon, icon,
const SizedBox(width: 3), const SizedBox(width: 3),
if (textWidget != null) if (textWidget != null)
textWidget! textWidget
else else
Text( Text(
text, text,

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
@ -163,17 +162,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
// } // }
final stream = final stream =
twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId); twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!);
var downloadTriggered = false; var downloadTriggered = false;
await downloadStateListener?.cancel(); await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async { downloadStateListener = stream.listen((updated) async {
if (updated == null) return; if (updated == null) return;
if (updated.downloadState != DownloadState.downloaded) { if (updated.downloadState != DownloadState.ready) {
if (!downloadTriggered) { if (!downloadTriggered) {
downloadTriggered = true; downloadTriggered = true;
await startDownloadMedia(currentMedia!.mediaFile, true); final mediaFile = await twonlyDB.mediaFilesDao
.getMediaFileById(allMediaFiles.first.mediaId!);
await startDownloadMedia(mediaFile!, true);
unawaited(tryDownloadAllMediaFiles(force: true)); unawaited(tryDownloadAllMediaFiles(force: true));
} }
return; return;
@ -211,11 +212,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
[currentMessage!.messageId], [currentMessage!.messageId],
); );
await twonlyDB.messagesDao.updateMessageId(
currentMessage!.messageId,
MessagesCompanion(openedAt: Value(DateTime.now())),
);
if (!currentMediaLocal.tempPath.existsSync()) { if (!currentMediaLocal.tempPath.existsSync()) {
Log.error('Temp media file not found...'); Log.error('Temp media file not found...');
await handleMediaError(currentMediaLocal.mediaFile); await handleMediaError(currentMediaLocal.mediaFile);
@ -289,9 +285,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
pb.EncryptedContent( pb.EncryptedContent(
mediaUpdate: pb.EncryptedContent_MediaUpdate( mediaUpdate: pb.EncryptedContent_MediaUpdate(
type: pb.EncryptedContent_MediaUpdate_Type.STORED, type: pb.EncryptedContent_MediaUpdate_Type.STORED,
targetMediaId: currentMedia!.mediaFile.mediaId, targetMessageId: currentMessage!.messageId,
), ),
), ),
null,
); );
setState(() { setState(() {
imageSaved = true; imageSaved = true;
@ -537,8 +534,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
], ],
), ),
), ),
if (currentMedia?.mediaFile.downloadState != if (currentMedia?.mediaFile.downloadState != DownloadState.ready)
DownloadState.downloaded)
const Positioned.fill( const Positioned.fill(
child: Center( child: Center(
child: SizedBox( child: SizedBox(

View file

@ -52,6 +52,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
emoji: widget.emoji, emoji: widget.emoji,
), ),
), ),
null,
); );
setState(() { setState(() {

View file

@ -167,9 +167,9 @@ class UserList extends StatelessWidget {
var directChat = var directChat =
await twonlyDB.groupsDao.getDirectChat(user.userId); await twonlyDB.groupsDao.getDirectChat(user.userId);
if (directChat == null) { if (directChat == null) {
await twonlyDB.groupsDao.insertGroup( await twonlyDB.groupsDao.createNewDirectChat(
user.userId,
GroupsCompanion( GroupsCompanion(
isDirectChat: const Value(true),
groupName: Value( groupName: Value(
getContactDisplayName(user), getContactDisplayName(user),
), ),

View file

@ -49,12 +49,14 @@ class MemoriesViewState extends State<MemoriesView> {
final applicationSupportDirectory = final applicationSupportDirectory =
await getApplicationSupportDirectory(); await getApplicationSupportDirectory();
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
galleryItems.add( final mediaService = MediaFileService(
MemoryItem(
mediaService: MediaFileService(
mediaFile, mediaFile,
applicationSupportDirectory: applicationSupportDirectory, applicationSupportDirectory: applicationSupportDirectory,
), );
if (!mediaService.imagePreviewAvailable) continue;
galleryItems.add(
MemoryItem(
mediaService: mediaService,
messages: [], messages: [],
), ),
); );

View file

@ -37,13 +37,19 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final media = widget.galleryItem.mediaService;
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Hero( child: Hero(
tag: widget.galleryItem.mediaService.mediaFile.mediaId, tag: media.mediaFile.mediaId,
child: Stack( child: Stack(
children: [ children: [
Image.file(widget.galleryItem.mediaService.thumbnailPath), if (media.thumbnailPath.existsSync())
Image.file(media.thumbnailPath)
else if (media.storedPath.existsSync())
Image.file(media.storedPath)
else
const Text('Media file removed.'),
if (widget.galleryItem.mediaService.mediaFile.type == if (widget.galleryItem.mediaService.mediaFile.type ==
MediaType.video) MediaType.video)
const Positioned.fill( const Positioned.fill(

View file

@ -84,7 +84,7 @@ class _RegisterViewState extends State<RegisterView> {
username: username, username: username,
displayName: username, displayName: username,
subscriptionPlan: 'Preview', subscriptionPlan: 'Preview',
); )..appVersion = 62;
await const FlutterSecureStorage() await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); .write(key: SecureStorageKeys.userData, value: jsonEncode(userData));

View file

@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class AutomatedTestingView extends StatefulWidget { class AutomatedTestingView extends StatefulWidget {
@ -38,11 +39,13 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
onTap: () async { onTap: () async {
final username = await showUserNameDialog(context); final username = await showUserNameDialog(context);
if (username == null) return; if (username == null) return;
Log.info('Requested to send to $username');
final contacts = final contacts = await twonlyDB.contactsDao
await twonlyDB.contactsDao.getContactsByUsername(username); .getContactsByUsername(username.toLowerCase());
for (final contact in contacts) { for (final contact in contacts) {
Log.info('Sending to ${contact.username}');
final group = final group =
await twonlyDB.groupsDao.getDirectChat(contact.userId); await twonlyDB.groupsDao.getDirectChat(contact.userId);
for (var i = 0; i < 200; i++) { for (var i = 0; i < 200; i++) {
@ -67,10 +70,10 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
Future<String?> showUserNameDialog( Future<String?> showUserNameDialog(
BuildContext context, BuildContext context,
) { ) async {
final controller = TextEditingController(); final controller = TextEditingController();
return showDialog<String>( await showDialog<String>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
@ -97,4 +100,5 @@ Future<String?> showUserNameDialog(
); );
}, },
); );
return controller.text;
} }

View file

@ -1,7 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; 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/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
class RetransmissionDataView extends StatefulWidget { class RetransmissionDataView extends StatefulWidget {
const RetransmissionDataView({super.key}); const RetransmissionDataView({super.key});
@ -101,11 +108,48 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
Text( Text(
'Server-Ack: ${retrans.receipt.ackByServerAt}', 'Server-Ack: ${retrans.receipt.ackByServerAt}',
), ),
if (retrans.receipt.messageId != null)
Text(
'MessageId: ${retrans.receipt.messageId}',
),
if (retrans.receipt.messageId != null)
FutureBuilder(
future: getPushNotificationFromEncryptedContent(
retrans.receipt.contactId,
retrans.receipt.messageId,
pb.EncryptedContent.fromBuffer(
pb.Message.fromBuffer(retrans.receipt.message)
.encryptedContent,
),
),
builder: (d, a) {
if (!a.hasData) return Container();
return Text(
'PushKind: ${a.data?.kind}',
);
},
),
Text( Text(
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
), ),
], ],
), ),
trailing: FilledButton.icon(
onPressed: () async {
final newReceiptId = uuid.v4();
await twonlyDB.receiptsDao.updateReceipt(
retrans.receipt.receiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
ackByServerAt: const Value(null),
),
);
await tryToSendCompleteMessage(
receiptId: newReceiptId,
);
},
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
),
), ),
) )
.toList(), .toList(),

View file

@ -1,4 +1,20 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/database/twonly_database_old.dart'
show TwonlyDatabaseOld;
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class DatabaseMigrationView extends StatefulWidget { class DatabaseMigrationView extends StatefulWidget {
const DatabaseMigrationView({super.key}); const DatabaseMigrationView({super.key});
@ -8,8 +24,426 @@ class DatabaseMigrationView extends StatefulWidget {
} }
class _DatabaseMigrationViewState extends State<DatabaseMigrationView> { class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
bool _isMigrating = false;
bool _isMigratingFinished = false;
int _contactsMigrated = 0;
int _storedMediaFiles = 0;
Future<void> startMigration() async {
setState(() {
_isMigrating = true;
});
final oldDatabase = TwonlyDatabaseOld();
final oldContacts = await oldDatabase.contacts.select().get();
final oldMessages = await oldDatabase.messages.select().get();
for (final oldContact in oldContacts) {
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
userId: Value(oldContact.userId),
username: Value(oldContact.username),
displayName: Value(oldContact.displayName),
nickName: Value(oldContact.nickName),
avatarSvg: Value(oldContact.avatarSvg),
senderProfileCounter: const Value(0),
accepted: Value(oldContact.accepted),
requested: Value(oldContact.requested),
blocked: Value(oldContact.blocked),
verified: Value(oldContact.verified),
deleted: Value(oldContact.deleted),
createdAt: Value(oldContact.createdAt),
),
);
setState(() {
_contactsMigrated += 1;
});
if (!oldContact.deleted) {
final group = await twonlyDB.groupsDao.createNewDirectChat(
oldContact.userId,
GroupsCompanion(
pinned: Value(oldContact.pinned),
archived: Value(oldContact.archived),
groupName: Value(getContactDisplayNameOld(oldContact)),
totalMediaCounter: Value(oldContact.totalMediaCounter),
alsoBestFriend: Value(oldContact.alsoBestFriend),
createdAt: Value(oldContact.createdAt),
lastFlameCounterChange: Value(oldContact.lastFlameCounterChange),
lastFlameSync: Value(oldContact.lastFlameSync),
lastMessageExchange: Value(oldContact.lastMessageExchange),
lastMessageReceived: Value(oldContact.lastMessageReceived),
lastMessageSend: Value(oldContact.lastMessageSend),
flameCounter: Value(oldContact.flameCounter),
),
);
if (group == null) continue;
for (final oldMessage in oldMessages) {
if (oldMessage.mediaUploadId == null &&
oldMessage.mediaDownloadId == null) {
/// only interested in media files...
continue;
}
if (oldMessage.contactId != oldContact.userId) continue;
if (!oldMessage.mediaStored) continue;
var storedMediaPath =
join((await getApplicationSupportDirectory()).path, 'media');
if (oldMessage.mediaDownloadId != null) {
storedMediaPath =
'${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}';
} else {
storedMediaPath =
'${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}';
}
var type = MediaType.image;
if (File('$storedMediaPath.mp4').existsSync()) {
type = MediaType.video;
storedMediaPath = '$storedMediaPath.mp4';
} else if (File('$storedMediaPath.png').existsSync()) {
type = MediaType.image;
storedMediaPath = '$storedMediaPath.png';
} else if (File('$storedMediaPath.webp').existsSync()) {
type = MediaType.image;
storedMediaPath = '$storedMediaPath.webp';
} else {
continue;
}
final uniqueId = Value(
getUUIDforDirectChat(
oldMessage.messageOtherId ?? oldMessage.messageId,
oldMessage.contactId ^ gUser.userId,
),
);
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
mediaId: uniqueId,
stored: const Value(true),
type: Value(type),
createdAt: Value(oldMessage.sendAt),
),
);
if (mediaFile == null) continue;
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: uniqueId,
groupId: Value(group.groupId),
mediaId: uniqueId,
type: const Value(MessageType.media),
),
);
if (message == null) continue;
final mediaService = await MediaFileService.fromMedia(mediaFile);
File(storedMediaPath).copySync(mediaService.storedPath.path);
setState(() {
_storedMediaFiles += 1;
});
}
}
}
final memoriesPath = Directory(
join((await getApplicationSupportDirectory()).path, 'media', 'memories'),
);
final files = memoriesPath.listSync();
for (final file in files) {
if (file.path.contains('thumbnail')) continue;
final type =
file.path.contains('mp4') ? MediaType.video : MediaType.image;
final stat = FileStat.statSync(file.path);
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(type),
createdAt: Value(stat.modified),
),
);
final mediaService = await MediaFileService.fromMedia(mediaFile!);
File(file.path).copySync(mediaService.storedPath.path);
setState(() {
_storedMediaFiles += 1;
});
}
final oldContactPreKeys =
await oldDatabase.signalContactPreKeys.select().get();
for (final oldContactPreKey in oldContactPreKeys) {
try {
await twonlyDB
.into(twonlyDB.signalContactPreKeys)
.insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson()));
} catch (e) {
Log.error(e);
}
}
final oldSignalSessionStores =
await oldDatabase.signalSessionStores.select().get();
for (final oldSignalSessionStore in oldSignalSessionStores) {
try {
await twonlyDB.into(twonlyDB.signalSessionStores).insert(
SignalSessionStore.fromJson(oldSignalSessionStore.toJson()));
} catch (e) {
Log.error(e);
}
}
final oldSignalSenderKeyStores =
await oldDatabase.signalSenderKeyStores.select().get();
for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) {
try {
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()),
);
} catch (e) {
Log.error(e);
}
}
final oldSignalPreyKeyStores =
await oldDatabase.signalPreKeyStores.select().get();
for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) {
try {
await twonlyDB
.into(twonlyDB.signalPreKeyStores)
.insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson()));
} catch (e) {
Log.error(e);
}
}
final oldSignalIdentityKeyStores =
await oldDatabase.signalIdentityKeyStores.select().get();
for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) {
try {
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
SignalIdentityKeyStore.fromJson(
oldSignalIdentityKeyStore.toJson()),
);
} catch (e) {
Log.error(e);
}
}
final oldSignalContactSignedPreKeys =
await oldDatabase.signalContactSignedPreKeys.select().get();
for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) {
try {
await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert(
SignalContactSignedPreKey.fromJson(
oldSignalContactSignedPreKey.toJson(),
),
);
} catch (e) {
Log.error(e);
}
}
await updateUserdata((u) {
u.appVersion = 62;
return u;
});
setState(() {
_isMigratingFinished = true;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Placeholder(); return Scaffold(
body: Padding(
padding: const EdgeInsets.all(12),
child: _isMigratingFinished
? ListView(
children: [
const SizedBox(height: 40),
const Text(
'Deine Daten wurden migriert.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
...[
'${_contactsMigrated} Kontakte',
'${_storedMediaFiles} gespeicherte Mediendateien',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
const SizedBox(height: 40),
const Text(
'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
const SizedBox(height: 30),
FilledButton(
onPressed: () {
Restart.restartApp(
notificationTitle: 'Deine Daten wurden migriert.',
notificationBody: 'Click here to open the app again',
);
},
child: const Text(
'App neu starten',
),
),
],
)
: _isMigrating
? ListView(
children: [
const SizedBox(height: 40),
const Text(
'Deine Daten werden migriert.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
const Center(
child: SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(),
),
),
const SizedBox(height: 40),
const Text(
'twonly während der Migration NICHT schließen!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, color: Colors.red),
),
const SizedBox(height: 40),
const Text(
'Aktueller Status',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
...[
'${_contactsMigrated} Kontakte',
'${_storedMediaFiles} gespeicherte Mediendateien',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
],
)
: ListView(
children: [
const SizedBox(height: 40),
const Text(
'twonly. Jetzt besser als je zuvor.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 30),
const Text(
'Das sind die neuen Features.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 10),
...[
'Gruppen',
'Nachrichten bearbeiten & löschen',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
const Text(
'Technische Neuerungen',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17),
),
...[
'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.',
'Verwendung von UUIDs in der Datenbank',
'Von Grund auf neues Datenbank-Schema',
'Verbesserung der Zuverlässigkeit von C2C Nachrichten',
'Verbesserung von Videos',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 10),
),
),
const SizedBox(height: 50),
const Text(
'Was bedeutet das für dich?',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
const Text(
'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 10),
const Text(
'Was nach der Migration erhalten bleibt.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15),
),
...[
'Gespeicherte Bilder',
'Kontakte',
'Flammen',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(height: 10),
const Text(
'Was durch die Migration verloren geht.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Colors.red),
),
...[
'Text-Nachrichten und Reaktionen',
'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(height: 30),
FilledButton(
onPressed: startMigration,
child: const Text(
'Jetzt starten',
),
),
],
),
),
);
} }
} }

View file

@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -21,5 +22,43 @@ void main() {
final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]); final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]);
expect(list1, hexToUint8List(uint8ListToHex(list1))); expect(list1, hexToUint8List(uint8ListToHex(list1)));
}); });
test('Zero inputs produce all-zero UUID', () {
expect(
getUUIDforDirectChat(0, 0),
'00000000-0000-0000-0000-000000000000',
);
expect(getUUIDforDirectChat(0, 0).length, uuid.v1().length);
});
test('Max int values (0x7fffffff)', () {
const max32 = 0x7fffffff; // 2147483647
expect(
getUUIDforDirectChat(max32, max32),
'00000000-7fff-ffff-0000-00007fffffff',
);
});
test('Bigger goes front', () {
expect(
getUUIDforDirectChat(1, 0),
'00000000-0000-0001-0000-000000000000',
);
expect(
getUUIDforDirectChat(0, 1),
'00000000-0000-0001-0000-000000000000',
);
});
test('Arbitrary within 32-bit range', () {
expect(
getUUIDforDirectChat(0x12345678, 0x0abcdef0),
'00000000-1234-5678-0000-00000abcdef0',
);
});
test('Reject values > 0x7fffffff', () {
expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError);
});
}); });
} }