mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
message and media sending does work
This commit is contained in:
parent
4cb7f0ab01
commit
5ae943bcf3
54 changed files with 1712 additions and 429 deletions
17
lib/app.dart
17
lib/app.dart
|
|
@ -1,9 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.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:twonly/globals.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
|
|
@ -160,14 +157,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
_showDatabaseMigration = File(
|
||||
join(
|
||||
(await getApplicationSupportDirectory()).path,
|
||||
'twonly_database.sqlite',
|
||||
),
|
||||
).existsSync();
|
||||
|
||||
_isUserCreated = await isUserCreated();
|
||||
|
||||
if (_isUserCreated) {
|
||||
if (gUser.appVersion < 62) {
|
||||
_showDatabaseMigration = true;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoaded = true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'dart:io';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.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:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -35,6 +38,15 @@ void main() async {
|
|||
|
||||
gCameras = await availableCameras();
|
||||
|
||||
// try {
|
||||
// File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite'))
|
||||
// .deleteSync();
|
||||
// } catch (e) {}
|
||||
// await updateUserdata((u) {
|
||||
// u.appVersion = 0;
|
||||
// return u;
|
||||
// });
|
||||
|
||||
apiService = ApiService();
|
||||
twonlyDB = TwonlyDB();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.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';
|
||||
|
||||
part 'contacts.dao.g.dart';
|
||||
|
|
@ -111,6 +112,22 @@ String getContactDisplayName(Contact user) {
|
|||
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) {
|
||||
return text.split('').map((char) => '$char\u0336').join();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:drift/drift.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/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
part 'groups.dao.g.dart';
|
||||
|
||||
|
|
@ -33,12 +36,46 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<void> insertGroup(GroupsCompanion group) async {
|
||||
await into(groups).insert(
|
||||
group.copyWith(
|
||||
Future<Group?> createNewGroup(GroupsCompanion group) async {
|
||||
final insertGroup = group.copyWith(
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
|
||||
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
|
||||
try {
|
||||
final rowId = await into(mediaFiles).insert(
|
||||
mediaFile.copyWith(
|
||||
var insertMediaFile = mediaFile;
|
||||
|
||||
if (insertMediaFile.mediaId == const Value.absent()) {
|
||||
insertMediaFile = mediaFile.copyWith(
|
||||
mediaId: Value(uuid.v7()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final rowId = await into(mediaFiles).insert(insertMediaFile);
|
||||
|
||||
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
|
||||
.getSingle();
|
||||
|
|
@ -72,11 +76,22 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
|
||||
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();
|
||||
}
|
||||
|
||||
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,24 +38,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
}
|
||||
|
||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||
return (select(messages)
|
||||
final query = select(messages).join([
|
||||
leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)),
|
||||
])
|
||||
..where(
|
||||
(t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.groupId.equals(groupId) &
|
||||
t.senderId.isNotNull() &
|
||||
t.type.equals(MessageType.media.name),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
||||
.watch();
|
||||
mediaFiles.downloadState
|
||||
.equals(DownloadState.reuploadRequested.name)
|
||||
.not() &
|
||||
messages.openedAt.isNull() &
|
||||
messages.groupId.equals(groupId) &
|
||||
messages.mediaId.isNotNull() &
|
||||
messages.senderId.isNotNull() &
|
||||
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)
|
||||
..where((t) => t.groupId.equals(groupId))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||
..limit(1))
|
||||
.watch();
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<List<Message>> watchByGroupId(String groupId) {
|
||||
|
|
@ -64,6 +68,16 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
.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() {
|
||||
// return (update(messages)
|
||||
// ..where(
|
||||
|
|
@ -206,14 +220,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
await into(messageActions).insert(
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
contactId: Value(contactId),
|
||||
type: const Value(MessageActionType.ackByUserAt),
|
||||
type: const Value(MessageActionType.openedAt),
|
||||
actionAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
if (await haveAllMembers(messageId, MessageActionType.openedAt)) {
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleMessageAckByServer(
|
||||
|
|
@ -221,7 +241,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
String messageId,
|
||||
DateTime timestamp,
|
||||
) async {
|
||||
await into(messageActions).insert(
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(messageId),
|
||||
contactId: Value(contactId),
|
||||
|
|
@ -229,14 +249,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
actionAt: Value(timestamp),
|
||||
),
|
||||
);
|
||||
if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) {
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
messageId,
|
||||
MessagesCompanion(ackByServer: Value(DateTime.now())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> haveAllMembers(
|
||||
String groupId,
|
||||
String messageId,
|
||||
MessageActionType action,
|
||||
) 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)
|
||||
..where(
|
||||
|
|
@ -291,11 +319,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
|
||||
Future<Message?> insertMessage(MessagesCompanion message) async {
|
||||
try {
|
||||
final rowId = await into(messages).insert(
|
||||
message.copyWith(
|
||||
var insertMessage = message;
|
||||
|
||||
if (message.messageId == const Value.absent()) {
|
||||
insertMessage = message.copyWith(
|
||||
messageId: Value(uuid.v7()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final rowId = await into(messages).insert(insertMessage);
|
||||
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
message.groupId.value,
|
||||
|
|
@ -323,19 +355,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
.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) {
|
||||
// return (delete(messages)
|
||||
// ..where(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
|
|||
|
||||
part 'receipts.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Receipts, Messages, MessageActions])
|
||||
@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts])
|
||||
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -52,11 +52,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
|
||||
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
||||
try {
|
||||
final id = await into(receipts).insert(
|
||||
entry.copyWith(
|
||||
var insertEntry = entry;
|
||||
if (entry.receiptId == const Value.absent()) {
|
||||
insertEntry = entry.copyWith(
|
||||
receiptId: Value(uuid.v4()),
|
||||
),
|
||||
);
|
||||
}
|
||||
final id = await into(receipts).insert(insertEntry);
|
||||
return await (select(receipts)..where((t) => t.rowId.equals(id)))
|
||||
.getSingle();
|
||||
} catch (e) {
|
||||
|
|
@ -97,4 +99,26 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor<TwonlyDB> {
|
|||
$MessagesTable get messages => attachedDatabase.messages;
|
||||
$ReceiptsTable get receipts => attachedDatabase.receipts;
|
||||
$MessageActionsTable get messageActions => attachedDatabase.messageActions;
|
||||
$ReceivedReceiptsTable get receivedReceipts =>
|
||||
attachedDatabase.receivedReceipts;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
|
|||
tbl.preKeyId.equals(preKey.preKeyId),
|
||||
))
|
||||
.go();
|
||||
Log.info('Using prekey ${preKey.preKeyId} for $contactId');
|
||||
Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId');
|
||||
return preKey;
|
||||
}
|
||||
return null;
|
||||
|
|
@ -68,6 +68,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
|
|||
List<SignalContactPreKeysCompanion> preKeys,
|
||||
) async {
|
||||
for (final preKey in preKeys) {
|
||||
Log.info('[PREKEY] Inserting others ${preKey.preKeyId}');
|
||||
try {
|
||||
await into(signalContactPreKeys).insert(preKey);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -19,15 +19,17 @@ class ConnectPreKeyStore extends PreKeyStore {
|
|||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||
.get();
|
||||
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;
|
||||
return PreKeyRecord.fromBuffer(preKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removePreKey(int preKeyId) async {
|
||||
Log.info('[PREKEY] Removing $preKeyId from my own storage.');
|
||||
await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
|
||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||
.go();
|
||||
|
|
@ -40,6 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
|
|||
preKey: Value(record.serialize()),
|
||||
);
|
||||
|
||||
Log.info('[PREKEY] Storing $preKeyId from my own storage.');
|
||||
try {
|
||||
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ class Messages extends Table {
|
|||
DateTimeColumn get openedAt => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get modifiedAt => dateTime().nullable()();
|
||||
DateTimeColumn get ackByUser => dateTime().nullable()();
|
||||
DateTimeColumn get ackByServer => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {messageId};
|
||||
|
|
|
|||
|
|
@ -30,3 +30,13 @@ class Receipts extends Table {
|
|||
@override
|
||||
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};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ part 'twonly.db.g.dart';
|
|||
Groups,
|
||||
GroupMembers,
|
||||
Receipts,
|
||||
ReceivedReceipts,
|
||||
SignalIdentityKeyStores,
|
||||
SignalPreKeyStores,
|
||||
SignalSenderKeyStores,
|
||||
|
|
|
|||
|
|
@ -2299,6 +2299,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>(
|
||||
'modified_at', aliasedName, true,
|
||||
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
|
||||
List<GeneratedColumn> get $columns => [
|
||||
groupId,
|
||||
|
|
@ -2313,7 +2325,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
isDeletedFromSender,
|
||||
openedAt,
|
||||
createdAt,
|
||||
modifiedAt
|
||||
modifiedAt,
|
||||
ackByUser,
|
||||
ackByServer
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
|
|
@ -2387,6 +2401,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
modifiedAt.isAcceptableOrUnknown(
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -2422,6 +2448,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||
modifiedAt: attachedDatabase.typeMapping
|
||||
.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 createdAt;
|
||||
final DateTime? modifiedAt;
|
||||
final DateTime? ackByUser;
|
||||
final DateTime? ackByServer;
|
||||
const Message(
|
||||
{required this.groupId,
|
||||
required this.messageId,
|
||||
|
|
@ -2461,7 +2493,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
required this.isDeletedFromSender,
|
||||
this.openedAt,
|
||||
required this.createdAt,
|
||||
this.modifiedAt});
|
||||
this.modifiedAt,
|
||||
this.ackByUser,
|
||||
this.ackByServer});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
|
|
@ -2494,6 +2528,12 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
if (!nullToAbsent || modifiedAt != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -2526,6 +2566,12 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
modifiedAt: modifiedAt == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: 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']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']),
|
||||
ackByUser: serializer.fromJson<DateTime?>(json['ackByUser']),
|
||||
ackByServer: serializer.fromJson<DateTime?>(json['ackByServer']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
|
|
@ -2568,6 +2616,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
'openedAt': serializer.toJson<DateTime?>(openedAt),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'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,
|
||||
Value<DateTime?> openedAt = const Value.absent(),
|
||||
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(
|
||||
groupId: groupId ?? this.groupId,
|
||||
messageId: messageId ?? this.messageId,
|
||||
|
|
@ -2602,6 +2654,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
openedAt: openedAt.present ? openedAt.value : this.openedAt,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
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) {
|
||||
return Message(
|
||||
|
|
@ -2626,6 +2680,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
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('openedAt: $openedAt, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('modifiedAt: $modifiedAt')
|
||||
..write('modifiedAt: $modifiedAt, ')
|
||||
..write('ackByUser: $ackByUser, ')
|
||||
..write('ackByServer: $ackByServer')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
|
@ -2663,7 +2722,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
isDeletedFromSender,
|
||||
openedAt,
|
||||
createdAt,
|
||||
modifiedAt);
|
||||
modifiedAt,
|
||||
ackByUser,
|
||||
ackByServer);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
|
|
@ -2680,7 +2741,9 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
other.isDeletedFromSender == this.isDeletedFromSender &&
|
||||
other.openedAt == this.openedAt &&
|
||||
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> {
|
||||
|
|
@ -2697,6 +2760,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
final Value<DateTime?> openedAt;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime?> modifiedAt;
|
||||
final Value<DateTime?> ackByUser;
|
||||
final Value<DateTime?> ackByServer;
|
||||
final Value<int> rowid;
|
||||
const MessagesCompanion({
|
||||
this.groupId = const Value.absent(),
|
||||
|
|
@ -2712,6 +2777,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
this.openedAt = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.modifiedAt = const Value.absent(),
|
||||
this.ackByUser = const Value.absent(),
|
||||
this.ackByServer = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
MessagesCompanion.insert({
|
||||
|
|
@ -2728,6 +2795,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
this.openedAt = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.modifiedAt = const Value.absent(),
|
||||
this.ackByUser = const Value.absent(),
|
||||
this.ackByServer = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : groupId = Value(groupId),
|
||||
messageId = Value(messageId),
|
||||
|
|
@ -2746,6 +2815,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Expression<DateTime>? openedAt,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? modifiedAt,
|
||||
Expression<DateTime>? ackByUser,
|
||||
Expression<DateTime>? ackByServer,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
|
|
@ -2763,6 +2834,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (openedAt != null) 'opened_at': openedAt,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
@ -2781,6 +2854,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Value<DateTime?>? openedAt,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime?>? modifiedAt,
|
||||
Value<DateTime?>? ackByUser,
|
||||
Value<DateTime?>? ackByServer,
|
||||
Value<int>? rowid}) {
|
||||
return MessagesCompanion(
|
||||
groupId: groupId ?? this.groupId,
|
||||
|
|
@ -2796,6 +2871,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
openedAt: openedAt ?? this.openedAt,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
||||
ackByUser: ackByUser ?? this.ackByUser,
|
||||
ackByServer: ackByServer ?? this.ackByServer,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
|
@ -2843,6 +2920,12 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (modifiedAt.present) {
|
||||
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) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
|
|
@ -2865,6 +2948,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
..write('openedAt: $openedAt, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('modifiedAt: $modifiedAt, ')
|
||||
..write('ackByUser: $ackByUser, ')
|
||||
..write('ackByServer: $ackByServer, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.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
|
||||
with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> {
|
||||
@override
|
||||
|
|
@ -6130,6 +6409,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
late final $ReactionsTable reactions = $ReactionsTable(this);
|
||||
late final $GroupMembersTable groupMembers = $GroupMembersTable(this);
|
||||
late final $ReceiptsTable receipts = $ReceiptsTable(this);
|
||||
late final $ReceivedReceiptsTable receivedReceipts =
|
||||
$ReceivedReceiptsTable(this);
|
||||
late final $SignalIdentityKeyStoresTable signalIdentityKeyStores =
|
||||
$SignalIdentityKeyStoresTable(this);
|
||||
late final $SignalPreKeyStoresTable signalPreKeyStores =
|
||||
|
|
@ -6163,6 +6444,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
|||
reactions,
|
||||
groupMembers,
|
||||
receipts,
|
||||
receivedReceipts,
|
||||
signalIdentityKeyStores,
|
||||
signalPreKeyStores,
|
||||
signalSenderKeyStores,
|
||||
|
|
@ -7854,6 +8136,8 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
|
|||
Value<DateTime?> openedAt,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime?> modifiedAt,
|
||||
Value<DateTime?> ackByUser,
|
||||
Value<DateTime?> ackByServer,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
||||
|
|
@ -7870,6 +8154,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
|||
Value<DateTime?> openedAt,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime?> modifiedAt,
|
||||
Value<DateTime?> ackByUser,
|
||||
Value<DateTime?> ackByServer,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
|
|
@ -8042,6 +8328,12 @@ class $$MessagesTableFilterComposer
|
|||
ColumnFilters<DateTime> get modifiedAt => $composableBuilder(
|
||||
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 {
|
||||
final $$GroupsTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
|
|
@ -8245,6 +8537,12 @@ class $$MessagesTableOrderingComposer
|
|||
ColumnOrderings<DateTime> get modifiedAt => $composableBuilder(
|
||||
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 {
|
||||
final $$GroupsTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
|
|
@ -8362,6 +8660,12 @@ class $$MessagesTableAnnotationComposer
|
|||
GeneratedColumn<DateTime> get modifiedAt => $composableBuilder(
|
||||
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 {
|
||||
final $$GroupsTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
|
|
@ -8571,6 +8875,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
Value<DateTime?> openedAt = const Value.absent(),
|
||||
Value<DateTime> createdAt = 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(),
|
||||
}) =>
|
||||
MessagesCompanion(
|
||||
|
|
@ -8587,6 +8893,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
openedAt: openedAt,
|
||||
createdAt: createdAt,
|
||||
modifiedAt: modifiedAt,
|
||||
ackByUser: ackByUser,
|
||||
ackByServer: ackByServer,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
|
|
@ -8603,6 +8911,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
Value<DateTime?> openedAt = const Value.absent(),
|
||||
Value<DateTime> createdAt = 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(),
|
||||
}) =>
|
||||
MessagesCompanion.insert(
|
||||
|
|
@ -8619,6 +8929,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
openedAt: openedAt,
|
||||
createdAt: createdAt,
|
||||
modifiedAt: modifiedAt,
|
||||
ackByUser: ackByUser,
|
||||
ackByServer: ackByServer,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
|
@ -10060,6 +10372,135 @@ typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager<
|
|||
(Receipt, $$ReceiptsTableReferences),
|
||||
Receipt,
|
||||
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
|
||||
= SignalIdentityKeyStoresCompanion Function({
|
||||
required int deviceId,
|
||||
|
|
@ -11497,6 +11938,8 @@ class $TwonlyDBManager {
|
|||
$$GroupMembersTableTableManager(_db, _db.groupMembers);
|
||||
$$ReceiptsTableTableManager get receipts =>
|
||||
$$ReceiptsTableTableManager(_db, _db.receipts);
|
||||
$$ReceivedReceiptsTableTableManager get receivedReceipts =>
|
||||
$$ReceivedReceiptsTableTableManager(_db, _db.receivedReceipts);
|
||||
$$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores =>
|
||||
$$SignalIdentityKeyStoresTableTableManager(
|
||||
_db, _db.signalIdentityKeyStores);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ class UserData {
|
|||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int appVersion = 0;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int avatarCounter = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||
|
|
@ -77,6 +78,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'appVersion': instance.appVersion,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDeveloper': instance.isDeveloper,
|
||||
'deviceId': instance.deviceId,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ class MemoryItem {
|
|||
final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
|
||||
if (mediaService == null) continue;
|
||||
|
||||
if (!mediaService.imagePreviewAvailable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
items
|
||||
.putIfAbsent(
|
||||
message.mediaId!,
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
|||
factory EncryptedContent_MessageUpdate({
|
||||
EncryptedContent_MessageUpdate_Type? type,
|
||||
$core.String? senderMessageId,
|
||||
$core.Iterable<$core.String>? multipleSenderMessageIds,
|
||||
$core.Iterable<$core.String>? multipleTargetMessageIds,
|
||||
$core.String? text,
|
||||
$fixnum.Int64? timestamp,
|
||||
}) {
|
||||
|
|
@ -399,8 +399,8 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
|||
if (senderMessageId != null) {
|
||||
$result.senderMessageId = senderMessageId;
|
||||
}
|
||||
if (multipleSenderMessageIds != null) {
|
||||
$result.multipleSenderMessageIds.addAll(multipleSenderMessageIds);
|
||||
if (multipleTargetMessageIds != null) {
|
||||
$result.multipleTargetMessageIds.addAll(multipleTargetMessageIds);
|
||||
}
|
||||
if (text != null) {
|
||||
$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)
|
||||
..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')
|
||||
..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds')
|
||||
..pPS(3, _omitFieldNames ? '' : 'multipleTargetMessageIds', protoName: 'multipleTargetMessageIds')
|
||||
..aOS(4, _omitFieldNames ? '' : 'text')
|
||||
..aInt64(5, _omitFieldNames ? '' : 'timestamp')
|
||||
..hasRequiredFields = false
|
||||
|
|
@ -463,7 +463,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
|||
void clearSenderMessageId() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.String> get multipleSenderMessageIds => $_getList(2);
|
||||
$core.List<$core.String> get multipleTargetMessageIds => $_getList(2);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.String get text => $_getSZ(3);
|
||||
|
|
@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
|||
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_MediaUpdate({
|
||||
EncryptedContent_MediaUpdate_Type? type,
|
||||
$core.String? targetMediaId,
|
||||
$core.String? targetMessageId,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (type != null) {
|
||||
$result.type = type;
|
||||
}
|
||||
if (targetMediaId != null) {
|
||||
$result.targetMediaId = targetMediaId;
|
||||
if (targetMessageId != null) {
|
||||
$result.targetMessageId = targetMessageId;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
|||
|
||||
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)
|
||||
..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId')
|
||||
..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
|
|
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
|||
void clearType() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get targetMediaId => $_getSZ(1);
|
||||
$core.String get targetMessageId => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set targetMediaId($core.String v) { $_setString(1, v); }
|
||||
set targetMessageId($core.String v) { $_setString(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasTargetMediaId() => $_has(1);
|
||||
$core.bool hasTargetMessageId() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearTargetMediaId() => clearField(2);
|
||||
void clearTargetMessageId() => clearField(2);
|
||||
}
|
||||
|
||||
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ const EncryptedContent_MessageUpdate$json = {
|
|||
'2': [
|
||||
{'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': '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': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
|
||||
],
|
||||
|
|
@ -221,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
|
|||
'1': 'MediaUpdate',
|
||||
'2': [
|
||||
{'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],
|
||||
};
|
||||
|
|
@ -338,8 +338,8 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
|
||||
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
|
||||
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
|
||||
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11'
|
||||
'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
|
||||
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11'
|
||||
'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
|
||||
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
|
||||
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
|
||||
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
|
||||
|
|
@ -353,24 +353,24 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
|
||||
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
|
||||
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
|
||||
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
|
||||
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn'
|
||||
'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg'
|
||||
'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw'
|
||||
'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg'
|
||||
'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw'
|
||||
'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG'
|
||||
'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt'
|
||||
'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE'
|
||||
'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB'
|
||||
'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA'
|
||||
'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo'
|
||||
'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2'
|
||||
'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl'
|
||||
'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm'
|
||||
'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK'
|
||||
'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg'
|
||||
'5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk'
|
||||
'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm'
|
||||
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl');
|
||||
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
|
||||
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn'
|
||||
'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA'
|
||||
'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK'
|
||||
'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG'
|
||||
'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa0gEKDUNvbnRh'
|
||||
'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS'
|
||||
'5UeXBlUgR0eXBlEiEKCWF2YXRhclN2ZxgCIAEoCUgAUglhdmF0YXJTdmeIAQESJQoLZGlzcGxh'
|
||||
'eU5hbWUYAyABKAlIAVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVV'
|
||||
'BEQVRFEAFCDAoKX2F2YXRhclN2Z0IOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5'
|
||||
'cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SW'
|
||||
'QYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQY'
|
||||
'BCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQg'
|
||||
'gKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNv'
|
||||
'dW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgAS'
|
||||
'gDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmll'
|
||||
'bmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZX'
|
||||
'JCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFj'
|
||||
'dFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCw'
|
||||
'oJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZQ==');
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ message EncryptedContent {
|
|||
}
|
||||
Type type = 1;
|
||||
optional string senderMessageId = 2;
|
||||
repeated string multipleSenderMessageIds = 3;
|
||||
repeated string multipleTargetMessageIds = 3;
|
||||
optional string text = 4;
|
||||
int64 timestamp = 5;
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ message EncryptedContent {
|
|||
DECRYPTION_ERROR = 2;
|
||||
}
|
||||
Type type = 1;
|
||||
string targetMediaId = 2;
|
||||
string targetMessageId = 2;
|
||||
}
|
||||
|
||||
message ContactRequest {
|
||||
|
|
|
|||
|
|
@ -156,11 +156,6 @@ class ApiService {
|
|||
}
|
||||
reconnectionTimer?.cancel();
|
||||
reconnectionTimer = null;
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
return false;
|
||||
}
|
||||
return lockConnecting.protect<bool>(() async {
|
||||
if (_channel != null) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
|||
}
|
||||
|
||||
if (failed) {
|
||||
Log.error('Background media upload failed: ${update.status}');
|
||||
await requestMediaReupload(mediaId);
|
||||
} else {
|
||||
await handleEncryptedFile(mediaId);
|
||||
|
|
@ -194,6 +195,9 @@ Future<void> downloadFileFast(
|
|||
if (response.statusCode == 404 ||
|
||||
response.statusCode == 403 ||
|
||||
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...
|
||||
await requestMediaReupload(media.mediaId);
|
||||
return;
|
||||
|
|
@ -217,7 +221,7 @@ Future<void> requestMediaReupload(String mediaId) async {
|
|||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||
targetMediaId: mediaId,
|
||||
targetMessageId: messages.first.messageId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
final mediaId = update.task.taskId.replaceAll('upload_', '');
|
||||
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||
|
||||
if (update.status == TaskStatus.enqueued ||
|
||||
update.status == TaskStatus.running) {
|
||||
// Ignore these updates
|
||||
return;
|
||||
}
|
||||
|
||||
if (media == null) {
|
||||
Log.error(
|
||||
'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);
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
await mediaService.setUploadState(UploadState.uploaded);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import 'package:cryptography_plus/cryptography_plus.dart';
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.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/messages.table.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/client/generated/messages.pb.dart';
|
||||
|
|
@ -47,6 +49,7 @@ Future<void> insertMediaFileInMessagesTable(
|
|||
MessagesCompanion(
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaService.mediaFile.mediaId),
|
||||
type: const Value(MessageType.media),
|
||||
),
|
||||
);
|
||||
if (message != null) {
|
||||
|
|
@ -147,12 +150,13 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
}
|
||||
|
||||
final notEncryptedContent = EncryptedContent(
|
||||
groupId: message.groupId,
|
||||
media: EncryptedContent_Media(
|
||||
senderMessageId: message.messageId,
|
||||
type: type,
|
||||
requiresAuthentication: media.mediaFile.requiresAuthentication,
|
||||
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||
downloadToken: media.mediaFile.downloadToken,
|
||||
downloadToken: downloadToken.toList(),
|
||||
encryptionKey: media.mediaFile.encryptionKey,
|
||||
encryptionNonce: media.mediaFile.encryptionNonce,
|
||||
encryptionMac: media.mediaFile.encryptionMac,
|
||||
|
|
@ -202,12 +206,25 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
|
||||
}
|
||||
|
||||
Mutex protectUpload = Mutex();
|
||||
|
||||
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()
|
||||
.read(key: SecureStorageKeys.apiAuthToken);
|
||||
|
||||
if (apiAuthTokenRaw == null) {
|
||||
Log.error('api auth token not defined.');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
||||
|
||||
|
|
@ -234,4 +251,5 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
|
|||
await FileDownloader().enqueue(task);
|
||||
|
||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:fixnum/fixnum.dart';
|
|||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:mutex/mutex.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/model/protobuf/api/websocket/error.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({
|
||||
String? receiptId,
|
||||
Receipt? receipt,
|
||||
bool reupload = false,
|
||||
bool onlyReturnEncryptedData = false,
|
||||
}) async {
|
||||
try {
|
||||
|
|
@ -49,15 +49,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
}
|
||||
receiptId = receipt.receiptId;
|
||||
|
||||
if (reupload) {
|
||||
await twonlyDB.receiptsDao.updateReceipt(
|
||||
receiptId,
|
||||
const ReceiptsCompanion(
|
||||
ackByServerAt: Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
|
||||
Log.error('$receiptId message already uploaded!');
|
||||
return null;
|
||||
|
|
@ -71,12 +62,18 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
final encryptedContent =
|
||||
pb.EncryptedContent.fromBuffer(message.encryptedContent);
|
||||
|
||||
var pushData = await getPushDataFromEncryptedContent(
|
||||
final pushNotification = await getPushNotificationFromEncryptedContent(
|
||||
receipt.contactId,
|
||||
receipt.messageId,
|
||||
encryptedContent,
|
||||
);
|
||||
|
||||
Uint8List? pushData;
|
||||
if (pushNotification != null) {
|
||||
pushData =
|
||||
await encryptPushNotification(receipt.contactId, pushNotification);
|
||||
}
|
||||
|
||||
if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
|
||||
pushData = (PushNotification()..kind = PushKind.testNotification)
|
||||
.writeToBuffer();
|
||||
|
|
@ -167,6 +164,7 @@ Future<void> insertAndSendTextMessage(
|
|||
MessagesCompanion(
|
||||
groupId: Value(groupId),
|
||||
content: Value(textMessage),
|
||||
type: const Value(MessageType.text),
|
||||
quotesMessageId: Value(quotesMessageId),
|
||||
),
|
||||
);
|
||||
|
|
@ -187,26 +185,24 @@ Future<void> insertAndSendTextMessage(
|
|||
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
||||
}
|
||||
|
||||
await sendCipherTextToGroup(groupId, encryptedContent);
|
||||
await sendCipherTextToGroup(groupId, encryptedContent, message.messageId);
|
||||
}
|
||||
|
||||
Future<void> sendCipherTextToGroup(
|
||||
String groupId,
|
||||
pb.EncryptedContent encryptedContent,
|
||||
String? messageId,
|
||||
) async {
|
||||
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||
if (group == null) return;
|
||||
|
||||
encryptedContent
|
||||
..groupId = groupId
|
||||
..isDirectChat = group.isDirectChat;
|
||||
encryptedContent.groupId = groupId;
|
||||
|
||||
for (final groupMember in groupMembers) {
|
||||
unawaited(
|
||||
sendCipherText(
|
||||
groupMember.contactId,
|
||||
encryptedContent,
|
||||
messageId: messageId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -216,6 +212,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
int contactId,
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
bool onlyReturnEncryptedData = false,
|
||||
String? messageId,
|
||||
}) async {
|
||||
final response = pb.Message()
|
||||
..type = pb.Message_Type.CIPHERTEXT
|
||||
|
|
@ -225,6 +222,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
ReceiptsCompanion(
|
||||
contactId: Value(contactId),
|
||||
message: Value(response.writeToBuffer()),
|
||||
messageId: Value(messageId),
|
||||
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
|
||||
),
|
||||
);
|
||||
|
|
@ -249,15 +247,23 @@ Future<void> notifyContactAboutOpeningMessage(
|
|||
biggestMessageId = messageOtherId;
|
||||
}
|
||||
}
|
||||
Log.info('Opened messages: $messageOtherIds');
|
||||
|
||||
await sendCipherText(
|
||||
contactId,
|
||||
pb.EncryptedContent(
|
||||
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
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));
|
||||
|
||||
Mutex protectReceiptCheck = Mutex();
|
||||
|
||||
Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||
final message = Message.fromBuffer(body);
|
||||
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) {
|
||||
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
||||
Log.info('Got delivery receipt for $receiptId!');
|
||||
|
|
@ -65,7 +76,15 @@ Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
Log.info(
|
||||
'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:
|
||||
|
|
@ -112,7 +131,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
final (content, decryptionErrorType) = await signalDecryptMessage(
|
||||
fromUserId,
|
||||
encryptedContentRaw,
|
||||
messageType as int,
|
||||
messageType.value,
|
||||
);
|
||||
|
||||
if (content == null) {
|
||||
|
|
@ -147,7 +166,24 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
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()) {
|
||||
Log.error('Messages should have a groupId $fromUserId.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -157,14 +193,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (content.hasMessageUpdate()) {
|
||||
await handleMessageUpdate(
|
||||
fromUserId,
|
||||
content.messageUpdate,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content.hasTextMessage()) {
|
||||
await handleTextMessage(
|
||||
fromUserId,
|
||||
|
|
@ -192,14 +220,5 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
|||
return null;
|
||||
}
|
||||
|
||||
if (content.hasMediaUpdate()) {
|
||||
await handleMediaUpdate(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.mediaUpdate,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.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/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
|
|
@ -92,6 +93,7 @@ Future<void> handleMedia(
|
|||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile.mediaId),
|
||||
type: const Value(MessageType.media),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
|
|
@ -112,16 +114,17 @@ Future<void> handleMedia(
|
|||
|
||||
Future<void> handleMediaUpdate(
|
||||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_MediaUpdate mediaUpdate,
|
||||
) async {
|
||||
final messages = await twonlyDB.messagesDao
|
||||
.getMessagesByMediaId(mediaUpdate.targetMediaId);
|
||||
if (messages.length != 1) return;
|
||||
final message = messages.first;
|
||||
if (message.senderId != fromUserId) return;
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(mediaUpdate.targetMessageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null) {
|
||||
Log.error(
|
||||
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.');
|
||||
}
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!);
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'Got media file update, but media file was not found ${message.mediaId}',
|
||||
|
|
@ -140,15 +143,8 @@ Future<void> handleMediaUpdate(
|
|||
);
|
||||
case EncryptedContent_MediaUpdate_Type.STORED:
|
||||
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);
|
||||
unawaited(mediaService.createThumbnail());
|
||||
await mediaService.storeMediaFile();
|
||||
|
||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||
|
|
|
|||
|
|
@ -9,17 +9,20 @@ Future<void> handleMessageUpdate(
|
|||
) async {
|
||||
switch (messageUpdate.type) {
|
||||
case EncryptedContent_MessageUpdate_Type.OPENED:
|
||||
for (final targetMessageId in messageUpdate.multipleTargetMessageIds) {
|
||||
Log.info(
|
||||
'Opened message ${messageUpdate.multipleSenderMessageIds.length}',
|
||||
'Opened message $targetMessageId',
|
||||
);
|
||||
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
|
||||
await twonlyDB.messagesDao.handleMessageOpened(
|
||||
contactId,
|
||||
senderMessageId,
|
||||
targetMessageId,
|
||||
fromTimestamp(messageUpdate.timestamp),
|
||||
);
|
||||
}
|
||||
case EncryptedContent_MessageUpdate_Type.DELETE:
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||
return;
|
||||
}
|
||||
Log.info('Delete message ${messageUpdate.senderMessageId}');
|
||||
await twonlyDB.messagesDao.handleMessageDeletion(
|
||||
contactId,
|
||||
|
|
@ -27,6 +30,9 @@ Future<void> handleMessageUpdate(
|
|||
fromTimestamp(messageUpdate.timestamp),
|
||||
);
|
||||
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
||||
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||
return;
|
||||
}
|
||||
Log.info('Edit message ${messageUpdate.senderMessageId}');
|
||||
await twonlyDB.messagesDao.handleTextEdit(
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:drift/drift.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/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
|
|
@ -20,6 +21,7 @@ Future<void> handleTextMessage(
|
|||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
content: Value(textMessage.text),
|
||||
type: const Value(MessageType.text),
|
||||
quotesMessageId: Value(
|
||||
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class Result<T, E> {
|
|||
}
|
||||
|
||||
DateTime fromTimestamp(Int64 timeStamp) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt() * 1000);
|
||||
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt());
|
||||
}
|
||||
|
||||
// ignore: strict_raw_type
|
||||
|
|
@ -88,7 +88,7 @@ Future<void> handleMediaError(MediaFile media) async {
|
|||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||
targetMediaId: message.mediaId,
|
||||
targetMessageId: message.messageId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
|
@ -129,6 +130,9 @@ class MediaFileService {
|
|||
}
|
||||
}
|
||||
|
||||
bool get imagePreviewAvailable =>
|
||||
thumbnailPath.existsSync() || storedPath.existsSync();
|
||||
|
||||
Future<void> storeMediaFile() async {
|
||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
|
|
@ -137,7 +141,25 @@ class MediaFileService {
|
|||
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);
|
||||
} else {
|
||||
Log.error(
|
||||
'Could not store image neither tempPath nor originalPath exists.',
|
||||
);
|
||||
}
|
||||
unawaited(createThumbnail());
|
||||
await updateFromDB();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,12 +195,12 @@ Future<void> updateLastMessageId(int fromUserId, String messageId) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> getPushDataFromEncryptedContent(
|
||||
Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
||||
int toUserId,
|
||||
String? messageId,
|
||||
EncryptedContent content,
|
||||
) async {
|
||||
late PushKind kind;
|
||||
PushKind? kind;
|
||||
String? reactionContent;
|
||||
|
||||
if (content.hasReaction()) {
|
||||
|
|
@ -270,6 +270,8 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
|
|||
}
|
||||
}
|
||||
|
||||
if (kind == null) return null;
|
||||
|
||||
final pushNotification = PushNotification()..kind = kind;
|
||||
if (reactionContent != null) {
|
||||
pushNotification.reactionContent = reactionContent;
|
||||
|
|
@ -277,7 +279,7 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
|
|||
if (messageId != null) {
|
||||
pushNotification.messageId = messageId;
|
||||
}
|
||||
return encryptPushNotification(toUserId, pushNotification);
|
||||
return pushNotification;
|
||||
}
|
||||
|
||||
/// this will trigger a push notification
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
|
|||
.isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) {
|
||||
return;
|
||||
}
|
||||
Log.info('Requesting new PREKEYS for $contactId');
|
||||
Log.info('[PREKEY] Requesting new PREKEYS for $contactId');
|
||||
lastPreKeyRequest = DateTime.now();
|
||||
await requestNewKeys.protect(() async {
|
||||
final otherKeys = await apiService.getPreKeysByUserId(contactId);
|
||||
if (otherKeys != null) {
|
||||
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
|
||||
.map(
|
||||
|
|
@ -50,7 +50,8 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
|
|||
.toList();
|
||||
await twonlyDB.signalDao.insertPreKeys(preKeys);
|
||||
} 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -246,11 +247,14 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
|
|||
}
|
||||
|
||||
bool isUUIDNewer(String uuid1, String uuid2) {
|
||||
try {
|
||||
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
|
||||
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
|
||||
print(timestamp1);
|
||||
print(timestamp2);
|
||||
return timestamp1 > timestamp2;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String uint8ListToHex(List<int> bytes) {
|
||||
|
|
@ -313,3 +317,34 @@ Color getMessageColorFromType(
|
|||
}
|
||||
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('-');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.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/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class SaveToGalleryButton extends StatefulWidget {
|
||||
const SaveToGalleryButton({
|
||||
required this.getMergedImage,
|
||||
required this.storeImageAsOriginal,
|
||||
required this.isLoading,
|
||||
required this.displayButtonLabel,
|
||||
required this.mediaService,
|
||||
super.key,
|
||||
});
|
||||
final Future<Uint8List?> Function() getMergedImage;
|
||||
final Future<bool> Function() storeImageAsOriginal;
|
||||
final bool displayButtonLabel;
|
||||
final MediaFileService mediaService;
|
||||
final bool isLoading;
|
||||
|
|
@ -45,6 +45,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
_imageSaving = true;
|
||||
});
|
||||
|
||||
if (widget.mediaService.mediaFile.type == MediaType.image) {
|
||||
await widget.storeImageAsOriginal();
|
||||
}
|
||||
|
||||
String? res;
|
||||
|
||||
final storedMediaPath = widget.mediaService.storedPath;
|
||||
|
|
|
|||
|
|
@ -171,20 +171,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
NotificationBadge(
|
||||
count: (media.type != MediaType.video)
|
||||
count: (media.type == MediaType.video)
|
||||
? '0'
|
||||
: media.displayLimitInMilliseconds == null
|
||||
? '∞'
|
||||
: media.displayLimitInMilliseconds.toString(),
|
||||
child: ActionButton(
|
||||
(media.type != MediaType.video)
|
||||
(media.type == MediaType.video)
|
||||
? media.displayLimitInMilliseconds == null
|
||||
? Icons.repeat_rounded
|
||||
: Icons.repeat_one_rounded
|
||||
: Icons.timer_outlined,
|
||||
tooltipText: context.lang.protectAsARealTwonly,
|
||||
onPressed: () async {
|
||||
if (media.type != MediaType.video) {
|
||||
if (media.type == MediaType.video) {
|
||||
await mediaService.setDisplayLimit(
|
||||
(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) {
|
||||
x.showCustomButtons = false;
|
||||
}
|
||||
|
|
@ -434,7 +434,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SaveToGalleryButton(
|
||||
getMergedImage: getEditedImageBytes,
|
||||
storeImageAsOriginal: storeImageAsOriginal,
|
||||
mediaService: mediaService,
|
||||
displayButtonLabel: widget.sendToGroup == null,
|
||||
isLoading: loadingImage,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: isConnected ? Container() : const ConnectionInfo(),
|
||||
),
|
||||
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)
|
||||
? Center(
|
||||
child: Padding(
|
||||
|
|
@ -223,17 +229,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
label: Text(context.lang.chatListViewSearchUserNameBtn),
|
||||
label:
|
||||
Text(context.lang.chatListViewSearchUserNameBtn),
|
||||
),
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect(force: true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: ListView.builder(
|
||||
: ListView.builder(
|
||||
itemCount: _groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||
_groupsNotPinned.length +
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
|
|
@ -36,10 +37,12 @@ class _UserListItem extends State<GroupListItem> {
|
|||
List<Message> messagesNotOpened = [];
|
||||
late StreamSubscription<List<Message>> messagesNotOpenedStream;
|
||||
|
||||
List<Message> lastMessages = [];
|
||||
late StreamSubscription<List<Message>> lastMessageStream;
|
||||
Message? lastMessage;
|
||||
late StreamSubscription<Message?> lastMessageStream;
|
||||
late StreamSubscription<List<MediaFile>> lastMediaFilesStream;
|
||||
|
||||
List<Message> previewMessages = [];
|
||||
List<MediaFile> previewMediaFiles = [];
|
||||
bool hasNonOpenedMediaFile = false;
|
||||
|
||||
@override
|
||||
|
|
@ -52,6 +55,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
void dispose() {
|
||||
messagesNotOpenedStream.cancel();
|
||||
lastMessageStream.cancel();
|
||||
lastMediaFilesStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -59,30 +63,44 @@ class _UserListItem extends State<GroupListItem> {
|
|||
lastMessageStream = twonlyDB.messagesDao
|
||||
.watchLastMessage(widget.group.groupId)
|
||||
.listen((update) {
|
||||
updateState(update, messagesNotOpened);
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(update, messagesNotOpened);
|
||||
});
|
||||
});
|
||||
|
||||
messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
.watchMessageNotOpened(widget.group.groupId)
|
||||
.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(
|
||||
List<Message> newLastMessages,
|
||||
Mutex protectUpdateState = Mutex();
|
||||
|
||||
Future<void> updateState(
|
||||
Message? newLastMessage,
|
||||
List<Message> newMessagesNotOpened,
|
||||
) {
|
||||
if (newLastMessages.isEmpty) {
|
||||
) async {
|
||||
if (newLastMessage == null) {
|
||||
// there are no messages at all
|
||||
currentMessage = null;
|
||||
previewMessages = [];
|
||||
} else if (newMessagesNotOpened.isEmpty) {
|
||||
// there are no not opened messages show just the last message in the table
|
||||
currentMessage = newLastMessages.last;
|
||||
previewMessages = newLastMessages;
|
||||
} else {
|
||||
// filter first for received messages
|
||||
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||
final receivedMessages =
|
||||
newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||
|
||||
|
|
@ -93,6 +111,10 @@ class _UserListItem extends State<GroupListItem> {
|
|||
previewMessages = newMessagesNotOpened;
|
||||
currentMessage = newMessagesNotOpened.first;
|
||||
}
|
||||
} else {
|
||||
// there are no not opened messages show just the last message in the table
|
||||
currentMessage = newLastMessage;
|
||||
previewMessages = [newLastMessage];
|
||||
}
|
||||
|
||||
final msgs =
|
||||
|
|
@ -106,7 +128,18 @@ class _UserListItem extends State<GroupListItem> {
|
|||
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;
|
||||
setState(() {
|
||||
// sets lastMessages, messagesNotOpened and currentMessage
|
||||
|
|
@ -136,7 +169,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
await startDownloadMedia(mediaFile, true);
|
||||
return;
|
||||
}
|
||||
if (mediaFile.downloadState! == DownloadState.downloaded) {
|
||||
if (mediaFile.downloadState! == DownloadState.ready) {
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
|
|
@ -184,7 +217,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
? Text(context.lang.chatsTapToSend)
|
||||
: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(previewMessages),
|
||||
MessageSendStateIcon(previewMessages, previewMediaFiles),
|
||||
const Text('•'),
|
||||
const SizedBox(width: 5),
|
||||
if (currentMessage != null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
|
@ -94,6 +95,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Mutex protectMessageUpdating = Mutex();
|
||||
|
||||
Future<void> initStreams() async {
|
||||
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
|
||||
userSub = groupStream.listen((newGroup) {
|
||||
|
|
@ -105,6 +108,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
|
||||
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
||||
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();
|
||||
|
||||
final chatItems = <ChatItem>[];
|
||||
|
|
@ -118,6 +127,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
if (msg.type == MessageType.text &&
|
||||
msg.senderId != null &&
|
||||
msg.openedAt == null) {
|
||||
if (openedMessages[msg.senderId!] == null) {
|
||||
openedMessages[msg.senderId!] = [];
|
||||
}
|
||||
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(() {
|
||||
messages = chatItems.reversed.toList();
|
||||
});
|
||||
|
|
@ -155,6 +166,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
galleryItems = items.values.toList();
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
|
|
@ -396,18 +408,8 @@ bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
|
|||
if (index <= 0) {
|
||||
return true; // If there is no previous message, return true
|
||||
}
|
||||
|
||||
final lastMessage = messages[index - 1];
|
||||
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;
|
||||
return (messages[index - 1].message?.senderId ==
|
||||
messages[index].message?.senderId);
|
||||
}
|
||||
|
||||
double calculateNumberOfLines(String text, double width, double fontSize) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart'
|
||||
hide MessageActions;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -35,14 +38,31 @@ class ChatListEntry extends StatefulWidget {
|
|||
class _ChatListEntryState extends State<ChatListEntry> {
|
||||
MediaFileService? mediaService;
|
||||
|
||||
StreamSubscription<MediaFile?>? mediaFileSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
mediaFileSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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(() {});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
|
@ -47,17 +48,20 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
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 {
|
||||
if (widget.mediaService.mediaFile.downloadState ==
|
||||
DownloadState.downloaded &&
|
||||
if (widget.mediaService.mediaFile.downloadState == DownloadState.ready &&
|
||||
widget.message.openedAt == null) {
|
||||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
|
|
@ -91,7 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
onTap: (widget.message.type == MessageType.media) ? onTap : null,
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: widget.message.mediaStored ? 271 : null,
|
||||
height: (widget.message.mediaStored &&
|
||||
widget.mediaService.imagePreviewAvailable)
|
||||
? 271
|
||||
: null,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ClipRRect(
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
bool loadIndex() {
|
||||
if (widget.message.mediaStored) {
|
||||
final index = widget.galleryItems.indexWhere(
|
||||
(x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId),
|
||||
(x) => x.mediaService.mediaFile.mediaId == (widget.message.mediaId),
|
||||
);
|
||||
if (index != -1) {
|
||||
galleryItemIndex = index;
|
||||
|
|
@ -112,7 +112,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.message.mediaStored) {
|
||||
if (!widget.message.mediaStored ||
|
||||
!widget.mediaService.imagePreviewAvailable) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 39,
|
||||
|
|
@ -130,6 +131,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
),
|
||||
child: MessageSendStateIcon(
|
||||
[widget.message],
|
||||
[widget.mediaService.mediaFile],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
canBeReopened: widget.canBeReopened,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class MessageContextMenu extends StatelessWidget {
|
|||
remove: false,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import 'dart:collection';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.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/messages.table.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/views/components/animate_icon.dart';
|
||||
|
||||
|
|
@ -18,49 +17,37 @@ enum MessageSendState {
|
|||
sending,
|
||||
}
|
||||
|
||||
Future<MessageSendState> messageSendStateFromMessage(Message msg) async {
|
||||
MessageSendState state;
|
||||
|
||||
final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
|
||||
msg.groupId,
|
||||
msg.messageId,
|
||||
MessageActionType.ackByServerAt,
|
||||
);
|
||||
|
||||
if (!ackByServer) {
|
||||
MessageSendState messageSendStateFromMessage(Message msg) {
|
||||
if (msg.senderId == null) {
|
||||
state = MessageSendState.sending;
|
||||
} else {
|
||||
state = MessageSendState.receiving;
|
||||
/// messages was send by me, look up if every messages was received by the server...
|
||||
if (msg.ackByServer == null) {
|
||||
return MessageSendState.sending;
|
||||
}
|
||||
if (msg.openedAt != null) {
|
||||
return MessageSendState.sendOpened;
|
||||
} else {
|
||||
if (msg.senderId == null) {
|
||||
// message send
|
||||
if (msg.openedAt == null) {
|
||||
state = MessageSendState.send;
|
||||
} else {
|
||||
state = MessageSendState.sendOpened;
|
||||
return MessageSendState.send;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// message received
|
||||
if (msg.openedAt == null) {
|
||||
state = MessageSendState.received;
|
||||
return MessageSendState.received;
|
||||
} else {
|
||||
state = MessageSendState.receivedOpened;
|
||||
return MessageSendState.receivedOpened;
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
class MessageSendStateIcon extends StatefulWidget {
|
||||
const MessageSendStateIcon(
|
||||
this.messages, {
|
||||
this.messages,
|
||||
this.mediaFiles, {
|
||||
super.key,
|
||||
this.canBeReopened = false,
|
||||
this.mainAxisAlignment = MainAxisAlignment.end,
|
||||
});
|
||||
final List<Message> messages;
|
||||
final List<MediaFile> mediaFiles;
|
||||
final MainAxisAlignment mainAxisAlignment;
|
||||
final bool canBeReopened;
|
||||
|
||||
|
|
@ -69,17 +56,30 @@ class MessageSendStateIcon extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||
List<Widget> icons = <Widget>[];
|
||||
String text = '';
|
||||
Widget? textWidget;
|
||||
|
||||
@override
|
||||
void 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>();
|
||||
|
||||
for (final message in widget.messages) {
|
||||
|
|
@ -87,16 +87,14 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
if (kindsAlreadyShown.contains(message.type)) continue;
|
||||
kindsAlreadyShown.add(message.type);
|
||||
|
||||
final state = await messageSendStateFromMessage(message);
|
||||
final state = messageSendStateFromMessage(message);
|
||||
|
||||
final mediaFile = message.mediaId == null
|
||||
? null
|
||||
: await MediaFileService.fromMediaId(message.mediaId!);
|
||||
: widget.mediaFiles
|
||||
.firstWhereOrNull((t) => t.mediaId == message.mediaId);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final color =
|
||||
getMessageColorFromType(message, mediaFile?.mediaFile, context);
|
||||
final color = getMessageColorFromType(message, mediaFile, context);
|
||||
|
||||
Widget icon = const Placeholder();
|
||||
textWidget = null;
|
||||
|
|
@ -126,11 +124,10 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||
text = context.lang.messageSendState_Received;
|
||||
if (message.type == MessageType.media) {
|
||||
if (mediaFile!.mediaFile.downloadState == DownloadState.pending) {
|
||||
if (mediaFile!.downloadState == DownloadState.pending) {
|
||||
text = context.lang.messageSendState_TapToLoad;
|
||||
}
|
||||
if (mediaFile.mediaFile.downloadState ==
|
||||
DownloadState.downloading) {
|
||||
if (mediaFile.downloadState == DownloadState.downloading) {
|
||||
text = context.lang.messageSendState_Loading;
|
||||
icon = getLoaderIcon(color);
|
||||
}
|
||||
|
|
@ -153,12 +150,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
}
|
||||
|
||||
if (mediaFile != null) {
|
||||
if (mediaFile.mediaFile.stored) {
|
||||
if (mediaFile.reopenByContact) {
|
||||
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
|
||||
text = context.lang.messageReopened;
|
||||
}
|
||||
|
||||
if (mediaFile.mediaFile.reuploadRequestedBy != null) {
|
||||
if (mediaFile.downloadState == DownloadState.reuploadRequested) {
|
||||
icon =
|
||||
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
||||
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();
|
||||
|
||||
var icon = icons[0];
|
||||
|
|
@ -201,18 +180,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
icon = Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
// First icon (bottom icon)
|
||||
icons[0],
|
||||
|
||||
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,
|
||||
Transform.scale(
|
||||
scale: 1.3,
|
||||
child: icons[1],
|
||||
),
|
||||
// Second icon (top icon, slightly offset)
|
||||
icons[0],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -223,7 +195,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
icon,
|
||||
const SizedBox(width: 3),
|
||||
if (textWidget != null)
|
||||
textWidget!
|
||||
textWidget
|
||||
else
|
||||
Text(
|
||||
text,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
|
@ -163,17 +162,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
// }
|
||||
|
||||
final stream =
|
||||
twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId);
|
||||
twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!);
|
||||
|
||||
var downloadTriggered = false;
|
||||
|
||||
await downloadStateListener?.cancel();
|
||||
downloadStateListener = stream.listen((updated) async {
|
||||
if (updated == null) return;
|
||||
if (updated.downloadState != DownloadState.downloaded) {
|
||||
if (updated.downloadState != DownloadState.ready) {
|
||||
if (!downloadTriggered) {
|
||||
downloadTriggered = true;
|
||||
await startDownloadMedia(currentMedia!.mediaFile, true);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao
|
||||
.getMediaFileById(allMediaFiles.first.mediaId!);
|
||||
await startDownloadMedia(mediaFile!, true);
|
||||
unawaited(tryDownloadAllMediaFiles(force: true));
|
||||
}
|
||||
return;
|
||||
|
|
@ -211,11 +212,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
[currentMessage!.messageId],
|
||||
);
|
||||
|
||||
await twonlyDB.messagesDao.updateMessageId(
|
||||
currentMessage!.messageId,
|
||||
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||
);
|
||||
|
||||
if (!currentMediaLocal.tempPath.existsSync()) {
|
||||
Log.error('Temp media file not found...');
|
||||
await handleMediaError(currentMediaLocal.mediaFile);
|
||||
|
|
@ -289,9 +285,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
pb.EncryptedContent(
|
||||
mediaUpdate: pb.EncryptedContent_MediaUpdate(
|
||||
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
|
||||
targetMediaId: currentMedia!.mediaFile.mediaId,
|
||||
targetMessageId: currentMessage!.messageId,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
setState(() {
|
||||
imageSaved = true;
|
||||
|
|
@ -537,8 +534,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (currentMedia?.mediaFile.downloadState !=
|
||||
DownloadState.downloaded)
|
||||
if (currentMedia?.mediaFile.downloadState != DownloadState.ready)
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
emoji: widget.emoji,
|
||||
),
|
||||
),
|
||||
null,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -167,9 +167,9 @@ class UserList extends StatelessWidget {
|
|||
var directChat =
|
||||
await twonlyDB.groupsDao.getDirectChat(user.userId);
|
||||
if (directChat == null) {
|
||||
await twonlyDB.groupsDao.insertGroup(
|
||||
await twonlyDB.groupsDao.createNewDirectChat(
|
||||
user.userId,
|
||||
GroupsCompanion(
|
||||
isDirectChat: const Value(true),
|
||||
groupName: Value(
|
||||
getContactDisplayName(user),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -49,12 +49,14 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
final applicationSupportDirectory =
|
||||
await getApplicationSupportDirectory();
|
||||
for (final mediaFile in mediaFiles) {
|
||||
galleryItems.add(
|
||||
MemoryItem(
|
||||
mediaService: MediaFileService(
|
||||
final mediaService = MediaFileService(
|
||||
mediaFile,
|
||||
applicationSupportDirectory: applicationSupportDirectory,
|
||||
),
|
||||
);
|
||||
if (!mediaService.imagePreviewAvailable) continue;
|
||||
galleryItems.add(
|
||||
MemoryItem(
|
||||
mediaService: mediaService,
|
||||
messages: [],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,13 +37,19 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = widget.galleryItem.mediaService;
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Hero(
|
||||
tag: widget.galleryItem.mediaService.mediaFile.mediaId,
|
||||
tag: media.mediaFile.mediaId,
|
||||
child: Stack(
|
||||
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 ==
|
||||
MediaType.video)
|
||||
const Positioned.fill(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
username: username,
|
||||
displayName: username,
|
||||
subscriptionPlan: 'Preview',
|
||||
);
|
||||
)..appVersion = 62;
|
||||
|
||||
await const FlutterSecureStorage()
|
||||
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class AutomatedTestingView extends StatefulWidget {
|
||||
|
|
@ -38,11 +39,13 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
|||
onTap: () async {
|
||||
final username = await showUserNameDialog(context);
|
||||
if (username == null) return;
|
||||
Log.info('Requested to send to $username');
|
||||
|
||||
final contacts =
|
||||
await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||
final contacts = await twonlyDB.contactsDao
|
||||
.getContactsByUsername(username.toLowerCase());
|
||||
|
||||
for (final contact in contacts) {
|
||||
Log.info('Sending to ${contact.username}');
|
||||
final group =
|
||||
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
||||
for (var i = 0; i < 200; i++) {
|
||||
|
|
@ -67,10 +70,10 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
|||
|
||||
Future<String?> showUserNameDialog(
|
||||
BuildContext context,
|
||||
) {
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
return showDialog<String>(
|
||||
await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
|
|
@ -97,4 +100,5 @@ Future<String?> showUserNameDialog(
|
|||
);
|
||||
},
|
||||
);
|
||||
return controller.text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
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/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 {
|
||||
const RetransmissionDataView({super.key});
|
||||
|
|
@ -101,11 +108,48 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
|||
Text(
|
||||
'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(
|
||||
'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(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,20 @@
|
|||
import 'dart:io';
|
||||
import 'package:drift/drift.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 {
|
||||
const DatabaseMigrationView({super.key});
|
||||
|
|
@ -8,8 +24,426 @@ class DatabaseMigrationView extends StatefulWidget {
|
|||
}
|
||||
|
||||
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
|
||||
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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/pow.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]);
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue