mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18: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:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:path/path.dart' show join;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
|
|
@ -160,14 +157,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_showDatabaseMigration = File(
|
|
||||||
join(
|
|
||||||
(await getApplicationSupportDirectory()).path,
|
|
||||||
'twonly_database.sqlite',
|
|
||||||
),
|
|
||||||
).existsSync();
|
|
||||||
|
|
||||||
_isUserCreated = await isUserCreated();
|
_isUserCreated = await isUserCreated();
|
||||||
|
|
||||||
|
if (_isUserCreated) {
|
||||||
|
if (gUser.appVersion < 62) {
|
||||||
|
_showDatabaseMigration = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -35,6 +38,15 @@ void main() async {
|
||||||
|
|
||||||
gCameras = await availableCameras();
|
gCameras = await availableCameras();
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite'))
|
||||||
|
// .deleteSync();
|
||||||
|
// } catch (e) {}
|
||||||
|
// await updateUserdata((u) {
|
||||||
|
// u.appVersion = 0;
|
||||||
|
// return u;
|
||||||
|
// });
|
||||||
|
|
||||||
apiService = ApiService();
|
apiService = ApiService();
|
||||||
twonlyDB = TwonlyDB();
|
twonlyDB = TwonlyDB();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/database/twonly_database_old.dart' as old;
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||||
|
|
||||||
part 'contacts.dao.g.dart';
|
part 'contacts.dao.g.dart';
|
||||||
|
|
@ -111,6 +112,22 @@ String getContactDisplayName(Contact user) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getContactDisplayNameOld(old.Contact user) {
|
||||||
|
var name = user.username;
|
||||||
|
if (user.nickName != null && user.nickName != '') {
|
||||||
|
name = user.nickName!;
|
||||||
|
} else if (user.displayName != null) {
|
||||||
|
name = user.displayName!;
|
||||||
|
}
|
||||||
|
if (user.deleted) {
|
||||||
|
name = applyStrikethrough(name);
|
||||||
|
}
|
||||||
|
if (name.length > 12) {
|
||||||
|
return '${name.substring(0, 12)}...';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
String applyStrikethrough(String text) {
|
String applyStrikethrough(String text) {
|
||||||
return text.split('').map((char) => '$char\u0336').join();
|
return text.split('').map((char) => '$char\u0336').join();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
part 'groups.dao.g.dart';
|
part 'groups.dao.g.dart';
|
||||||
|
|
||||||
|
|
@ -33,12 +36,46 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertGroup(GroupsCompanion group) async {
|
Future<Group?> createNewGroup(GroupsCompanion group) async {
|
||||||
await into(groups).insert(
|
final insertGroup = group.copyWith(
|
||||||
group.copyWith(
|
groupId: Value(uuid.v4()),
|
||||||
groupId: Value(uuid.v4()),
|
isGroupAdmin: const Value(true),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
return _insertGroup(insertGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Group?> createNewDirectChat(
|
||||||
|
int contactId,
|
||||||
|
GroupsCompanion group,
|
||||||
|
) async {
|
||||||
|
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId);
|
||||||
|
final insertGroup = group.copyWith(
|
||||||
|
groupId: Value(groupIdDirectChat),
|
||||||
|
isDirectChat: const Value(true),
|
||||||
|
isGroupAdmin: const Value(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await _insertGroup(insertGroup);
|
||||||
|
if (result != null) {
|
||||||
|
await into(groupMembers).insert(GroupMembersCompanion(
|
||||||
|
groupId: Value(result.groupId),
|
||||||
|
contactId: Value(
|
||||||
|
contactId,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Group?> _insertGroup(GroupsCompanion group) async {
|
||||||
|
try {
|
||||||
|
final rowId = await into(groups).insert(group);
|
||||||
|
return await (select(groups)..where((t) => t.rowId.equals(rowId)))
|
||||||
|
.getSingle();
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not insert group: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>> getGroupContact(String groupId) async {
|
Future<List<Contact>> getGroupContact(String groupId) async {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
|
|
||||||
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
|
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
|
||||||
try {
|
try {
|
||||||
final rowId = await into(mediaFiles).insert(
|
var insertMediaFile = mediaFile;
|
||||||
mediaFile.copyWith(
|
|
||||||
|
if (insertMediaFile.mediaId == const Value.absent()) {
|
||||||
|
insertMediaFile = mediaFile.copyWith(
|
||||||
mediaId: Value(uuid.v7()),
|
mediaId: Value(uuid.v7()),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
final rowId = await into(mediaFiles).insert(insertMediaFile);
|
||||||
|
|
||||||
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
|
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
|
|
@ -72,11 +76,22 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
|
|
||||||
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
|
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
|
||||||
return (select(mediaFiles)
|
return (select(mediaFiles)
|
||||||
..where((t) => t.downloadState.equals(DownloadState.pending.name)))
|
..where(
|
||||||
|
(t) =>
|
||||||
|
t.downloadState.equals(DownloadState.pending.name) |
|
||||||
|
t.downloadState.equals(DownloadState.downloading.name),
|
||||||
|
))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
||||||
return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch();
|
return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<MediaFile>> watchNewestMediaFiles() {
|
||||||
|
return (select(mediaFiles)
|
||||||
|
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||||
|
..limit(100))
|
||||||
|
.watch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,24 +38,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||||
return (select(messages)
|
final query = select(messages).join([
|
||||||
..where(
|
leftOuterJoin(mediaFiles, mediaFiles.mediaId.equalsExp(messages.mediaId)),
|
||||||
(t) =>
|
])
|
||||||
t.openedAt.isNull() &
|
..where(
|
||||||
t.groupId.equals(groupId) &
|
mediaFiles.downloadState
|
||||||
t.senderId.isNotNull() &
|
.equals(DownloadState.reuploadRequested.name)
|
||||||
t.type.equals(MessageType.media.name),
|
.not() &
|
||||||
)
|
messages.openedAt.isNull() &
|
||||||
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
messages.groupId.equals(groupId) &
|
||||||
.watch();
|
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)
|
return (select(messages)
|
||||||
..where((t) => t.groupId.equals(groupId))
|
..where((t) => t.groupId.equals(groupId))
|
||||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
|
||||||
..limit(1))
|
..limit(1))
|
||||||
.watch();
|
.watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Message>> watchByGroupId(String groupId) {
|
Stream<List<Message>> watchByGroupId(String groupId) {
|
||||||
|
|
@ -64,6 +68,16 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<MessageAction>> watchMessageActionChanges(String messageId) {
|
||||||
|
return (select(messageActions)..where((t) => t.messageId.equals(messageId)))
|
||||||
|
.watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Message?> watchMessageById(String messageId) {
|
||||||
|
return (select(messages)..where((t) => t.messageId.equals(messageId)))
|
||||||
|
.watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
// Future<void> removeOldMessages() {
|
// Future<void> removeOldMessages() {
|
||||||
// return (update(messages)
|
// return (update(messages)
|
||||||
// ..where(
|
// ..where(
|
||||||
|
|
@ -206,14 +220,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
String messageId,
|
String messageId,
|
||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
) async {
|
) async {
|
||||||
await into(messageActions).insert(
|
await into(messageActions).insertOnConflictUpdate(
|
||||||
MessageActionsCompanion(
|
MessageActionsCompanion(
|
||||||
messageId: Value(messageId),
|
messageId: Value(messageId),
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
type: const Value(MessageActionType.ackByUserAt),
|
type: const Value(MessageActionType.openedAt),
|
||||||
actionAt: Value(timestamp),
|
actionAt: Value(timestamp),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (await haveAllMembers(messageId, MessageActionType.openedAt)) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleMessageAckByServer(
|
Future<void> handleMessageAckByServer(
|
||||||
|
|
@ -221,7 +241,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
String messageId,
|
String messageId,
|
||||||
DateTime timestamp,
|
DateTime timestamp,
|
||||||
) async {
|
) async {
|
||||||
await into(messageActions).insert(
|
await into(messageActions).insertOnConflictUpdate(
|
||||||
MessageActionsCompanion(
|
MessageActionsCompanion(
|
||||||
messageId: Value(messageId),
|
messageId: Value(messageId),
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
|
|
@ -229,14 +249,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
actionAt: Value(timestamp),
|
actionAt: Value(timestamp),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (await haveAllMembers(messageId, MessageActionType.ackByServerAt)) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(ackByServer: Value(DateTime.now())),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> haveAllMembers(
|
Future<bool> haveAllMembers(
|
||||||
String groupId,
|
|
||||||
String messageId,
|
String messageId,
|
||||||
MessageActionType action,
|
MessageActionType action,
|
||||||
) async {
|
) async {
|
||||||
final members = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
final message =
|
||||||
|
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||||
|
if (message == null) return true;
|
||||||
|
final members = await twonlyDB.groupsDao.getGroupMembers(message.groupId);
|
||||||
|
|
||||||
final actions = await (select(messageActions)
|
final actions = await (select(messageActions)
|
||||||
..where(
|
..where(
|
||||||
|
|
@ -291,11 +319,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
|
|
||||||
Future<Message?> insertMessage(MessagesCompanion message) async {
|
Future<Message?> insertMessage(MessagesCompanion message) async {
|
||||||
try {
|
try {
|
||||||
final rowId = await into(messages).insert(
|
var insertMessage = message;
|
||||||
message.copyWith(
|
|
||||||
|
if (message.messageId == const Value.absent()) {
|
||||||
|
insertMessage = message.copyWith(
|
||||||
messageId: Value(uuid.v7()),
|
messageId: Value(uuid.v7()),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
final rowId = await into(messages).insert(insertMessage);
|
||||||
|
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
message.groupId.value,
|
message.groupId.value,
|
||||||
|
|
@ -323,19 +355,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reopenedMedia(String messageId) async {
|
|
||||||
await (delete(messageActions)
|
|
||||||
..where(
|
|
||||||
(t) =>
|
|
||||||
t.messageId.equals(messageId) &
|
|
||||||
t.contactId.isNull() &
|
|
||||||
t.type.equals(
|
|
||||||
MessageActionType.openedAt.name,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.go();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future<void> deleteMessagesByContactId(int contactId) {
|
// Future<void> deleteMessagesByContactId(int contactId) {
|
||||||
// return (delete(messages)
|
// return (delete(messages)
|
||||||
// ..where(
|
// ..where(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
part 'receipts.dao.g.dart';
|
part 'receipts.dao.g.dart';
|
||||||
|
|
||||||
@DriftAccessor(tables: [Receipts, Messages, MessageActions])
|
@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts])
|
||||||
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
// this constructor is required so that the main database can create an instance
|
// this constructor is required so that the main database can create an instance
|
||||||
// of this object.
|
// of this object.
|
||||||
|
|
@ -52,11 +52,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
|
|
||||||
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
||||||
try {
|
try {
|
||||||
final id = await into(receipts).insert(
|
var insertEntry = entry;
|
||||||
entry.copyWith(
|
if (entry.receiptId == const Value.absent()) {
|
||||||
|
insertEntry = entry.copyWith(
|
||||||
receiptId: Value(uuid.v4()),
|
receiptId: Value(uuid.v4()),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
final id = await into(receipts).insert(insertEntry);
|
||||||
return await (select(receipts)..where((t) => t.rowId.equals(id)))
|
return await (select(receipts)..where((t) => t.rowId.equals(id)))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -97,4 +99,26 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
|
await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
|
||||||
.write(updates);
|
.write(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isDuplicated(String receiptId) async {
|
||||||
|
return await (select(receivedReceipts)
|
||||||
|
..where((t) => t.receiptId.equals(receiptId)))
|
||||||
|
.getSingleOrNull() !=
|
||||||
|
null;
|
||||||
|
// try {
|
||||||
|
// return await (select()
|
||||||
|
// ..where(
|
||||||
|
// (t) => t.receiptId.equals(receiptId),
|
||||||
|
// ))
|
||||||
|
// .getSingleOrNull();
|
||||||
|
// } catch (e) {
|
||||||
|
// Log.error(e);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> gotReceipt(String receiptId) async {
|
||||||
|
await into(receivedReceipts)
|
||||||
|
.insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,6 @@ mixin _$ReceiptsDaoMixin on DatabaseAccessor<TwonlyDB> {
|
||||||
$MessagesTable get messages => attachedDatabase.messages;
|
$MessagesTable get messages => attachedDatabase.messages;
|
||||||
$ReceiptsTable get receipts => attachedDatabase.receipts;
|
$ReceiptsTable get receipts => attachedDatabase.receipts;
|
||||||
$MessageActionsTable get messageActions => attachedDatabase.messageActions;
|
$MessageActionsTable get messageActions => attachedDatabase.messageActions;
|
||||||
|
$ReceivedReceiptsTable get receivedReceipts =>
|
||||||
|
attachedDatabase.receivedReceipts;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
|
||||||
tbl.preKeyId.equals(preKey.preKeyId),
|
tbl.preKeyId.equals(preKey.preKeyId),
|
||||||
))
|
))
|
||||||
.go();
|
.go();
|
||||||
Log.info('Using prekey ${preKey.preKeyId} for $contactId');
|
Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId');
|
||||||
return preKey;
|
return preKey;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -68,6 +68,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
|
||||||
List<SignalContactPreKeysCompanion> preKeys,
|
List<SignalContactPreKeysCompanion> preKeys,
|
||||||
) async {
|
) async {
|
||||||
for (final preKey in preKeys) {
|
for (final preKey in preKeys) {
|
||||||
|
Log.info('[PREKEY] Inserting others ${preKey.preKeyId}');
|
||||||
try {
|
try {
|
||||||
await into(signalContactPreKeys).insert(preKey);
|
await into(signalContactPreKeys).insert(preKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,17 @@ class ConnectPreKeyStore extends PreKeyStore {
|
||||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||||
.get();
|
.get();
|
||||||
if (preKeyRecord.isEmpty) {
|
if (preKeyRecord.isEmpty) {
|
||||||
throw InvalidKeyIdException('No such preKey record! - $preKeyId');
|
throw InvalidKeyIdException(
|
||||||
|
'[PREKEY] No such preKey record! - $preKeyId');
|
||||||
}
|
}
|
||||||
Log.info('Contact used preKey $preKeyId');
|
Log.info('[PREKEY] Contact used my preKey $preKeyId');
|
||||||
final preKey = preKeyRecord.first.preKey;
|
final preKey = preKeyRecord.first.preKey;
|
||||||
return PreKeyRecord.fromBuffer(preKey);
|
return PreKeyRecord.fromBuffer(preKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removePreKey(int preKeyId) async {
|
Future<void> removePreKey(int preKeyId) async {
|
||||||
|
Log.info('[PREKEY] Removing $preKeyId from my own storage.');
|
||||||
await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
|
await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
|
||||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||||
.go();
|
.go();
|
||||||
|
|
@ -40,6 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
|
||||||
preKey: Value(record.serialize()),
|
preKey: Value(record.serialize()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Log.info('[PREKEY] Storing $preKeyId from my own storage.');
|
||||||
try {
|
try {
|
||||||
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
|
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ class Messages extends Table {
|
||||||
DateTimeColumn get openedAt => dateTime().nullable()();
|
DateTimeColumn get openedAt => dateTime().nullable()();
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
DateTimeColumn get modifiedAt => dateTime().nullable()();
|
DateTimeColumn get modifiedAt => dateTime().nullable()();
|
||||||
|
DateTimeColumn get ackByUser => dateTime().nullable()();
|
||||||
|
DateTimeColumn get ackByServer => dateTime().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {messageId};
|
Set<Column> get primaryKey => {messageId};
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,13 @@ class Receipts extends Table {
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {receiptId};
|
Set<Column> get primaryKey => {receiptId};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DataClassName('ReceivedReceipt')
|
||||||
|
class ReceivedReceipts extends Table {
|
||||||
|
TextColumn get receiptId => text()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {receiptId};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ part 'twonly.db.g.dart';
|
||||||
Groups,
|
Groups,
|
||||||
GroupMembers,
|
GroupMembers,
|
||||||
Receipts,
|
Receipts,
|
||||||
|
ReceivedReceipts,
|
||||||
SignalIdentityKeyStores,
|
SignalIdentityKeyStores,
|
||||||
SignalPreKeyStores,
|
SignalPreKeyStores,
|
||||||
SignalSenderKeyStores,
|
SignalSenderKeyStores,
|
||||||
|
|
|
||||||
|
|
@ -2299,6 +2299,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
||||||
late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>(
|
late final GeneratedColumn<DateTime> modifiedAt = GeneratedColumn<DateTime>(
|
||||||
'modified_at', aliasedName, true,
|
'modified_at', aliasedName, true,
|
||||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
static const VerificationMeta _ackByUserMeta =
|
||||||
|
const VerificationMeta('ackByUser');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> ackByUser = GeneratedColumn<DateTime>(
|
||||||
|
'ack_by_user', aliasedName, true,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
static const VerificationMeta _ackByServerMeta =
|
||||||
|
const VerificationMeta('ackByServer');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> ackByServer = GeneratedColumn<DateTime>(
|
||||||
|
'ack_by_server', aliasedName, true,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
groupId,
|
groupId,
|
||||||
|
|
@ -2313,7 +2325,9 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
||||||
isDeletedFromSender,
|
isDeletedFromSender,
|
||||||
openedAt,
|
openedAt,
|
||||||
createdAt,
|
createdAt,
|
||||||
modifiedAt
|
modifiedAt,
|
||||||
|
ackByUser,
|
||||||
|
ackByServer
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
|
@ -2387,6 +2401,18 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
||||||
modifiedAt.isAcceptableOrUnknown(
|
modifiedAt.isAcceptableOrUnknown(
|
||||||
data['modified_at']!, _modifiedAtMeta));
|
data['modified_at']!, _modifiedAtMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('ack_by_user')) {
|
||||||
|
context.handle(
|
||||||
|
_ackByUserMeta,
|
||||||
|
ackByUser.isAcceptableOrUnknown(
|
||||||
|
data['ack_by_user']!, _ackByUserMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('ack_by_server')) {
|
||||||
|
context.handle(
|
||||||
|
_ackByServerMeta,
|
||||||
|
ackByServer.isAcceptableOrUnknown(
|
||||||
|
data['ack_by_server']!, _ackByServerMeta));
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2422,6 +2448,10 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
||||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
modifiedAt: attachedDatabase.typeMapping
|
modifiedAt: attachedDatabase.typeMapping
|
||||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']),
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at']),
|
||||||
|
ackByUser: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_user']),
|
||||||
|
ackByServer: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2448,6 +2478,8 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
final DateTime? openedAt;
|
final DateTime? openedAt;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime? modifiedAt;
|
final DateTime? modifiedAt;
|
||||||
|
final DateTime? ackByUser;
|
||||||
|
final DateTime? ackByServer;
|
||||||
const Message(
|
const Message(
|
||||||
{required this.groupId,
|
{required this.groupId,
|
||||||
required this.messageId,
|
required this.messageId,
|
||||||
|
|
@ -2461,7 +2493,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
required this.isDeletedFromSender,
|
required this.isDeletedFromSender,
|
||||||
this.openedAt,
|
this.openedAt,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.modifiedAt});
|
this.modifiedAt,
|
||||||
|
this.ackByUser,
|
||||||
|
this.ackByServer});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, Expression>{};
|
final map = <String, Expression>{};
|
||||||
|
|
@ -2494,6 +2528,12 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
if (!nullToAbsent || modifiedAt != null) {
|
if (!nullToAbsent || modifiedAt != null) {
|
||||||
map['modified_at'] = Variable<DateTime>(modifiedAt);
|
map['modified_at'] = Variable<DateTime>(modifiedAt);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || ackByUser != null) {
|
||||||
|
map['ack_by_user'] = Variable<DateTime>(ackByUser);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || ackByServer != null) {
|
||||||
|
map['ack_by_server'] = Variable<DateTime>(ackByServer);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2526,6 +2566,12 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
modifiedAt: modifiedAt == null && nullToAbsent
|
modifiedAt: modifiedAt == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(modifiedAt),
|
: Value(modifiedAt),
|
||||||
|
ackByUser: ackByUser == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(ackByUser),
|
||||||
|
ackByServer: ackByServer == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(ackByServer),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2548,6 +2594,8 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
openedAt: serializer.fromJson<DateTime?>(json['openedAt']),
|
openedAt: serializer.fromJson<DateTime?>(json['openedAt']),
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']),
|
modifiedAt: serializer.fromJson<DateTime?>(json['modifiedAt']),
|
||||||
|
ackByUser: serializer.fromJson<DateTime?>(json['ackByUser']),
|
||||||
|
ackByServer: serializer.fromJson<DateTime?>(json['ackByServer']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
|
@ -2568,6 +2616,8 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
'openedAt': serializer.toJson<DateTime?>(openedAt),
|
'openedAt': serializer.toJson<DateTime?>(openedAt),
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
'modifiedAt': serializer.toJson<DateTime?>(modifiedAt),
|
'modifiedAt': serializer.toJson<DateTime?>(modifiedAt),
|
||||||
|
'ackByUser': serializer.toJson<DateTime?>(ackByUser),
|
||||||
|
'ackByServer': serializer.toJson<DateTime?>(ackByServer),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2584,7 +2634,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
bool? isDeletedFromSender,
|
bool? isDeletedFromSender,
|
||||||
Value<DateTime?> openedAt = const Value.absent(),
|
Value<DateTime?> openedAt = const Value.absent(),
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
Value<DateTime?> modifiedAt = const Value.absent()}) =>
|
Value<DateTime?> modifiedAt = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByUser = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByServer = const Value.absent()}) =>
|
||||||
Message(
|
Message(
|
||||||
groupId: groupId ?? this.groupId,
|
groupId: groupId ?? this.groupId,
|
||||||
messageId: messageId ?? this.messageId,
|
messageId: messageId ?? this.messageId,
|
||||||
|
|
@ -2602,6 +2654,8 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
openedAt: openedAt.present ? openedAt.value : this.openedAt,
|
openedAt: openedAt.present ? openedAt.value : this.openedAt,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt,
|
modifiedAt: modifiedAt.present ? modifiedAt.value : this.modifiedAt,
|
||||||
|
ackByUser: ackByUser.present ? ackByUser.value : this.ackByUser,
|
||||||
|
ackByServer: ackByServer.present ? ackByServer.value : this.ackByServer,
|
||||||
);
|
);
|
||||||
Message copyWithCompanion(MessagesCompanion data) {
|
Message copyWithCompanion(MessagesCompanion data) {
|
||||||
return Message(
|
return Message(
|
||||||
|
|
@ -2626,6 +2680,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
modifiedAt:
|
modifiedAt:
|
||||||
data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt,
|
data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt,
|
||||||
|
ackByUser: data.ackByUser.present ? data.ackByUser.value : this.ackByUser,
|
||||||
|
ackByServer:
|
||||||
|
data.ackByServer.present ? data.ackByServer.value : this.ackByServer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2644,7 +2701,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
..write('isDeletedFromSender: $isDeletedFromSender, ')
|
..write('isDeletedFromSender: $isDeletedFromSender, ')
|
||||||
..write('openedAt: $openedAt, ')
|
..write('openedAt: $openedAt, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('modifiedAt: $modifiedAt')
|
..write('modifiedAt: $modifiedAt, ')
|
||||||
|
..write('ackByUser: $ackByUser, ')
|
||||||
|
..write('ackByServer: $ackByServer')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -2663,7 +2722,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
isDeletedFromSender,
|
isDeletedFromSender,
|
||||||
openedAt,
|
openedAt,
|
||||||
createdAt,
|
createdAt,
|
||||||
modifiedAt);
|
modifiedAt,
|
||||||
|
ackByUser,
|
||||||
|
ackByServer);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|
@ -2680,7 +2741,9 @@ class Message extends DataClass implements Insertable<Message> {
|
||||||
other.isDeletedFromSender == this.isDeletedFromSender &&
|
other.isDeletedFromSender == this.isDeletedFromSender &&
|
||||||
other.openedAt == this.openedAt &&
|
other.openedAt == this.openedAt &&
|
||||||
other.createdAt == this.createdAt &&
|
other.createdAt == this.createdAt &&
|
||||||
other.modifiedAt == this.modifiedAt);
|
other.modifiedAt == this.modifiedAt &&
|
||||||
|
other.ackByUser == this.ackByUser &&
|
||||||
|
other.ackByServer == this.ackByServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesCompanion extends UpdateCompanion<Message> {
|
class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
|
|
@ -2697,6 +2760,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
final Value<DateTime?> openedAt;
|
final Value<DateTime?> openedAt;
|
||||||
final Value<DateTime> createdAt;
|
final Value<DateTime> createdAt;
|
||||||
final Value<DateTime?> modifiedAt;
|
final Value<DateTime?> modifiedAt;
|
||||||
|
final Value<DateTime?> ackByUser;
|
||||||
|
final Value<DateTime?> ackByServer;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
const MessagesCompanion({
|
const MessagesCompanion({
|
||||||
this.groupId = const Value.absent(),
|
this.groupId = const Value.absent(),
|
||||||
|
|
@ -2712,6 +2777,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
this.openedAt = const Value.absent(),
|
this.openedAt = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.modifiedAt = const Value.absent(),
|
this.modifiedAt = const Value.absent(),
|
||||||
|
this.ackByUser = const Value.absent(),
|
||||||
|
this.ackByServer = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
});
|
});
|
||||||
MessagesCompanion.insert({
|
MessagesCompanion.insert({
|
||||||
|
|
@ -2728,6 +2795,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
this.openedAt = const Value.absent(),
|
this.openedAt = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.modifiedAt = const Value.absent(),
|
this.modifiedAt = const Value.absent(),
|
||||||
|
this.ackByUser = const Value.absent(),
|
||||||
|
this.ackByServer = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
}) : groupId = Value(groupId),
|
}) : groupId = Value(groupId),
|
||||||
messageId = Value(messageId),
|
messageId = Value(messageId),
|
||||||
|
|
@ -2746,6 +2815,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
Expression<DateTime>? openedAt,
|
Expression<DateTime>? openedAt,
|
||||||
Expression<DateTime>? createdAt,
|
Expression<DateTime>? createdAt,
|
||||||
Expression<DateTime>? modifiedAt,
|
Expression<DateTime>? modifiedAt,
|
||||||
|
Expression<DateTime>? ackByUser,
|
||||||
|
Expression<DateTime>? ackByServer,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
|
|
@ -2763,6 +2834,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
if (openedAt != null) 'opened_at': openedAt,
|
if (openedAt != null) 'opened_at': openedAt,
|
||||||
if (createdAt != null) 'created_at': createdAt,
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
if (modifiedAt != null) 'modified_at': modifiedAt,
|
if (modifiedAt != null) 'modified_at': modifiedAt,
|
||||||
|
if (ackByUser != null) 'ack_by_user': ackByUser,
|
||||||
|
if (ackByServer != null) 'ack_by_server': ackByServer,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2781,6 +2854,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
Value<DateTime?>? openedAt,
|
Value<DateTime?>? openedAt,
|
||||||
Value<DateTime>? createdAt,
|
Value<DateTime>? createdAt,
|
||||||
Value<DateTime?>? modifiedAt,
|
Value<DateTime?>? modifiedAt,
|
||||||
|
Value<DateTime?>? ackByUser,
|
||||||
|
Value<DateTime?>? ackByServer,
|
||||||
Value<int>? rowid}) {
|
Value<int>? rowid}) {
|
||||||
return MessagesCompanion(
|
return MessagesCompanion(
|
||||||
groupId: groupId ?? this.groupId,
|
groupId: groupId ?? this.groupId,
|
||||||
|
|
@ -2796,6 +2871,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
openedAt: openedAt ?? this.openedAt,
|
openedAt: openedAt ?? this.openedAt,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
modifiedAt: modifiedAt ?? this.modifiedAt,
|
modifiedAt: modifiedAt ?? this.modifiedAt,
|
||||||
|
ackByUser: ackByUser ?? this.ackByUser,
|
||||||
|
ackByServer: ackByServer ?? this.ackByServer,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -2843,6 +2920,12 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
if (modifiedAt.present) {
|
if (modifiedAt.present) {
|
||||||
map['modified_at'] = Variable<DateTime>(modifiedAt.value);
|
map['modified_at'] = Variable<DateTime>(modifiedAt.value);
|
||||||
}
|
}
|
||||||
|
if (ackByUser.present) {
|
||||||
|
map['ack_by_user'] = Variable<DateTime>(ackByUser.value);
|
||||||
|
}
|
||||||
|
if (ackByServer.present) {
|
||||||
|
map['ack_by_server'] = Variable<DateTime>(ackByServer.value);
|
||||||
|
}
|
||||||
if (rowid.present) {
|
if (rowid.present) {
|
||||||
map['rowid'] = Variable<int>(rowid.value);
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
}
|
}
|
||||||
|
|
@ -2865,6 +2948,8 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
||||||
..write('openedAt: $openedAt, ')
|
..write('openedAt: $openedAt, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('modifiedAt: $modifiedAt, ')
|
..write('modifiedAt: $modifiedAt, ')
|
||||||
|
..write('ackByUser: $ackByUser, ')
|
||||||
|
..write('ackByServer: $ackByServer, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
|
|
@ -4243,6 +4328,200 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $ReceivedReceiptsTable extends ReceivedReceipts
|
||||||
|
with TableInfo<$ReceivedReceiptsTable, ReceivedReceipt> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$ReceivedReceiptsTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _receiptIdMeta =
|
||||||
|
const VerificationMeta('receiptId');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> receiptId = GeneratedColumn<String>(
|
||||||
|
'receipt_id', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _createdAtMeta =
|
||||||
|
const VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [receiptId, createdAt];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'received_receipts';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<ReceivedReceipt> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('receipt_id')) {
|
||||||
|
context.handle(_receiptIdMeta,
|
||||||
|
receiptId.isAcceptableOrUnknown(data['receipt_id']!, _receiptIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_receiptIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {receiptId};
|
||||||
|
@override
|
||||||
|
ReceivedReceipt map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return ReceivedReceipt(
|
||||||
|
receiptId: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}receipt_id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ReceivedReceiptsTable createAlias(String alias) {
|
||||||
|
return $ReceivedReceiptsTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReceivedReceipt extends DataClass implements Insertable<ReceivedReceipt> {
|
||||||
|
final String receiptId;
|
||||||
|
final DateTime createdAt;
|
||||||
|
const ReceivedReceipt({required this.receiptId, required this.createdAt});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['receipt_id'] = Variable<String>(receiptId);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceivedReceiptsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return ReceivedReceiptsCompanion(
|
||||||
|
receiptId: Value(receiptId),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ReceivedReceipt.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ReceivedReceipt(
|
||||||
|
receiptId: serializer.fromJson<String>(json['receiptId']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'receiptId': serializer.toJson<String>(receiptId),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceivedReceipt copyWith({String? receiptId, DateTime? createdAt}) =>
|
||||||
|
ReceivedReceipt(
|
||||||
|
receiptId: receiptId ?? this.receiptId,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
ReceivedReceipt copyWithCompanion(ReceivedReceiptsCompanion data) {
|
||||||
|
return ReceivedReceipt(
|
||||||
|
receiptId: data.receiptId.present ? data.receiptId.value : this.receiptId,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ReceivedReceipt(')
|
||||||
|
..write('receiptId: $receiptId, ')
|
||||||
|
..write('createdAt: $createdAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(receiptId, createdAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ReceivedReceipt &&
|
||||||
|
other.receiptId == this.receiptId &&
|
||||||
|
other.createdAt == this.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReceivedReceiptsCompanion extends UpdateCompanion<ReceivedReceipt> {
|
||||||
|
final Value<String> receiptId;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<int> rowid;
|
||||||
|
const ReceivedReceiptsCompanion({
|
||||||
|
this.receiptId = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
});
|
||||||
|
ReceivedReceiptsCompanion.insert({
|
||||||
|
required String receiptId,
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.rowid = const Value.absent(),
|
||||||
|
}) : receiptId = Value(receiptId);
|
||||||
|
static Insertable<ReceivedReceipt> custom({
|
||||||
|
Expression<String>? receiptId,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<int>? rowid,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (receiptId != null) 'receipt_id': receiptId,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceivedReceiptsCompanion copyWith(
|
||||||
|
{Value<String>? receiptId,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
Value<int>? rowid}) {
|
||||||
|
return ReceivedReceiptsCompanion(
|
||||||
|
receiptId: receiptId ?? this.receiptId,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
rowid: rowid ?? this.rowid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (receiptId.present) {
|
||||||
|
map['receipt_id'] = Variable<String>(receiptId.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (rowid.present) {
|
||||||
|
map['rowid'] = Variable<int>(rowid.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ReceivedReceiptsCompanion(')
|
||||||
|
..write('receiptId: $receiptId, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('rowid: $rowid')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores
|
class $SignalIdentityKeyStoresTable extends SignalIdentityKeyStores
|
||||||
with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> {
|
with TableInfo<$SignalIdentityKeyStoresTable, SignalIdentityKeyStore> {
|
||||||
@override
|
@override
|
||||||
|
|
@ -6130,6 +6409,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
||||||
late final $ReactionsTable reactions = $ReactionsTable(this);
|
late final $ReactionsTable reactions = $ReactionsTable(this);
|
||||||
late final $GroupMembersTable groupMembers = $GroupMembersTable(this);
|
late final $GroupMembersTable groupMembers = $GroupMembersTable(this);
|
||||||
late final $ReceiptsTable receipts = $ReceiptsTable(this);
|
late final $ReceiptsTable receipts = $ReceiptsTable(this);
|
||||||
|
late final $ReceivedReceiptsTable receivedReceipts =
|
||||||
|
$ReceivedReceiptsTable(this);
|
||||||
late final $SignalIdentityKeyStoresTable signalIdentityKeyStores =
|
late final $SignalIdentityKeyStoresTable signalIdentityKeyStores =
|
||||||
$SignalIdentityKeyStoresTable(this);
|
$SignalIdentityKeyStoresTable(this);
|
||||||
late final $SignalPreKeyStoresTable signalPreKeyStores =
|
late final $SignalPreKeyStoresTable signalPreKeyStores =
|
||||||
|
|
@ -6163,6 +6444,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
|
||||||
reactions,
|
reactions,
|
||||||
groupMembers,
|
groupMembers,
|
||||||
receipts,
|
receipts,
|
||||||
|
receivedReceipts,
|
||||||
signalIdentityKeyStores,
|
signalIdentityKeyStores,
|
||||||
signalPreKeyStores,
|
signalPreKeyStores,
|
||||||
signalSenderKeyStores,
|
signalSenderKeyStores,
|
||||||
|
|
@ -7854,6 +8136,8 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
|
||||||
Value<DateTime?> openedAt,
|
Value<DateTime?> openedAt,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<DateTime?> modifiedAt,
|
Value<DateTime?> modifiedAt,
|
||||||
|
Value<DateTime?> ackByUser,
|
||||||
|
Value<DateTime?> ackByServer,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
||||||
|
|
@ -7870,6 +8154,8 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
||||||
Value<DateTime?> openedAt,
|
Value<DateTime?> openedAt,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<DateTime?> modifiedAt,
|
Value<DateTime?> modifiedAt,
|
||||||
|
Value<DateTime?> ackByUser,
|
||||||
|
Value<DateTime?> ackByServer,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -8042,6 +8328,12 @@ class $$MessagesTableFilterComposer
|
||||||
ColumnFilters<DateTime> get modifiedAt => $composableBuilder(
|
ColumnFilters<DateTime> get modifiedAt => $composableBuilder(
|
||||||
column: $table.modifiedAt, builder: (column) => ColumnFilters(column));
|
column: $table.modifiedAt, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get ackByUser => $composableBuilder(
|
||||||
|
column: $table.ackByUser, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get ackByServer => $composableBuilder(
|
||||||
|
column: $table.ackByServer, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
$$GroupsTableFilterComposer get groupId {
|
$$GroupsTableFilterComposer get groupId {
|
||||||
final $$GroupsTableFilterComposer composer = $composerBuilder(
|
final $$GroupsTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
|
|
@ -8245,6 +8537,12 @@ class $$MessagesTableOrderingComposer
|
||||||
ColumnOrderings<DateTime> get modifiedAt => $composableBuilder(
|
ColumnOrderings<DateTime> get modifiedAt => $composableBuilder(
|
||||||
column: $table.modifiedAt, builder: (column) => ColumnOrderings(column));
|
column: $table.modifiedAt, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get ackByUser => $composableBuilder(
|
||||||
|
column: $table.ackByUser, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get ackByServer => $composableBuilder(
|
||||||
|
column: $table.ackByServer, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
$$GroupsTableOrderingComposer get groupId {
|
$$GroupsTableOrderingComposer get groupId {
|
||||||
final $$GroupsTableOrderingComposer composer = $composerBuilder(
|
final $$GroupsTableOrderingComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
|
|
@ -8362,6 +8660,12 @@ class $$MessagesTableAnnotationComposer
|
||||||
GeneratedColumn<DateTime> get modifiedAt => $composableBuilder(
|
GeneratedColumn<DateTime> get modifiedAt => $composableBuilder(
|
||||||
column: $table.modifiedAt, builder: (column) => column);
|
column: $table.modifiedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get ackByUser =>
|
||||||
|
$composableBuilder(column: $table.ackByUser, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get ackByServer => $composableBuilder(
|
||||||
|
column: $table.ackByServer, builder: (column) => column);
|
||||||
|
|
||||||
$$GroupsTableAnnotationComposer get groupId {
|
$$GroupsTableAnnotationComposer get groupId {
|
||||||
final $$GroupsTableAnnotationComposer composer = $composerBuilder(
|
final $$GroupsTableAnnotationComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
|
|
@ -8571,6 +8875,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
||||||
Value<DateTime?> openedAt = const Value.absent(),
|
Value<DateTime?> openedAt = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<DateTime?> modifiedAt = const Value.absent(),
|
Value<DateTime?> modifiedAt = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByUser = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByServer = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
|
|
@ -8587,6 +8893,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
||||||
openedAt: openedAt,
|
openedAt: openedAt,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
modifiedAt: modifiedAt,
|
modifiedAt: modifiedAt,
|
||||||
|
ackByUser: ackByUser,
|
||||||
|
ackByServer: ackByServer,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
|
|
@ -8603,6 +8911,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
||||||
Value<DateTime?> openedAt = const Value.absent(),
|
Value<DateTime?> openedAt = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<DateTime?> modifiedAt = const Value.absent(),
|
Value<DateTime?> modifiedAt = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByUser = const Value.absent(),
|
||||||
|
Value<DateTime?> ackByServer = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
MessagesCompanion.insert(
|
MessagesCompanion.insert(
|
||||||
|
|
@ -8619,6 +8929,8 @@ class $$MessagesTableTableManager extends RootTableManager<
|
||||||
openedAt: openedAt,
|
openedAt: openedAt,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
modifiedAt: modifiedAt,
|
modifiedAt: modifiedAt,
|
||||||
|
ackByUser: ackByUser,
|
||||||
|
ackByServer: ackByServer,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
|
|
@ -10060,6 +10372,135 @@ typedef $$ReceiptsTableProcessedTableManager = ProcessedTableManager<
|
||||||
(Receipt, $$ReceiptsTableReferences),
|
(Receipt, $$ReceiptsTableReferences),
|
||||||
Receipt,
|
Receipt,
|
||||||
PrefetchHooks Function({bool contactId, bool messageId})>;
|
PrefetchHooks Function({bool contactId, bool messageId})>;
|
||||||
|
typedef $$ReceivedReceiptsTableCreateCompanionBuilder
|
||||||
|
= ReceivedReceiptsCompanion Function({
|
||||||
|
required String receiptId,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
typedef $$ReceivedReceiptsTableUpdateCompanionBuilder
|
||||||
|
= ReceivedReceiptsCompanion Function({
|
||||||
|
Value<String> receiptId,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<int> rowid,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$ReceivedReceiptsTableFilterComposer
|
||||||
|
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
|
||||||
|
$$ReceivedReceiptsTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<String> get receiptId => $composableBuilder(
|
||||||
|
column: $table.receiptId, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ReceivedReceiptsTableOrderingComposer
|
||||||
|
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
|
||||||
|
$$ReceivedReceiptsTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<String> get receiptId => $composableBuilder(
|
||||||
|
column: $table.receiptId, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ReceivedReceiptsTableAnnotationComposer
|
||||||
|
extends Composer<_$TwonlyDB, $ReceivedReceiptsTable> {
|
||||||
|
$$ReceivedReceiptsTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<String> get receiptId =>
|
||||||
|
$composableBuilder(column: $table.receiptId, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ReceivedReceiptsTableTableManager extends RootTableManager<
|
||||||
|
_$TwonlyDB,
|
||||||
|
$ReceivedReceiptsTable,
|
||||||
|
ReceivedReceipt,
|
||||||
|
$$ReceivedReceiptsTableFilterComposer,
|
||||||
|
$$ReceivedReceiptsTableOrderingComposer,
|
||||||
|
$$ReceivedReceiptsTableAnnotationComposer,
|
||||||
|
$$ReceivedReceiptsTableCreateCompanionBuilder,
|
||||||
|
$$ReceivedReceiptsTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ReceivedReceipt,
|
||||||
|
BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt>
|
||||||
|
),
|
||||||
|
ReceivedReceipt,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$$ReceivedReceiptsTableTableManager(
|
||||||
|
_$TwonlyDB db, $ReceivedReceiptsTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$ReceivedReceiptsTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$ReceivedReceiptsTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$ReceivedReceiptsTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<String> receiptId = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ReceivedReceiptsCompanion(
|
||||||
|
receiptId: receiptId,
|
||||||
|
createdAt: createdAt,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String receiptId,
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<int> rowid = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ReceivedReceiptsCompanion.insert(
|
||||||
|
receiptId: receiptId,
|
||||||
|
createdAt: createdAt,
|
||||||
|
rowid: rowid,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$ReceivedReceiptsTableProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$TwonlyDB,
|
||||||
|
$ReceivedReceiptsTable,
|
||||||
|
ReceivedReceipt,
|
||||||
|
$$ReceivedReceiptsTableFilterComposer,
|
||||||
|
$$ReceivedReceiptsTableOrderingComposer,
|
||||||
|
$$ReceivedReceiptsTableAnnotationComposer,
|
||||||
|
$$ReceivedReceiptsTableCreateCompanionBuilder,
|
||||||
|
$$ReceivedReceiptsTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ReceivedReceipt,
|
||||||
|
BaseReferences<_$TwonlyDB, $ReceivedReceiptsTable, ReceivedReceipt>
|
||||||
|
),
|
||||||
|
ReceivedReceipt,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder
|
typedef $$SignalIdentityKeyStoresTableCreateCompanionBuilder
|
||||||
= SignalIdentityKeyStoresCompanion Function({
|
= SignalIdentityKeyStoresCompanion Function({
|
||||||
required int deviceId,
|
required int deviceId,
|
||||||
|
|
@ -11497,6 +11938,8 @@ class $TwonlyDBManager {
|
||||||
$$GroupMembersTableTableManager(_db, _db.groupMembers);
|
$$GroupMembersTableTableManager(_db, _db.groupMembers);
|
||||||
$$ReceiptsTableTableManager get receipts =>
|
$$ReceiptsTableTableManager get receipts =>
|
||||||
$$ReceiptsTableTableManager(_db, _db.receipts);
|
$$ReceiptsTableTableManager(_db, _db.receipts);
|
||||||
|
$$ReceivedReceiptsTableTableManager get receivedReceipts =>
|
||||||
|
$$ReceivedReceiptsTableTableManager(_db, _db.receivedReceipts);
|
||||||
$$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores =>
|
$$SignalIdentityKeyStoresTableTableManager get signalIdentityKeyStores =>
|
||||||
$$SignalIdentityKeyStoresTableTableManager(
|
$$SignalIdentityKeyStoresTableTableManager(
|
||||||
_db, _db.signalIdentityKeyStores);
|
_db, _db.signalIdentityKeyStores);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ class UserData {
|
||||||
String? avatarSvg;
|
String? avatarSvg;
|
||||||
String? avatarJson;
|
String? avatarJson;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: 0)
|
||||||
|
int appVersion = 0;
|
||||||
|
|
||||||
@JsonKey(defaultValue: 0)
|
@JsonKey(defaultValue: 0)
|
||||||
int avatarCounter = 0;
|
int avatarCounter = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
||||||
)
|
)
|
||||||
..avatarSvg = json['avatarSvg'] as String?
|
..avatarSvg = json['avatarSvg'] as String?
|
||||||
..avatarJson = json['avatarJson'] as String?
|
..avatarJson = json['avatarJson'] as String?
|
||||||
|
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
|
||||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||||
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
..isDeveloper = json['isDeveloper'] as bool? ?? false
|
||||||
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
|
||||||
|
|
@ -77,6 +78,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'displayName': instance.displayName,
|
'displayName': instance.displayName,
|
||||||
'avatarSvg': instance.avatarSvg,
|
'avatarSvg': instance.avatarSvg,
|
||||||
'avatarJson': instance.avatarJson,
|
'avatarJson': instance.avatarJson,
|
||||||
|
'appVersion': instance.appVersion,
|
||||||
'avatarCounter': instance.avatarCounter,
|
'avatarCounter': instance.avatarCounter,
|
||||||
'isDeveloper': instance.isDeveloper,
|
'isDeveloper': instance.isDeveloper,
|
||||||
'deviceId': instance.deviceId,
|
'deviceId': instance.deviceId,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class MemoryItem {
|
||||||
final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
|
final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
|
||||||
if (mediaService == null) continue;
|
if (mediaService == null) continue;
|
||||||
|
|
||||||
|
if (!mediaService.imagePreviewAvailable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
.putIfAbsent(
|
.putIfAbsent(
|
||||||
message.mediaId!,
|
message.mediaId!,
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
||||||
factory EncryptedContent_MessageUpdate({
|
factory EncryptedContent_MessageUpdate({
|
||||||
EncryptedContent_MessageUpdate_Type? type,
|
EncryptedContent_MessageUpdate_Type? type,
|
||||||
$core.String? senderMessageId,
|
$core.String? senderMessageId,
|
||||||
$core.Iterable<$core.String>? multipleSenderMessageIds,
|
$core.Iterable<$core.String>? multipleTargetMessageIds,
|
||||||
$core.String? text,
|
$core.String? text,
|
||||||
$fixnum.Int64? timestamp,
|
$fixnum.Int64? timestamp,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -399,8 +399,8 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
||||||
if (senderMessageId != null) {
|
if (senderMessageId != null) {
|
||||||
$result.senderMessageId = senderMessageId;
|
$result.senderMessageId = senderMessageId;
|
||||||
}
|
}
|
||||||
if (multipleSenderMessageIds != null) {
|
if (multipleTargetMessageIds != null) {
|
||||||
$result.multipleSenderMessageIds.addAll(multipleSenderMessageIds);
|
$result.multipleTargetMessageIds.addAll(multipleTargetMessageIds);
|
||||||
}
|
}
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
$result.text = text;
|
$result.text = text;
|
||||||
|
|
@ -417,7 +417,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create)
|
||||||
..e<EncryptedContent_MessageUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values)
|
..e<EncryptedContent_MessageUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values)
|
||||||
..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
|
..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
|
||||||
..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds')
|
..pPS(3, _omitFieldNames ? '' : 'multipleTargetMessageIds', protoName: 'multipleTargetMessageIds')
|
||||||
..aOS(4, _omitFieldNames ? '' : 'text')
|
..aOS(4, _omitFieldNames ? '' : 'text')
|
||||||
..aInt64(5, _omitFieldNames ? '' : 'timestamp')
|
..aInt64(5, _omitFieldNames ? '' : 'timestamp')
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
|
|
@ -463,7 +463,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
|
||||||
void clearSenderMessageId() => clearField(2);
|
void clearSenderMessageId() => clearField(2);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.List<$core.String> get multipleSenderMessageIds => $_getList(2);
|
$core.List<$core.String> get multipleTargetMessageIds => $_getList(2);
|
||||||
|
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$core.String get text => $_getSZ(3);
|
$core.String get text => $_getSZ(3);
|
||||||
|
|
@ -663,14 +663,14 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
|
||||||
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||||
factory EncryptedContent_MediaUpdate({
|
factory EncryptedContent_MediaUpdate({
|
||||||
EncryptedContent_MediaUpdate_Type? type,
|
EncryptedContent_MediaUpdate_Type? type,
|
||||||
$core.String? targetMediaId,
|
$core.String? targetMessageId,
|
||||||
}) {
|
}) {
|
||||||
final $result = create();
|
final $result = create();
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
$result.type = type;
|
$result.type = type;
|
||||||
}
|
}
|
||||||
if (targetMediaId != null) {
|
if (targetMessageId != null) {
|
||||||
$result.targetMediaId = targetMediaId;
|
$result.targetMessageId = targetMessageId;
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
@ -680,7 +680,7 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create)
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MediaUpdate', createEmptyInstance: create)
|
||||||
..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values)
|
..e<EncryptedContent_MediaUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MediaUpdate_Type.REOPENED, valueOf: EncryptedContent_MediaUpdate_Type.valueOf, enumValues: EncryptedContent_MediaUpdate_Type.values)
|
||||||
..aOS(2, _omitFieldNames ? '' : 'targetMediaId', protoName: 'targetMediaId')
|
..aOS(2, _omitFieldNames ? '' : 'targetMessageId', protoName: 'targetMessageId')
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -715,13 +715,13 @@ class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
|
||||||
void clearType() => clearField(1);
|
void clearType() => clearField(1);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.String get targetMediaId => $_getSZ(1);
|
$core.String get targetMessageId => $_getSZ(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set targetMediaId($core.String v) { $_setString(1, v); }
|
set targetMessageId($core.String v) { $_setString(1, v); }
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasTargetMediaId() => $_has(1);
|
$core.bool hasTargetMessageId() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearTargetMediaId() => clearField(2);
|
void clearTargetMessageId() => clearField(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
|
class EncryptedContent_ContactRequest extends $pb.GeneratedMessage {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ const EncryptedContent_MessageUpdate$json = {
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'},
|
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'},
|
||||||
{'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true},
|
{'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true},
|
||||||
{'1': 'multipleSenderMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleSenderMessageIds'},
|
{'1': 'multipleTargetMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleTargetMessageIds'},
|
||||||
{'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true},
|
{'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true},
|
||||||
{'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
|
{'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
|
||||||
],
|
],
|
||||||
|
|
@ -221,7 +221,7 @@ const EncryptedContent_MediaUpdate$json = {
|
||||||
'1': 'MediaUpdate',
|
'1': 'MediaUpdate',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'},
|
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MediaUpdate.Type', '10': 'type'},
|
||||||
{'1': 'targetMediaId', '3': 2, '4': 1, '5': 9, '10': 'targetMediaId'},
|
{'1': 'targetMessageId', '3': 2, '4': 1, '5': 9, '10': 'targetMessageId'},
|
||||||
],
|
],
|
||||||
'4': [EncryptedContent_MediaUpdate_Type$json],
|
'4': [EncryptedContent_MediaUpdate_Type$json],
|
||||||
};
|
};
|
||||||
|
|
@ -338,8 +338,8 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||||
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
|
'AFIFZW1vammIAQESGwoGcmVtb3ZlGAMgASgISAFSBnJlbW92ZYgBAUIICgZfZW1vamlCCQoHX3'
|
||||||
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
|
'JlbW92ZRq3AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVu'
|
||||||
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
|
'dC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLQoPc2VuZGVyTWVzc2FnZUlkGAIgASgJSABSD3'
|
||||||
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMYAyADKAlSGG11'
|
'NlbmRlck1lc3NhZ2VJZIgBARI6ChhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMYAyADKAlSGG11'
|
||||||
'bHRpcGxlU2VuZGVyTWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
|
'bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZX'
|
||||||
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
|
'N0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQ'
|
||||||
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
|
'ARIKCgZPUEVORUQQAkISChBfc2VuZGVyTWVzc2FnZUlkQgcKBV90ZXh0GowFCgVNZWRpYRIoCg'
|
||||||
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
|
'9zZW5kZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwu'
|
||||||
|
|
@ -353,24 +353,24 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||||
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
|
'VSD2VuY3J5cHRpb25Ob25jZYgBASIzCgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJ'
|
||||||
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
|
'CgVWSURFTxACEgcKA0dJRhADQh0KG19kaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcX'
|
||||||
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
|
'VvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2Vu'
|
||||||
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqjAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
|
'Y3J5cHRpb25NYWNCEgoQX2VuY3J5cHRpb25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZR'
|
||||||
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIkCg10YXJn'
|
'gBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJn'
|
||||||
'ZXRNZWRpYUlkGAIgASgJUg10YXJnZXRNZWRpYUlkIjYKBFR5cGUSDAoIUkVPUEVORUQQABIKCg'
|
'ZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEA'
|
||||||
'ZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QSOQoEdHlw'
|
'ASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkK'
|
||||||
'ZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdHlwZSIrCg'
|
'BHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cG'
|
||||||
'RUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhrSAQoNQ29udGFjdFVw'
|
'UiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIa0gEKDUNvbnRh'
|
||||||
'ZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlLlR5cG'
|
'Y3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS'
|
||||||
'VSBHR5cGUSIQoJYXZhdGFyU3ZnGAIgASgJSABSCWF2YXRhclN2Z4gBARIlCgtkaXNwbGF5TmFt'
|
'5UeXBlUgR0eXBlEiEKCWF2YXRhclN2ZxgCIAEoCUgAUglhdmF0YXJTdmeIAQESJQoLZGlzcGxh'
|
||||||
'ZRgDIAEoCUgBUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVE'
|
'eU5hbWUYAyABKAlIAVILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVV'
|
||||||
'UQAUIMCgpfYXZhdGFyU3ZnQg4KDF9kaXNwbGF5TmFtZRrVAQoIUHVzaEtleXMSMwoEdHlwZRgB'
|
'BEQVRFEAFCDAoKX2F2YXRhclN2Z0IOCgxfZGlzcGxheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5'
|
||||||
'IAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIA'
|
'cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SW'
|
||||||
'EoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEo'
|
'QYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSAFSA2tleYgBARIhCgljcmVhdGVkQXQY'
|
||||||
'A0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2'
|
'BCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQg'
|
||||||
'tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRl'
|
'gKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNv'
|
||||||
'chgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFm'
|
'dW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgAS'
|
||||||
'xhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIK'
|
'gDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmll'
|
||||||
'CghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg'
|
'bmRCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZX'
|
||||||
'5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBk'
|
'JCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFj'
|
||||||
'YXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcm'
|
'dFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCw'
|
||||||
'VhY3Rpb25CDgoMX3RleHRNZXNzYWdl');
|
'oJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZQ==');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ message EncryptedContent {
|
||||||
}
|
}
|
||||||
Type type = 1;
|
Type type = 1;
|
||||||
optional string senderMessageId = 2;
|
optional string senderMessageId = 2;
|
||||||
repeated string multipleSenderMessageIds = 3;
|
repeated string multipleTargetMessageIds = 3;
|
||||||
optional string text = 4;
|
optional string text = 4;
|
||||||
int64 timestamp = 5;
|
int64 timestamp = 5;
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ message EncryptedContent {
|
||||||
DECRYPTION_ERROR = 2;
|
DECRYPTION_ERROR = 2;
|
||||||
}
|
}
|
||||||
Type type = 1;
|
Type type = 1;
|
||||||
string targetMediaId = 2;
|
string targetMessageId = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ContactRequest {
|
message ContactRequest {
|
||||||
|
|
|
||||||
|
|
@ -156,11 +156,6 @@ class ApiService {
|
||||||
}
|
}
|
||||||
reconnectionTimer?.cancel();
|
reconnectionTimer?.cancel();
|
||||||
reconnectionTimer = null;
|
reconnectionTimer = null;
|
||||||
final user = await getUser();
|
|
||||||
if (user != null) {
|
|
||||||
globalCallbackConnectionState(isConnected: true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return lockConnecting.protect<bool>(() async {
|
return lockConnecting.protect<bool>(() async {
|
||||||
if (_channel != null) {
|
if (_channel != null) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failed) {
|
if (failed) {
|
||||||
|
Log.error('Background media upload failed: ${update.status}');
|
||||||
await requestMediaReupload(mediaId);
|
await requestMediaReupload(mediaId);
|
||||||
} else {
|
} else {
|
||||||
await handleEncryptedFile(mediaId);
|
await handleEncryptedFile(mediaId);
|
||||||
|
|
@ -194,6 +195,9 @@ Future<void> downloadFileFast(
|
||||||
if (response.statusCode == 404 ||
|
if (response.statusCode == 404 ||
|
||||||
response.statusCode == 403 ||
|
response.statusCode == 403 ||
|
||||||
response.statusCode == 400) {
|
response.statusCode == 400) {
|
||||||
|
Log.error(
|
||||||
|
'Got ${response.statusCode} from server. Requesting upload again',
|
||||||
|
);
|
||||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
||||||
await requestMediaReupload(media.mediaId);
|
await requestMediaReupload(media.mediaId);
|
||||||
return;
|
return;
|
||||||
|
|
@ -217,7 +221,7 @@ Future<void> requestMediaReupload(String mediaId) async {
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||||
targetMediaId: mediaId,
|
targetMessageId: messages.first.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,12 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
final mediaId = update.task.taskId.replaceAll('upload_', '');
|
final mediaId = update.task.taskId.replaceAll('upload_', '');
|
||||||
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||||
|
|
||||||
|
if (update.status == TaskStatus.enqueued ||
|
||||||
|
update.status == TaskStatus.running) {
|
||||||
|
// Ignore these updates
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'Got an upload task but no upload media in the media upload database',
|
'Got an upload task but no upload media in the media upload database',
|
||||||
|
|
@ -115,7 +121,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||||
|
|
||||||
final mediaService = await MediaFileService.fromMedia(media);
|
final mediaService = await MediaFileService.fromMedia(media);
|
||||||
|
|
||||||
await mediaService.setUploadState(UploadState.uploading);
|
await mediaService.setUploadState(UploadState.uploaded);
|
||||||
// In all other cases just try the upload again...
|
// In all other cases just try the upload again...
|
||||||
await startBackgroundMediaUpload(mediaService);
|
await startBackgroundMediaUpload(mediaService);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
|
|
@ -47,6 +49,7 @@ Future<void> insertMediaFileInMessagesTable(
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
mediaId: Value(mediaService.mediaFile.mediaId),
|
mediaId: Value(mediaService.mediaFile.mediaId),
|
||||||
|
type: const Value(MessageType.media),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
|
|
@ -147,12 +150,13 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
final notEncryptedContent = EncryptedContent(
|
final notEncryptedContent = EncryptedContent(
|
||||||
|
groupId: message.groupId,
|
||||||
media: EncryptedContent_Media(
|
media: EncryptedContent_Media(
|
||||||
senderMessageId: message.messageId,
|
senderMessageId: message.messageId,
|
||||||
type: type,
|
type: type,
|
||||||
requiresAuthentication: media.mediaFile.requiresAuthentication,
|
requiresAuthentication: media.mediaFile.requiresAuthentication,
|
||||||
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||||
downloadToken: media.mediaFile.downloadToken,
|
downloadToken: downloadToken.toList(),
|
||||||
encryptionKey: media.mediaFile.encryptionKey,
|
encryptionKey: media.mediaFile.encryptionKey,
|
||||||
encryptionNonce: media.mediaFile.encryptionNonce,
|
encryptionNonce: media.mediaFile.encryptionNonce,
|
||||||
encryptionMac: media.mediaFile.encryptionMac,
|
encryptionMac: media.mediaFile.encryptionMac,
|
||||||
|
|
@ -202,36 +206,50 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
||||||
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
|
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mutex protectUpload = Mutex();
|
||||||
|
|
||||||
Future<void> _uploadUploadRequest(MediaFileService media) async {
|
Future<void> _uploadUploadRequest(MediaFileService media) async {
|
||||||
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
await protectUpload.protect(() async {
|
||||||
.read(key: SecureStorageKeys.apiAuthToken);
|
final currentMedia =
|
||||||
if (apiAuthTokenRaw == null) {
|
await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaFile.mediaId);
|
||||||
Log.error('api auth token not defined.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
|
||||||
|
|
||||||
final apiUrl =
|
if (currentMedia == null ||
|
||||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
|
currentMedia.uploadState == UploadState.backgroundUploadTaskStarted) {
|
||||||
|
Log.info('Download for ${media.mediaFile.mediaId} already started.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// try {
|
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
||||||
Log.info('Starting upload from ${media.mediaFile.mediaId}');
|
.read(key: SecureStorageKeys.apiAuthToken);
|
||||||
|
|
||||||
final task = UploadTask.fromFile(
|
if (apiAuthTokenRaw == null) {
|
||||||
taskId: 'upload_${media.mediaFile.mediaId}',
|
Log.error('api auth token not defined.');
|
||||||
displayName: media.mediaFile.type.name,
|
return null;
|
||||||
file: media.uploadRequestPath,
|
}
|
||||||
url: apiUrl,
|
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
||||||
priority: 0,
|
|
||||||
retries: 10,
|
|
||||||
headers: {
|
|
||||||
'x-twonly-auth-token': apiAuthToken,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Log.info('Enqueue upload task: ${task.taskId}');
|
final apiUrl =
|
||||||
|
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
|
||||||
|
|
||||||
await FileDownloader().enqueue(task);
|
// try {
|
||||||
|
Log.info('Starting upload from ${media.mediaFile.mediaId}');
|
||||||
|
|
||||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
final task = UploadTask.fromFile(
|
||||||
|
taskId: 'upload_${media.mediaFile.mediaId}',
|
||||||
|
displayName: media.mediaFile.type.name,
|
||||||
|
file: media.uploadRequestPath,
|
||||||
|
url: apiUrl,
|
||||||
|
priority: 0,
|
||||||
|
retries: 10,
|
||||||
|
headers: {
|
||||||
|
'x-twonly-auth-token': apiAuthToken,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Log.info('Enqueue upload task: ${task.taskId}');
|
||||||
|
|
||||||
|
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:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
|
|
@ -35,7 +36,6 @@ Future<void> tryTransmitMessages() async {
|
||||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
String? receiptId,
|
String? receiptId,
|
||||||
Receipt? receipt,
|
Receipt? receipt,
|
||||||
bool reupload = false,
|
|
||||||
bool onlyReturnEncryptedData = false,
|
bool onlyReturnEncryptedData = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -49,15 +49,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
}
|
}
|
||||||
receiptId = receipt.receiptId;
|
receiptId = receipt.receiptId;
|
||||||
|
|
||||||
if (reupload) {
|
|
||||||
await twonlyDB.receiptsDao.updateReceipt(
|
|
||||||
receiptId,
|
|
||||||
const ReceiptsCompanion(
|
|
||||||
ackByServerAt: Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
|
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
|
||||||
Log.error('$receiptId message already uploaded!');
|
Log.error('$receiptId message already uploaded!');
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -71,12 +62,18 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
final encryptedContent =
|
final encryptedContent =
|
||||||
pb.EncryptedContent.fromBuffer(message.encryptedContent);
|
pb.EncryptedContent.fromBuffer(message.encryptedContent);
|
||||||
|
|
||||||
var pushData = await getPushDataFromEncryptedContent(
|
final pushNotification = await getPushNotificationFromEncryptedContent(
|
||||||
receipt.contactId,
|
receipt.contactId,
|
||||||
receipt.messageId,
|
receipt.messageId,
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Uint8List? pushData;
|
||||||
|
if (pushNotification != null) {
|
||||||
|
pushData =
|
||||||
|
await encryptPushNotification(receipt.contactId, pushNotification);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
|
if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
|
||||||
pushData = (PushNotification()..kind = PushKind.testNotification)
|
pushData = (PushNotification()..kind = PushKind.testNotification)
|
||||||
.writeToBuffer();
|
.writeToBuffer();
|
||||||
|
|
@ -167,6 +164,7 @@ Future<void> insertAndSendTextMessage(
|
||||||
MessagesCompanion(
|
MessagesCompanion(
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
content: Value(textMessage),
|
content: Value(textMessage),
|
||||||
|
type: const Value(MessageType.text),
|
||||||
quotesMessageId: Value(quotesMessageId),
|
quotesMessageId: Value(quotesMessageId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -187,26 +185,24 @@ Future<void> insertAndSendTextMessage(
|
||||||
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
encryptedContent.textMessage.quoteMessageId = quotesMessageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendCipherTextToGroup(groupId, encryptedContent);
|
await sendCipherTextToGroup(groupId, encryptedContent, message.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendCipherTextToGroup(
|
Future<void> sendCipherTextToGroup(
|
||||||
String groupId,
|
String groupId,
|
||||||
pb.EncryptedContent encryptedContent,
|
pb.EncryptedContent encryptedContent,
|
||||||
|
String? messageId,
|
||||||
) async {
|
) async {
|
||||||
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
final groupMembers = await twonlyDB.groupsDao.getGroupMembers(groupId);
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
|
||||||
if (group == null) return;
|
|
||||||
|
|
||||||
encryptedContent
|
encryptedContent.groupId = groupId;
|
||||||
..groupId = groupId
|
|
||||||
..isDirectChat = group.isDirectChat;
|
|
||||||
|
|
||||||
for (final groupMember in groupMembers) {
|
for (final groupMember in groupMembers) {
|
||||||
unawaited(
|
unawaited(
|
||||||
sendCipherText(
|
sendCipherText(
|
||||||
groupMember.contactId,
|
groupMember.contactId,
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
|
messageId: messageId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +212,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
||||||
int contactId,
|
int contactId,
|
||||||
pb.EncryptedContent encryptedContent, {
|
pb.EncryptedContent encryptedContent, {
|
||||||
bool onlyReturnEncryptedData = false,
|
bool onlyReturnEncryptedData = false,
|
||||||
|
String? messageId,
|
||||||
}) async {
|
}) async {
|
||||||
final response = pb.Message()
|
final response = pb.Message()
|
||||||
..type = pb.Message_Type.CIPHERTEXT
|
..type = pb.Message_Type.CIPHERTEXT
|
||||||
|
|
@ -225,6 +222,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
||||||
ReceiptsCompanion(
|
ReceiptsCompanion(
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
message: Value(response.writeToBuffer()),
|
message: Value(response.writeToBuffer()),
|
||||||
|
messageId: Value(messageId),
|
||||||
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
|
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -249,15 +247,23 @@ Future<void> notifyContactAboutOpeningMessage(
|
||||||
biggestMessageId = messageOtherId;
|
biggestMessageId = messageOtherId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.info('Opened messages: $messageOtherIds');
|
||||||
|
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
contactId,
|
contactId,
|
||||||
pb.EncryptedContent(
|
pb.EncryptedContent(
|
||||||
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
messageUpdate: pb.EncryptedContent_MessageUpdate(
|
||||||
type: pb.EncryptedContent_MessageUpdate_Type.OPENED,
|
type: pb.EncryptedContent_MessageUpdate_Type.OPENED,
|
||||||
multipleSenderMessageIds: messageOtherIds,
|
multipleTargetMessageIds: messageOtherIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
for (final messageId in messageOtherIds) {
|
||||||
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
|
messageId,
|
||||||
|
MessagesCompanion(openedAt: Value(DateTime.now())),
|
||||||
|
);
|
||||||
|
}
|
||||||
await updateLastMessageId(contactId, biggestMessageId);
|
await updateLastMessageId(contactId, biggestMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart' hide Message;
|
import 'package:twonly/src/database/twonly.db.dart' hide Message;
|
||||||
|
|
@ -50,10 +51,20 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
|
||||||
|
|
||||||
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
|
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
|
||||||
|
|
||||||
|
Mutex protectReceiptCheck = Mutex();
|
||||||
|
|
||||||
Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
|
Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
final message = Message.fromBuffer(body);
|
final message = Message.fromBuffer(body);
|
||||||
final receiptId = message.receiptId;
|
final receiptId = message.receiptId;
|
||||||
|
|
||||||
|
await protectReceiptCheck.protect(() async {
|
||||||
|
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
||||||
|
Log.error('Got duplicated message from the server. Ignoring it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
||||||
|
});
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
case Message_Type.SENDER_DELIVERY_RECEIPT:
|
||||||
Log.info('Got delivery receipt for $receiptId!');
|
Log.info('Got delivery receipt for $receiptId!');
|
||||||
|
|
@ -65,7 +76,15 @@ Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
|
||||||
);
|
);
|
||||||
await tryToSendCompleteMessage(receiptId: receiptId, reupload: true);
|
final newReceiptId = uuid.v4();
|
||||||
|
await twonlyDB.receiptsDao.updateReceipt(
|
||||||
|
receiptId,
|
||||||
|
ReceiptsCompanion(
|
||||||
|
receiptId: Value(newReceiptId),
|
||||||
|
ackByServerAt: const Value(null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tryToSendCompleteMessage(receiptId: newReceiptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Message_Type.CIPHERTEXT:
|
case Message_Type.CIPHERTEXT:
|
||||||
|
|
@ -112,7 +131,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
final (content, decryptionErrorType) = await signalDecryptMessage(
|
final (content, decryptionErrorType) = await signalDecryptMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
encryptedContentRaw,
|
encryptedContentRaw,
|
||||||
messageType as int,
|
messageType.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
|
|
@ -147,7 +166,24 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content.hasMessageUpdate()) {
|
||||||
|
await handleMessageUpdate(
|
||||||
|
fromUserId,
|
||||||
|
content.messageUpdate,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.hasMediaUpdate()) {
|
||||||
|
await handleMediaUpdate(
|
||||||
|
fromUserId,
|
||||||
|
content.mediaUpdate,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!content.hasGroupId()) {
|
if (!content.hasGroupId()) {
|
||||||
|
Log.error('Messages should have a groupId $fromUserId.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,14 +193,6 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasMessageUpdate()) {
|
|
||||||
await handleMessageUpdate(
|
|
||||||
fromUserId,
|
|
||||||
content.messageUpdate,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.hasTextMessage()) {
|
if (content.hasTextMessage()) {
|
||||||
await handleTextMessage(
|
await handleTextMessage(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
|
|
@ -192,14 +220,5 @@ Future<PlaintextContent?> handleEncryptedMessage(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasMediaUpdate()) {
|
|
||||||
await handleMediaUpdate(
|
|
||||||
fromUserId,
|
|
||||||
content.groupId,
|
|
||||||
content.mediaUpdate,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||||
|
|
@ -92,6 +93,7 @@ Future<void> handleMedia(
|
||||||
senderId: Value(fromUserId),
|
senderId: Value(fromUserId),
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
mediaId: Value(mediaFile.mediaId),
|
mediaId: Value(mediaFile.mediaId),
|
||||||
|
type: const Value(MessageType.media),
|
||||||
quotesMessageId: Value(
|
quotesMessageId: Value(
|
||||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||||
),
|
),
|
||||||
|
|
@ -112,16 +114,17 @@ Future<void> handleMedia(
|
||||||
|
|
||||||
Future<void> handleMediaUpdate(
|
Future<void> handleMediaUpdate(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String groupId,
|
|
||||||
EncryptedContent_MediaUpdate mediaUpdate,
|
EncryptedContent_MediaUpdate mediaUpdate,
|
||||||
) async {
|
) async {
|
||||||
final messages = await twonlyDB.messagesDao
|
final message = await twonlyDB.messagesDao
|
||||||
.getMessagesByMediaId(mediaUpdate.targetMediaId);
|
.getMessageById(mediaUpdate.targetMessageId)
|
||||||
if (messages.length != 1) return;
|
.getSingleOrNull();
|
||||||
final message = messages.first;
|
if (message == null) {
|
||||||
if (message.senderId != fromUserId) return;
|
Log.error(
|
||||||
|
'Got media update to message ${mediaUpdate.targetMessageId} but message not found.');
|
||||||
|
}
|
||||||
final mediaFile =
|
final mediaFile =
|
||||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
await twonlyDB.mediaFilesDao.getMediaFileById(message!.mediaId!);
|
||||||
if (mediaFile == null) {
|
if (mediaFile == null) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Got media file update, but media file was not found ${message.mediaId}',
|
'Got media file update, but media file was not found ${message.mediaId}',
|
||||||
|
|
@ -140,15 +143,8 @@ Future<void> handleMediaUpdate(
|
||||||
);
|
);
|
||||||
case EncryptedContent_MediaUpdate_Type.STORED:
|
case EncryptedContent_MediaUpdate_Type.STORED:
|
||||||
Log.info('Got media file stored ${mediaFile.mediaId}');
|
Log.info('Got media file stored ${mediaFile.mediaId}');
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
stored: Value(true),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final mediaService = await MediaFileService.fromMedia(mediaFile);
|
final mediaService = await MediaFileService.fromMedia(mediaFile);
|
||||||
unawaited(mediaService.createThumbnail());
|
await mediaService.storeMediaFile();
|
||||||
|
|
||||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,20 @@ Future<void> handleMessageUpdate(
|
||||||
) async {
|
) async {
|
||||||
switch (messageUpdate.type) {
|
switch (messageUpdate.type) {
|
||||||
case EncryptedContent_MessageUpdate_Type.OPENED:
|
case EncryptedContent_MessageUpdate_Type.OPENED:
|
||||||
Log.info(
|
for (final targetMessageId in messageUpdate.multipleTargetMessageIds) {
|
||||||
'Opened message ${messageUpdate.multipleSenderMessageIds.length}',
|
Log.info(
|
||||||
);
|
'Opened message $targetMessageId',
|
||||||
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
|
);
|
||||||
await twonlyDB.messagesDao.handleMessageOpened(
|
await twonlyDB.messagesDao.handleMessageOpened(
|
||||||
contactId,
|
contactId,
|
||||||
senderMessageId,
|
targetMessageId,
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case EncryptedContent_MessageUpdate_Type.DELETE:
|
case EncryptedContent_MessageUpdate_Type.DELETE:
|
||||||
|
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.info('Delete message ${messageUpdate.senderMessageId}');
|
Log.info('Delete message ${messageUpdate.senderMessageId}');
|
||||||
await twonlyDB.messagesDao.handleMessageDeletion(
|
await twonlyDB.messagesDao.handleMessageDeletion(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -27,6 +30,9 @@ Future<void> handleMessageUpdate(
|
||||||
fromTimestamp(messageUpdate.timestamp),
|
fromTimestamp(messageUpdate.timestamp),
|
||||||
);
|
);
|
||||||
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
|
||||||
|
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.info('Edit message ${messageUpdate.senderMessageId}');
|
Log.info('Edit message ${messageUpdate.senderMessageId}');
|
||||||
await twonlyDB.messagesDao.handleTextEdit(
|
await twonlyDB.messagesDao.handleTextEdit(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -36,3 +42,14 @@ Future<void> handleMessageUpdate(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isSender(int fromUserId, String messageId) async {
|
||||||
|
final message =
|
||||||
|
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||||
|
if (message == null) return false;
|
||||||
|
if (message.senderId == fromUserId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Log.error('Contact $fromUserId tried to modify the message $messageId');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||||
import 'package:twonly/src/services/api/utils.dart';
|
import 'package:twonly/src/services/api/utils.dart';
|
||||||
|
|
@ -20,6 +21,7 @@ Future<void> handleTextMessage(
|
||||||
senderId: Value(fromUserId),
|
senderId: Value(fromUserId),
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
content: Value(textMessage.text),
|
content: Value(textMessage.text),
|
||||||
|
type: const Value(MessageType.text),
|
||||||
quotesMessageId: Value(
|
quotesMessageId: Value(
|
||||||
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
|
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class Result<T, E> {
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime fromTimestamp(Int64 timeStamp) {
|
DateTime fromTimestamp(Int64 timeStamp) {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt() * 1000);
|
return DateTime.fromMillisecondsSinceEpoch(timeStamp.toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: strict_raw_type
|
// ignore: strict_raw_type
|
||||||
|
|
@ -88,7 +88,7 @@ Future<void> handleMediaError(MediaFile media) async {
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||||
targetMediaId: message.mediaId,
|
targetMessageId: message.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
@ -129,6 +130,9 @@ class MediaFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get imagePreviewAvailable =>
|
||||||
|
thumbnailPath.existsSync() || storedPath.existsSync();
|
||||||
|
|
||||||
Future<void> storeMediaFile() async {
|
Future<void> storeMediaFile() async {
|
||||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
|
@ -137,7 +141,25 @@ class MediaFileService {
|
||||||
stored: Value(true),
|
stored: Value(true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tempPath.copy(storedPath.path);
|
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();
|
await updateFromDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,12 +195,12 @@ Future<void> updateLastMessageId(int fromUserId, String messageId) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List?> getPushDataFromEncryptedContent(
|
Future<PushNotification?> getPushNotificationFromEncryptedContent(
|
||||||
int toUserId,
|
int toUserId,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
EncryptedContent content,
|
EncryptedContent content,
|
||||||
) async {
|
) async {
|
||||||
late PushKind kind;
|
PushKind? kind;
|
||||||
String? reactionContent;
|
String? reactionContent;
|
||||||
|
|
||||||
if (content.hasReaction()) {
|
if (content.hasReaction()) {
|
||||||
|
|
@ -270,6 +270,8 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind == null) return null;
|
||||||
|
|
||||||
final pushNotification = PushNotification()..kind = kind;
|
final pushNotification = PushNotification()..kind = kind;
|
||||||
if (reactionContent != null) {
|
if (reactionContent != null) {
|
||||||
pushNotification.reactionContent = reactionContent;
|
pushNotification.reactionContent = reactionContent;
|
||||||
|
|
@ -277,7 +279,7 @@ Future<Uint8List?> getPushDataFromEncryptedContent(
|
||||||
if (messageId != null) {
|
if (messageId != null) {
|
||||||
pushNotification.messageId = messageId;
|
pushNotification.messageId = messageId;
|
||||||
}
|
}
|
||||||
return encryptPushNotification(toUserId, pushNotification);
|
return pushNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// this will trigger a push notification
|
/// this will trigger a push notification
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,13 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
|
||||||
.isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) {
|
.isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.info('Requesting new PREKEYS for $contactId');
|
Log.info('[PREKEY] Requesting new PREKEYS for $contactId');
|
||||||
lastPreKeyRequest = DateTime.now();
|
lastPreKeyRequest = DateTime.now();
|
||||||
await requestNewKeys.protect(() async {
|
await requestNewKeys.protect(() async {
|
||||||
final otherKeys = await apiService.getPreKeysByUserId(contactId);
|
final otherKeys = await apiService.getPreKeysByUserId(contactId);
|
||||||
if (otherKeys != null) {
|
if (otherKeys != null) {
|
||||||
Log.info(
|
Log.info(
|
||||||
'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!',
|
'[PREKEY] Got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!',
|
||||||
);
|
);
|
||||||
final preKeys = otherKeys.preKeys
|
final preKeys = otherKeys.preKeys
|
||||||
.map(
|
.map(
|
||||||
|
|
@ -50,7 +50,8 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
|
||||||
.toList();
|
.toList();
|
||||||
await twonlyDB.signalDao.insertPreKeys(preKeys);
|
await twonlyDB.signalDao.insertPreKeys(preKeys);
|
||||||
} else {
|
} else {
|
||||||
Log.error('could not load new pre keys for user $contactId');
|
// 104400
|
||||||
|
Log.error('[PREKEY] Could not load new pre keys for user $contactId');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:pie_menu/pie_menu.dart';
|
import 'package:pie_menu/pie_menu.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -246,11 +247,14 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isUUIDNewer(String uuid1, String uuid2) {
|
bool isUUIDNewer(String uuid1, String uuid2) {
|
||||||
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
|
try {
|
||||||
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
|
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
|
||||||
print(timestamp1);
|
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
|
||||||
print(timestamp2);
|
return timestamp1 > timestamp2;
|
||||||
return timestamp1 > timestamp2;
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String uint8ListToHex(List<int> bytes) {
|
String uint8ListToHex(List<int> bytes) {
|
||||||
|
|
@ -313,3 +317,34 @@ Color getMessageColorFromType(
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getUUIDforDirectChat(int a, int b) {
|
||||||
|
if (a < 0 || b < 0) {
|
||||||
|
throw ArgumentError('Inputs must be non-negative integers.');
|
||||||
|
}
|
||||||
|
if (a > integerMax || b > integerMax) {
|
||||||
|
throw ArgumentError('Inputs must be <= 0x7fffffff.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask to 64 bits in case inputs exceed 64 bits
|
||||||
|
final mask64 = (BigInt.one << 64) - BigInt.one;
|
||||||
|
final ai = BigInt.from(a) & mask64;
|
||||||
|
final bi = BigInt.from(b) & mask64;
|
||||||
|
|
||||||
|
// Ensure the bigger integer is in front (high 64 bits)
|
||||||
|
final hi = ai >= bi ? ai : bi;
|
||||||
|
final lo = ai >= bi ? bi : ai;
|
||||||
|
|
||||||
|
final combined = (hi << 64) | lo;
|
||||||
|
|
||||||
|
final hex = combined.toRadixString(16).padLeft(32, '0');
|
||||||
|
|
||||||
|
final parts = [
|
||||||
|
hex.substring(0, 8),
|
||||||
|
hex.substring(8, 12),
|
||||||
|
hex.substring(12, 16),
|
||||||
|
hex.substring(16, 20),
|
||||||
|
hex.substring(20, 32),
|
||||||
|
];
|
||||||
|
return parts.join('-');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class SaveToGalleryButton extends StatefulWidget {
|
class SaveToGalleryButton extends StatefulWidget {
|
||||||
const SaveToGalleryButton({
|
const SaveToGalleryButton({
|
||||||
required this.getMergedImage,
|
required this.storeImageAsOriginal,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.displayButtonLabel,
|
required this.displayButtonLabel,
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Future<Uint8List?> Function() getMergedImage;
|
final Future<bool> Function() storeImageAsOriginal;
|
||||||
final bool displayButtonLabel;
|
final bool displayButtonLabel;
|
||||||
final MediaFileService mediaService;
|
final MediaFileService mediaService;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
@ -45,6 +45,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
_imageSaving = true;
|
_imageSaving = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (widget.mediaService.mediaFile.type == MediaType.image) {
|
||||||
|
await widget.storeImageAsOriginal();
|
||||||
|
}
|
||||||
|
|
||||||
String? res;
|
String? res;
|
||||||
|
|
||||||
final storedMediaPath = widget.mediaService.storedPath;
|
final storedMediaPath = widget.mediaService.storedPath;
|
||||||
|
|
|
||||||
|
|
@ -171,20 +171,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
NotificationBadge(
|
NotificationBadge(
|
||||||
count: (media.type != MediaType.video)
|
count: (media.type == MediaType.video)
|
||||||
? '0'
|
? '0'
|
||||||
: media.displayLimitInMilliseconds == null
|
: media.displayLimitInMilliseconds == null
|
||||||
? '∞'
|
? '∞'
|
||||||
: media.displayLimitInMilliseconds.toString(),
|
: media.displayLimitInMilliseconds.toString(),
|
||||||
child: ActionButton(
|
child: ActionButton(
|
||||||
(media.type != MediaType.video)
|
(media.type == MediaType.video)
|
||||||
? media.displayLimitInMilliseconds == null
|
? media.displayLimitInMilliseconds == null
|
||||||
? Icons.repeat_rounded
|
? Icons.repeat_rounded
|
||||||
: Icons.repeat_one_rounded
|
: Icons.repeat_one_rounded
|
||||||
: Icons.timer_outlined,
|
: Icons.timer_outlined,
|
||||||
tooltipText: context.lang.protectAsARealTwonly,
|
tooltipText: context.lang.protectAsARealTwonly,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (media.type != MediaType.video) {
|
if (media.type == MediaType.video) {
|
||||||
await mediaService.setDisplayLimit(
|
await mediaService.setDisplayLimit(
|
||||||
(media.displayLimitInMilliseconds == null) ? 0 : null,
|
(media.displayLimitInMilliseconds == null) ? 0 : null,
|
||||||
);
|
);
|
||||||
|
|
@ -311,7 +311,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layers.length > 1 || media.type != MediaType.video) {
|
if (layers.length > 1 || media.type == MediaType.video) {
|
||||||
for (final x in layers) {
|
for (final x in layers) {
|
||||||
x.showCustomButtons = false;
|
x.showCustomButtons = false;
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +434,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SaveToGalleryButton(
|
SaveToGalleryButton(
|
||||||
getMergedImage: getEditedImageBytes,
|
storeImageAsOriginal: storeImageAsOriginal,
|
||||||
mediaService: mediaService,
|
mediaService: mediaService,
|
||||||
displayButtonLabel: widget.sendToGroup == null,
|
displayButtonLabel: widget.sendToGroup == null,
|
||||||
isLoading: loadingImage,
|
isLoading: loadingImage,
|
||||||
|
|
|
||||||
|
|
@ -209,31 +209,32 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
child: isConnected ? Container() : const ConnectionInfo(),
|
child: isConnected ? Container() : const ConnectionInfo(),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
|
child: RefreshIndicator(
|
||||||
? Center(
|
onRefresh: () async {
|
||||||
child: Padding(
|
await apiService.close(() {});
|
||||||
padding: const EdgeInsets.all(10),
|
await apiService.connect(force: true);
|
||||||
child: OutlinedButton.icon(
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
icon: const Icon(Icons.person_add),
|
},
|
||||||
onPressed: () {
|
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
|
||||||
Navigator.push(
|
? Center(
|
||||||
context,
|
child: Padding(
|
||||||
MaterialPageRoute(
|
padding: const EdgeInsets.all(10),
|
||||||
builder: (context) => const AddNewUserView(),
|
child: OutlinedButton.icon(
|
||||||
),
|
icon: const Icon(Icons.person_add),
|
||||||
);
|
onPressed: () {
|
||||||
},
|
Navigator.push(
|
||||||
label: Text(context.lang.chatListViewSearchUserNameBtn),
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const AddNewUserView(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label:
|
||||||
|
Text(context.lang.chatListViewSearchUserNameBtn),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: ListView.builder(
|
||||||
: RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await apiService.close(() {});
|
|
||||||
await apiService.connect(force: true);
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
},
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: _groupsPinned.length +
|
itemCount: _groupsPinned.length +
|
||||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||||
_groupsNotPinned.length +
|
_groupsNotPinned.length +
|
||||||
|
|
@ -276,7 +277,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
|
|
@ -36,10 +37,12 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
List<Message> messagesNotOpened = [];
|
List<Message> messagesNotOpened = [];
|
||||||
late StreamSubscription<List<Message>> messagesNotOpenedStream;
|
late StreamSubscription<List<Message>> messagesNotOpenedStream;
|
||||||
|
|
||||||
List<Message> lastMessages = [];
|
Message? lastMessage;
|
||||||
late StreamSubscription<List<Message>> lastMessageStream;
|
late StreamSubscription<Message?> lastMessageStream;
|
||||||
|
late StreamSubscription<List<MediaFile>> lastMediaFilesStream;
|
||||||
|
|
||||||
List<Message> previewMessages = [];
|
List<Message> previewMessages = [];
|
||||||
|
List<MediaFile> previewMediaFiles = [];
|
||||||
bool hasNonOpenedMediaFile = false;
|
bool hasNonOpenedMediaFile = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -52,6 +55,7 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
messagesNotOpenedStream.cancel();
|
messagesNotOpenedStream.cancel();
|
||||||
lastMessageStream.cancel();
|
lastMessageStream.cancel();
|
||||||
|
lastMediaFilesStream.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,30 +63,44 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
lastMessageStream = twonlyDB.messagesDao
|
lastMessageStream = twonlyDB.messagesDao
|
||||||
.watchLastMessage(widget.group.groupId)
|
.watchLastMessage(widget.group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
updateState(update, messagesNotOpened);
|
protectUpdateState.protect(() async {
|
||||||
|
await updateState(update, messagesNotOpened);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
messagesNotOpenedStream = twonlyDB.messagesDao
|
messagesNotOpenedStream = twonlyDB.messagesDao
|
||||||
.watchMessageNotOpened(widget.group.groupId)
|
.watchMessageNotOpened(widget.group.groupId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
updateState(lastMessages, update);
|
protectUpdateState.protect(() async {
|
||||||
|
await updateState(lastMessage, update);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lastMediaFilesStream =
|
||||||
|
twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
||||||
|
for (final mediaFile in mediaFiles) {
|
||||||
|
final index =
|
||||||
|
previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId);
|
||||||
|
if (index >= 0) {
|
||||||
|
previewMediaFiles[index] = mediaFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateState(
|
Mutex protectUpdateState = Mutex();
|
||||||
List<Message> newLastMessages,
|
|
||||||
|
Future<void> updateState(
|
||||||
|
Message? newLastMessage,
|
||||||
List<Message> newMessagesNotOpened,
|
List<Message> newMessagesNotOpened,
|
||||||
) {
|
) async {
|
||||||
if (newLastMessages.isEmpty) {
|
if (newLastMessage == null) {
|
||||||
// there are no messages at all
|
// there are no messages at all
|
||||||
currentMessage = null;
|
currentMessage = null;
|
||||||
previewMessages = [];
|
previewMessages = [];
|
||||||
} else if (newMessagesNotOpened.isEmpty) {
|
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||||
// there are no not opened messages show just the last message in the table
|
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||||
currentMessage = newLastMessages.last;
|
|
||||||
previewMessages = newLastMessages;
|
|
||||||
} else {
|
|
||||||
// filter first for received messages
|
|
||||||
final receivedMessages =
|
final receivedMessages =
|
||||||
newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||||
|
|
||||||
|
|
@ -93,6 +111,10 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
previewMessages = newMessagesNotOpened;
|
previewMessages = newMessagesNotOpened;
|
||||||
currentMessage = newMessagesNotOpened.first;
|
currentMessage = newMessagesNotOpened.first;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// there are no not opened messages show just the last message in the table
|
||||||
|
currentMessage = newLastMessage;
|
||||||
|
previewMessages = [newLastMessage];
|
||||||
}
|
}
|
||||||
|
|
||||||
final msgs =
|
final msgs =
|
||||||
|
|
@ -106,7 +128,18 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
hasNonOpenedMediaFile = false;
|
hasNonOpenedMediaFile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessages = newLastMessages;
|
for (final message in previewMessages) {
|
||||||
|
if (message.mediaId != null &&
|
||||||
|
!previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||||
|
final mediaFile =
|
||||||
|
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||||
|
if (mediaFile != null) {
|
||||||
|
previewMediaFiles.add(mediaFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMessage = newLastMessage;
|
||||||
messagesNotOpened = newMessagesNotOpened;
|
messagesNotOpened = newMessagesNotOpened;
|
||||||
setState(() {
|
setState(() {
|
||||||
// sets lastMessages, messagesNotOpened and currentMessage
|
// sets lastMessages, messagesNotOpened and currentMessage
|
||||||
|
|
@ -136,7 +169,7 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
await startDownloadMedia(mediaFile, true);
|
await startDownloadMedia(mediaFile, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mediaFile.downloadState! == DownloadState.downloaded) {
|
if (mediaFile.downloadState! == DownloadState.ready) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -184,7 +217,7 @@ class _UserListItem extends State<GroupListItem> {
|
||||||
? Text(context.lang.chatsTapToSend)
|
? Text(context.lang.chatsTapToSend)
|
||||||
: Row(
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
MessageSendStateIcon(previewMessages),
|
MessageSendStateIcon(previewMessages, previewMediaFiles),
|
||||||
const Text('•'),
|
const Text('•'),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
if (currentMessage != null)
|
if (currentMessage != null)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:pie_menu/pie_menu.dart';
|
import 'package:pie_menu/pie_menu.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -94,6 +95,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mutex protectMessageUpdating = Mutex();
|
||||||
|
|
||||||
Future<void> initStreams() async {
|
Future<void> initStreams() async {
|
||||||
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
|
final groupStream = twonlyDB.groupsDao.watchGroup(group.groupId);
|
||||||
userSub = groupStream.listen((newGroup) {
|
userSub = groupStream.listen((newGroup) {
|
||||||
|
|
@ -105,55 +108,64 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
|
|
||||||
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
|
||||||
messageSub = msgStream.listen((newMessages) async {
|
messageSub = msgStream.listen((newMessages) async {
|
||||||
await flutterLocalNotificationsPlugin.cancelAll();
|
/// 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...
|
||||||
final chatItems = <ChatItem>[];
|
if (protectMessageUpdating.isLocked) {
|
||||||
final storedMediaFiles = <Message>[];
|
return;
|
||||||
|
|
||||||
DateTime? lastDate;
|
|
||||||
|
|
||||||
final openedMessages = <int, List<String>>{};
|
|
||||||
|
|
||||||
for (final msg in newMessages) {
|
|
||||||
if (msg.type == MessageType.text &&
|
|
||||||
msg.senderId != null &&
|
|
||||||
msg.openedAt == null) {
|
|
||||||
openedMessages[msg.senderId!]!.add(msg.messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.type == MessageType.media && msg.mediaStored) {
|
|
||||||
storedMediaFiles.add(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastDate == null ||
|
|
||||||
msg.createdAt.day != lastDate.day ||
|
|
||||||
msg.createdAt.month != lastDate.month ||
|
|
||||||
msg.createdAt.year != lastDate.year) {
|
|
||||||
chatItems.add(ChatItem.date(msg.createdAt));
|
|
||||||
lastDate = msg.createdAt;
|
|
||||||
} else if (msg.createdAt.difference(lastDate).inMinutes >= 20) {
|
|
||||||
chatItems.add(ChatItem.time(msg.createdAt));
|
|
||||||
lastDate = msg.createdAt;
|
|
||||||
}
|
|
||||||
chatItems.add(ChatItem.message(msg));
|
|
||||||
}
|
}
|
||||||
|
await protectMessageUpdating.protect(() async {
|
||||||
|
await flutterLocalNotificationsPlugin.cancelAll();
|
||||||
|
|
||||||
for (final contactId in openedMessages.keys) {
|
final chatItems = <ChatItem>[];
|
||||||
await notifyContactAboutOpeningMessage(
|
final storedMediaFiles = <Message>[];
|
||||||
contactId,
|
|
||||||
openedMessages[contactId]!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await twonlyDB.messagesDao.openedAllTextMessages(widget.group.groupId);
|
DateTime? lastDate;
|
||||||
|
|
||||||
setState(() {
|
final openedMessages = <int, List<String>>{};
|
||||||
messages = chatItems.reversed.toList();
|
|
||||||
|
for (final msg in newMessages) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type == MessageType.media && msg.mediaStored) {
|
||||||
|
storedMediaFiles.add(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastDate == null ||
|
||||||
|
msg.createdAt.day != lastDate.day ||
|
||||||
|
msg.createdAt.month != lastDate.month ||
|
||||||
|
msg.createdAt.year != lastDate.year) {
|
||||||
|
chatItems.add(ChatItem.date(msg.createdAt));
|
||||||
|
lastDate = msg.createdAt;
|
||||||
|
} else if (msg.createdAt.difference(lastDate).inMinutes >= 20) {
|
||||||
|
chatItems.add(ChatItem.time(msg.createdAt));
|
||||||
|
lastDate = msg.createdAt;
|
||||||
|
}
|
||||||
|
chatItems.add(ChatItem.message(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final contactId in openedMessages.keys) {
|
||||||
|
await notifyContactAboutOpeningMessage(
|
||||||
|
contactId,
|
||||||
|
openedMessages[contactId]!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
messages = chatItems.reversed.toList();
|
||||||
|
});
|
||||||
|
|
||||||
|
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||||
|
galleryItems = items.values.toList();
|
||||||
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
|
||||||
galleryItems = items.values.toList();
|
|
||||||
setState(() {});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,18 +408,8 @@ bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
|
||||||
if (index <= 0) {
|
if (index <= 0) {
|
||||||
return true; // If there is no previous message, return true
|
return true; // If there is no previous message, return true
|
||||||
}
|
}
|
||||||
|
return (messages[index - 1].message?.senderId ==
|
||||||
final lastMessage = messages[index - 1];
|
messages[index].message?.senderId);
|
||||||
final currentMessage = messages[index];
|
|
||||||
|
|
||||||
if (lastMessage.isMessage && currentMessage.isMessage) {
|
|
||||||
// Check if both messages have the same quotesMessageId (or both are null)
|
|
||||||
return (lastMessage.message!.quotesMessageId == null &&
|
|
||||||
currentMessage.message!.quotesMessageId == null) ||
|
|
||||||
(lastMessage.message!.quotesMessageId != null &&
|
|
||||||
currentMessage.message!.quotesMessageId != null);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double calculateNumberOfLines(String text, double width, double fontSize) {
|
double calculateNumberOfLines(String text, double width, double fontSize) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart'
|
import 'package:twonly/src/database/tables/messages.table.dart'
|
||||||
hide MessageActions;
|
hide MessageActions;
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -35,14 +38,31 @@ class ChatListEntry extends StatefulWidget {
|
||||||
class _ChatListEntryState extends State<ChatListEntry> {
|
class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
MediaFileService? mediaService;
|
MediaFileService? mediaService;
|
||||||
|
|
||||||
|
StreamSubscription<MediaFile?>? mediaFileSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
initAsync();
|
initAsync();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
mediaFileSub?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
mediaService = await MediaFileService.fromMediaId(widget.message.messageId);
|
if (widget.message.mediaId != null) {
|
||||||
|
final mediaFileStream =
|
||||||
|
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!);
|
||||||
|
mediaFileSub = mediaFileStream.listen((mediaFiles) async {
|
||||||
|
if (mediaFiles != null) {
|
||||||
|
mediaService = await MediaFileService.fromMedia(mediaFiles);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
|
@ -47,17 +48,20 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||||
type: EncryptedContent_MediaUpdate_Type.REOPENED,
|
type: EncryptedContent_MediaUpdate_Type.REOPENED,
|
||||||
targetMediaId: widget.message.mediaId,
|
targetMessageId: widget.message.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
await twonlyDB.messagesDao.updateMessageId(
|
||||||
|
widget.message.messageId,
|
||||||
|
const MessagesCompanion(openedAt: Value(null)),
|
||||||
);
|
);
|
||||||
await twonlyDB.messagesDao.reopenedMedia(widget.message.messageId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onTap() async {
|
Future<void> onTap() async {
|
||||||
if (widget.mediaService.mediaFile.downloadState ==
|
if (widget.mediaService.mediaFile.downloadState == DownloadState.ready &&
|
||||||
DownloadState.downloaded &&
|
|
||||||
widget.message.openedAt == null) {
|
widget.message.openedAt == null) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
|
|
@ -91,7 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
onTap: (widget.message.type == MessageType.media) ? onTap : null,
|
onTap: (widget.message.type == MessageType.media) ? onTap : null,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 150,
|
width: 150,
|
||||||
height: widget.message.mediaStored ? 271 : null,
|
height: (widget.message.mediaStored &&
|
||||||
|
widget.mediaService.imagePreviewAvailable)
|
||||||
|
? 271
|
||||||
|
: null,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
bool loadIndex() {
|
bool loadIndex() {
|
||||||
if (widget.message.mediaStored) {
|
if (widget.message.mediaStored) {
|
||||||
final index = widget.galleryItems.indexWhere(
|
final index = widget.galleryItems.indexWhere(
|
||||||
(x) => x.mediaService.mediaFile.mediaId == (widget.message.messageId),
|
(x) => x.mediaService.mediaFile.mediaId == (widget.message.mediaId),
|
||||||
);
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
galleryItemIndex = index;
|
galleryItemIndex = index;
|
||||||
|
|
@ -112,7 +112,8 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!widget.message.mediaStored) {
|
if (!widget.message.mediaStored ||
|
||||||
|
!widget.mediaService.imagePreviewAvailable) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minHeight: 39,
|
minHeight: 39,
|
||||||
|
|
@ -130,6 +131,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
),
|
),
|
||||||
child: MessageSendStateIcon(
|
child: MessageSendStateIcon(
|
||||||
[widget.message],
|
[widget.message],
|
||||||
|
[widget.mediaService.mediaFile],
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
canBeReopened: widget.canBeReopened,
|
canBeReopened: widget.canBeReopened,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class MessageContextMenu extends StatelessWidget {
|
||||||
remove: false,
|
remove: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:twonly/globals.dart';
|
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
|
|
||||||
|
|
@ -18,49 +17,37 @@ enum MessageSendState {
|
||||||
sending,
|
sending,
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MessageSendState> messageSendStateFromMessage(Message msg) async {
|
MessageSendState messageSendStateFromMessage(Message msg) {
|
||||||
MessageSendState state;
|
if (msg.senderId == null) {
|
||||||
|
/// messages was send by me, look up if every messages was received by the server...
|
||||||
final ackByServer = await twonlyDB.messagesDao.haveAllMembers(
|
if (msg.ackByServer == null) {
|
||||||
msg.groupId,
|
return MessageSendState.sending;
|
||||||
msg.messageId,
|
|
||||||
MessageActionType.ackByServerAt,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ackByServer) {
|
|
||||||
if (msg.senderId == null) {
|
|
||||||
state = MessageSendState.sending;
|
|
||||||
} else {
|
|
||||||
state = MessageSendState.receiving;
|
|
||||||
}
|
}
|
||||||
} else {
|
if (msg.openedAt != null) {
|
||||||
if (msg.senderId == null) {
|
return MessageSendState.sendOpened;
|
||||||
// message send
|
|
||||||
if (msg.openedAt == null) {
|
|
||||||
state = MessageSendState.send;
|
|
||||||
} else {
|
|
||||||
state = MessageSendState.sendOpened;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// message received
|
return MessageSendState.send;
|
||||||
if (msg.openedAt == null) {
|
|
||||||
state = MessageSendState.received;
|
|
||||||
} else {
|
|
||||||
state = MessageSendState.receivedOpened;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
|
||||||
|
// message received
|
||||||
|
if (msg.openedAt == null) {
|
||||||
|
return MessageSendState.received;
|
||||||
|
} else {
|
||||||
|
return MessageSendState.receivedOpened;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageSendStateIcon extends StatefulWidget {
|
class MessageSendStateIcon extends StatefulWidget {
|
||||||
const MessageSendStateIcon(
|
const MessageSendStateIcon(
|
||||||
this.messages, {
|
this.messages,
|
||||||
|
this.mediaFiles, {
|
||||||
super.key,
|
super.key,
|
||||||
this.canBeReopened = false,
|
this.canBeReopened = false,
|
||||||
this.mainAxisAlignment = MainAxisAlignment.end,
|
this.mainAxisAlignment = MainAxisAlignment.end,
|
||||||
});
|
});
|
||||||
final List<Message> messages;
|
final List<Message> messages;
|
||||||
|
final List<MediaFile> mediaFiles;
|
||||||
final MainAxisAlignment mainAxisAlignment;
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
final bool canBeReopened;
|
final bool canBeReopened;
|
||||||
|
|
||||||
|
|
@ -69,17 +56,30 @@ class MessageSendStateIcon extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
List<Widget> icons = <Widget>[];
|
|
||||||
String text = '';
|
|
||||||
Widget? textWidget;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Widget getLoaderIcon(Color color) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 1, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final icons = <Widget>[];
|
||||||
|
var text = '';
|
||||||
|
Widget? textWidget;
|
||||||
|
textWidget = null;
|
||||||
final kindsAlreadyShown = HashSet<MessageType>();
|
final kindsAlreadyShown = HashSet<MessageType>();
|
||||||
|
|
||||||
for (final message in widget.messages) {
|
for (final message in widget.messages) {
|
||||||
|
|
@ -87,16 +87,14 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
if (kindsAlreadyShown.contains(message.type)) continue;
|
if (kindsAlreadyShown.contains(message.type)) continue;
|
||||||
kindsAlreadyShown.add(message.type);
|
kindsAlreadyShown.add(message.type);
|
||||||
|
|
||||||
final state = await messageSendStateFromMessage(message);
|
final state = messageSendStateFromMessage(message);
|
||||||
|
|
||||||
final mediaFile = message.mediaId == null
|
final mediaFile = message.mediaId == null
|
||||||
? null
|
? null
|
||||||
: await MediaFileService.fromMediaId(message.mediaId!);
|
: widget.mediaFiles
|
||||||
|
.firstWhereOrNull((t) => t.mediaId == message.mediaId);
|
||||||
|
|
||||||
if (!mounted) return;
|
final color = getMessageColorFromType(message, mediaFile, context);
|
||||||
|
|
||||||
final color =
|
|
||||||
getMessageColorFromType(message, mediaFile?.mediaFile, context);
|
|
||||||
|
|
||||||
Widget icon = const Placeholder();
|
Widget icon = const Placeholder();
|
||||||
textWidget = null;
|
textWidget = null;
|
||||||
|
|
@ -126,11 +124,10 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||||
text = context.lang.messageSendState_Received;
|
text = context.lang.messageSendState_Received;
|
||||||
if (message.type == MessageType.media) {
|
if (message.type == MessageType.media) {
|
||||||
if (mediaFile!.mediaFile.downloadState == DownloadState.pending) {
|
if (mediaFile!.downloadState == DownloadState.pending) {
|
||||||
text = context.lang.messageSendState_TapToLoad;
|
text = context.lang.messageSendState_TapToLoad;
|
||||||
}
|
}
|
||||||
if (mediaFile.mediaFile.downloadState ==
|
if (mediaFile.downloadState == DownloadState.downloading) {
|
||||||
DownloadState.downloading) {
|
|
||||||
text = context.lang.messageSendState_Loading;
|
text = context.lang.messageSendState_Loading;
|
||||||
icon = getLoaderIcon(color);
|
icon = getLoaderIcon(color);
|
||||||
}
|
}
|
||||||
|
|
@ -153,12 +150,12 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaFile != null) {
|
if (mediaFile != null) {
|
||||||
if (mediaFile.mediaFile.stored) {
|
if (mediaFile.reopenByContact) {
|
||||||
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
|
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
|
||||||
text = context.lang.messageReopened;
|
text = context.lang.messageReopened;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaFile.mediaFile.reuploadRequestedBy != null) {
|
if (mediaFile.downloadState == DownloadState.reuploadRequested) {
|
||||||
icon =
|
icon =
|
||||||
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
FaIcon(FontAwesomeIcons.clockRotateLeft, size: 12, color: color);
|
||||||
textWidget = Text(
|
textWidget = Text(
|
||||||
|
|
@ -175,24 +172,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget getLoaderIcon(Color color) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 1, color: color),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (icons.isEmpty) return Container();
|
if (icons.isEmpty) return Container();
|
||||||
|
|
||||||
var icon = icons[0];
|
var icon = icons[0];
|
||||||
|
|
@ -201,18 +180,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
icon = Stack(
|
icon = Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// First icon (bottom icon)
|
Transform.scale(
|
||||||
icons[0],
|
scale: 1.3,
|
||||||
|
|
||||||
Transform(
|
|
||||||
transform: Matrix4.identity()
|
|
||||||
..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half
|
|
||||||
..translateByDouble(3, 5, 0, 1),
|
|
||||||
// Move down by 10 pixels (adjust as needed)
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: icons[1],
|
child: icons[1],
|
||||||
),
|
),
|
||||||
// Second icon (top icon, slightly offset)
|
icons[0],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +195,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
icon,
|
icon,
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
if (textWidget != null)
|
if (textWidget != null)
|
||||||
textWidget!
|
textWidget
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:drift/drift.dart' hide Column;
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
|
|
@ -163,17 +162,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
final stream =
|
final stream =
|
||||||
twonlyDB.mediaFilesDao.watchMedia(currentMedia!.mediaFile.mediaId);
|
twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!);
|
||||||
|
|
||||||
var downloadTriggered = false;
|
var downloadTriggered = false;
|
||||||
|
|
||||||
await downloadStateListener?.cancel();
|
await downloadStateListener?.cancel();
|
||||||
downloadStateListener = stream.listen((updated) async {
|
downloadStateListener = stream.listen((updated) async {
|
||||||
if (updated == null) return;
|
if (updated == null) return;
|
||||||
if (updated.downloadState != DownloadState.downloaded) {
|
if (updated.downloadState != DownloadState.ready) {
|
||||||
if (!downloadTriggered) {
|
if (!downloadTriggered) {
|
||||||
downloadTriggered = true;
|
downloadTriggered = true;
|
||||||
await startDownloadMedia(currentMedia!.mediaFile, true);
|
final mediaFile = await twonlyDB.mediaFilesDao
|
||||||
|
.getMediaFileById(allMediaFiles.first.mediaId!);
|
||||||
|
await startDownloadMedia(mediaFile!, true);
|
||||||
unawaited(tryDownloadAllMediaFiles(force: true));
|
unawaited(tryDownloadAllMediaFiles(force: true));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -211,11 +212,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
[currentMessage!.messageId],
|
[currentMessage!.messageId],
|
||||||
);
|
);
|
||||||
|
|
||||||
await twonlyDB.messagesDao.updateMessageId(
|
|
||||||
currentMessage!.messageId,
|
|
||||||
MessagesCompanion(openedAt: Value(DateTime.now())),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentMediaLocal.tempPath.existsSync()) {
|
if (!currentMediaLocal.tempPath.existsSync()) {
|
||||||
Log.error('Temp media file not found...');
|
Log.error('Temp media file not found...');
|
||||||
await handleMediaError(currentMediaLocal.mediaFile);
|
await handleMediaError(currentMediaLocal.mediaFile);
|
||||||
|
|
@ -289,9 +285,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
pb.EncryptedContent(
|
pb.EncryptedContent(
|
||||||
mediaUpdate: pb.EncryptedContent_MediaUpdate(
|
mediaUpdate: pb.EncryptedContent_MediaUpdate(
|
||||||
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
|
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
|
||||||
targetMediaId: currentMedia!.mediaFile.mediaId,
|
targetMessageId: currentMessage!.messageId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
imageSaved = true;
|
imageSaved = true;
|
||||||
|
|
@ -537,8 +534,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (currentMedia?.mediaFile.downloadState !=
|
if (currentMedia?.mediaFile.downloadState != DownloadState.ready)
|
||||||
DownloadState.downloaded)
|
|
||||||
const Positioned.fill(
|
const Positioned.fill(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
||||||
emoji: widget.emoji,
|
emoji: widget.emoji,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,9 @@ class UserList extends StatelessWidget {
|
||||||
var directChat =
|
var directChat =
|
||||||
await twonlyDB.groupsDao.getDirectChat(user.userId);
|
await twonlyDB.groupsDao.getDirectChat(user.userId);
|
||||||
if (directChat == null) {
|
if (directChat == null) {
|
||||||
await twonlyDB.groupsDao.insertGroup(
|
await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
|
user.userId,
|
||||||
GroupsCompanion(
|
GroupsCompanion(
|
||||||
isDirectChat: const Value(true),
|
|
||||||
groupName: Value(
|
groupName: Value(
|
||||||
getContactDisplayName(user),
|
getContactDisplayName(user),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,14 @@ class MemoriesViewState extends State<MemoriesView> {
|
||||||
final applicationSupportDirectory =
|
final applicationSupportDirectory =
|
||||||
await getApplicationSupportDirectory();
|
await getApplicationSupportDirectory();
|
||||||
for (final mediaFile in mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
|
final mediaService = MediaFileService(
|
||||||
|
mediaFile,
|
||||||
|
applicationSupportDirectory: applicationSupportDirectory,
|
||||||
|
);
|
||||||
|
if (!mediaService.imagePreviewAvailable) continue;
|
||||||
galleryItems.add(
|
galleryItems.add(
|
||||||
MemoryItem(
|
MemoryItem(
|
||||||
mediaService: MediaFileService(
|
mediaService: mediaService,
|
||||||
mediaFile,
|
|
||||||
applicationSupportDirectory: applicationSupportDirectory,
|
|
||||||
),
|
|
||||||
messages: [],
|
messages: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,19 @@ class _MemoriesItemThumbnailState extends State<MemoriesItemThumbnail> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final media = widget.galleryItem.mediaService;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: widget.galleryItem.mediaService.mediaFile.mediaId,
|
tag: media.mediaFile.mediaId,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Image.file(widget.galleryItem.mediaService.thumbnailPath),
|
if (media.thumbnailPath.existsSync())
|
||||||
|
Image.file(media.thumbnailPath)
|
||||||
|
else if (media.storedPath.existsSync())
|
||||||
|
Image.file(media.storedPath)
|
||||||
|
else
|
||||||
|
const Text('Media file removed.'),
|
||||||
if (widget.galleryItem.mediaService.mediaFile.type ==
|
if (widget.galleryItem.mediaService.mediaFile.type ==
|
||||||
MediaType.video)
|
MediaType.video)
|
||||||
const Positioned.fill(
|
const Positioned.fill(
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
username: username,
|
username: username,
|
||||||
displayName: username,
|
displayName: username,
|
||||||
subscriptionPlan: 'Preview',
|
subscriptionPlan: 'Preview',
|
||||||
);
|
)..appVersion = 62;
|
||||||
|
|
||||||
await const FlutterSecureStorage()
|
await const FlutterSecureStorage()
|
||||||
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
|
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/services/api/messages.dart';
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class AutomatedTestingView extends StatefulWidget {
|
class AutomatedTestingView extends StatefulWidget {
|
||||||
|
|
@ -38,11 +39,13 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final username = await showUserNameDialog(context);
|
final username = await showUserNameDialog(context);
|
||||||
if (username == null) return;
|
if (username == null) return;
|
||||||
|
Log.info('Requested to send to $username');
|
||||||
|
|
||||||
final contacts =
|
final contacts = await twonlyDB.contactsDao
|
||||||
await twonlyDB.contactsDao.getContactsByUsername(username);
|
.getContactsByUsername(username.toLowerCase());
|
||||||
|
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
|
Log.info('Sending to ${contact.username}');
|
||||||
final group =
|
final group =
|
||||||
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
||||||
for (var i = 0; i < 200; i++) {
|
for (var i = 0; i < 200; i++) {
|
||||||
|
|
@ -67,10 +70,10 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
|
|
||||||
Future<String?> showUserNameDialog(
|
Future<String?> showUserNameDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) async {
|
||||||
final controller = TextEditingController();
|
final controller = TextEditingController();
|
||||||
|
|
||||||
return showDialog<String>(
|
await showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
|
@ -97,4 +100,5 @@ Future<String?> showUserNameDialog(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return controller.text;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
|
as pb;
|
||||||
|
import 'package:twonly/src/services/api/messages.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||||
|
|
||||||
class RetransmissionDataView extends StatefulWidget {
|
class RetransmissionDataView extends StatefulWidget {
|
||||||
const RetransmissionDataView({super.key});
|
const RetransmissionDataView({super.key});
|
||||||
|
|
@ -101,11 +108,48 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
||||||
Text(
|
Text(
|
||||||
'Server-Ack: ${retrans.receipt.ackByServerAt}',
|
'Server-Ack: ${retrans.receipt.ackByServerAt}',
|
||||||
),
|
),
|
||||||
|
if (retrans.receipt.messageId != null)
|
||||||
|
Text(
|
||||||
|
'MessageId: ${retrans.receipt.messageId}',
|
||||||
|
),
|
||||||
|
if (retrans.receipt.messageId != null)
|
||||||
|
FutureBuilder(
|
||||||
|
future: getPushNotificationFromEncryptedContent(
|
||||||
|
retrans.receipt.contactId,
|
||||||
|
retrans.receipt.messageId,
|
||||||
|
pb.EncryptedContent.fromBuffer(
|
||||||
|
pb.Message.fromBuffer(retrans.receipt.message)
|
||||||
|
.encryptedContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (d, a) {
|
||||||
|
if (!a.hasData) return Container();
|
||||||
|
return Text(
|
||||||
|
'PushKind: ${a.data?.kind}',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
trailing: FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final newReceiptId = uuid.v4();
|
||||||
|
await twonlyDB.receiptsDao.updateReceipt(
|
||||||
|
retrans.receipt.receiptId,
|
||||||
|
ReceiptsCompanion(
|
||||||
|
receiptId: Value(newReceiptId),
|
||||||
|
ackByServerAt: const Value(null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tryToSendCompleteMessage(
|
||||||
|
receiptId: newReceiptId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:restart_app/restart_app.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/database/twonly_database_old.dart'
|
||||||
|
show TwonlyDatabaseOld;
|
||||||
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
class DatabaseMigrationView extends StatefulWidget {
|
class DatabaseMigrationView extends StatefulWidget {
|
||||||
const DatabaseMigrationView({super.key});
|
const DatabaseMigrationView({super.key});
|
||||||
|
|
@ -8,8 +24,426 @@ class DatabaseMigrationView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
|
class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
|
||||||
|
bool _isMigrating = false;
|
||||||
|
bool _isMigratingFinished = false;
|
||||||
|
int _contactsMigrated = 0;
|
||||||
|
int _storedMediaFiles = 0;
|
||||||
|
|
||||||
|
Future<void> startMigration() async {
|
||||||
|
setState(() {
|
||||||
|
_isMigrating = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final oldDatabase = TwonlyDatabaseOld();
|
||||||
|
final oldContacts = await oldDatabase.contacts.select().get();
|
||||||
|
final oldMessages = await oldDatabase.messages.select().get();
|
||||||
|
|
||||||
|
for (final oldContact in oldContacts) {
|
||||||
|
await twonlyDB.contactsDao.insertContact(
|
||||||
|
ContactsCompanion(
|
||||||
|
userId: Value(oldContact.userId),
|
||||||
|
username: Value(oldContact.username),
|
||||||
|
displayName: Value(oldContact.displayName),
|
||||||
|
nickName: Value(oldContact.nickName),
|
||||||
|
avatarSvg: Value(oldContact.avatarSvg),
|
||||||
|
senderProfileCounter: const Value(0),
|
||||||
|
accepted: Value(oldContact.accepted),
|
||||||
|
requested: Value(oldContact.requested),
|
||||||
|
blocked: Value(oldContact.blocked),
|
||||||
|
verified: Value(oldContact.verified),
|
||||||
|
deleted: Value(oldContact.deleted),
|
||||||
|
createdAt: Value(oldContact.createdAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_contactsMigrated += 1;
|
||||||
|
});
|
||||||
|
if (!oldContact.deleted) {
|
||||||
|
final group = await twonlyDB.groupsDao.createNewDirectChat(
|
||||||
|
oldContact.userId,
|
||||||
|
GroupsCompanion(
|
||||||
|
pinned: Value(oldContact.pinned),
|
||||||
|
archived: Value(oldContact.archived),
|
||||||
|
groupName: Value(getContactDisplayNameOld(oldContact)),
|
||||||
|
totalMediaCounter: Value(oldContact.totalMediaCounter),
|
||||||
|
alsoBestFriend: Value(oldContact.alsoBestFriend),
|
||||||
|
createdAt: Value(oldContact.createdAt),
|
||||||
|
lastFlameCounterChange: Value(oldContact.lastFlameCounterChange),
|
||||||
|
lastFlameSync: Value(oldContact.lastFlameSync),
|
||||||
|
lastMessageExchange: Value(oldContact.lastMessageExchange),
|
||||||
|
lastMessageReceived: Value(oldContact.lastMessageReceived),
|
||||||
|
lastMessageSend: Value(oldContact.lastMessageSend),
|
||||||
|
flameCounter: Value(oldContact.flameCounter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (group == null) continue;
|
||||||
|
for (final oldMessage in oldMessages) {
|
||||||
|
if (oldMessage.mediaUploadId == null &&
|
||||||
|
oldMessage.mediaDownloadId == null) {
|
||||||
|
/// only interested in media files...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldMessage.contactId != oldContact.userId) continue;
|
||||||
|
if (!oldMessage.mediaStored) continue;
|
||||||
|
|
||||||
|
var storedMediaPath =
|
||||||
|
join((await getApplicationSupportDirectory()).path, 'media');
|
||||||
|
if (oldMessage.mediaDownloadId != null) {
|
||||||
|
storedMediaPath =
|
||||||
|
'${join(storedMediaPath, 'received')}/${oldMessage.mediaDownloadId}';
|
||||||
|
} else {
|
||||||
|
storedMediaPath =
|
||||||
|
'${join(storedMediaPath, 'send')}/${oldMessage.mediaDownloadId}';
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = MediaType.image;
|
||||||
|
if (File('$storedMediaPath.mp4').existsSync()) {
|
||||||
|
type = MediaType.video;
|
||||||
|
storedMediaPath = '$storedMediaPath.mp4';
|
||||||
|
} else if (File('$storedMediaPath.png').existsSync()) {
|
||||||
|
type = MediaType.image;
|
||||||
|
storedMediaPath = '$storedMediaPath.png';
|
||||||
|
} else if (File('$storedMediaPath.webp').existsSync()) {
|
||||||
|
type = MediaType.image;
|
||||||
|
storedMediaPath = '$storedMediaPath.webp';
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final uniqueId = Value(
|
||||||
|
getUUIDforDirectChat(
|
||||||
|
oldMessage.messageOtherId ?? oldMessage.messageId,
|
||||||
|
oldMessage.contactId ^ gUser.userId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
|
MediaFilesCompanion(
|
||||||
|
mediaId: uniqueId,
|
||||||
|
stored: const Value(true),
|
||||||
|
type: Value(type),
|
||||||
|
createdAt: Value(oldMessage.sendAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (mediaFile == null) continue;
|
||||||
|
|
||||||
|
final message = await twonlyDB.messagesDao.insertMessage(
|
||||||
|
MessagesCompanion(
|
||||||
|
messageId: uniqueId,
|
||||||
|
groupId: Value(group.groupId),
|
||||||
|
mediaId: uniqueId,
|
||||||
|
type: const Value(MessageType.media),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (message == null) continue;
|
||||||
|
|
||||||
|
final mediaService = await MediaFileService.fromMedia(mediaFile);
|
||||||
|
File(storedMediaPath).copySync(mediaService.storedPath.path);
|
||||||
|
setState(() {
|
||||||
|
_storedMediaFiles += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final memoriesPath = Directory(
|
||||||
|
join((await getApplicationSupportDirectory()).path, 'media', 'memories'),
|
||||||
|
);
|
||||||
|
final files = memoriesPath.listSync();
|
||||||
|
for (final file in files) {
|
||||||
|
if (file.path.contains('thumbnail')) continue;
|
||||||
|
final type =
|
||||||
|
file.path.contains('mp4') ? MediaType.video : MediaType.image;
|
||||||
|
final stat = FileStat.statSync(file.path);
|
||||||
|
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
|
MediaFilesCompanion(
|
||||||
|
type: Value(type),
|
||||||
|
createdAt: Value(stat.modified),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final mediaService = await MediaFileService.fromMedia(mediaFile!);
|
||||||
|
File(file.path).copySync(mediaService.storedPath.path);
|
||||||
|
setState(() {
|
||||||
|
_storedMediaFiles += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldContactPreKeys =
|
||||||
|
await oldDatabase.signalContactPreKeys.select().get();
|
||||||
|
for (final oldContactPreKey in oldContactPreKeys) {
|
||||||
|
try {
|
||||||
|
await twonlyDB
|
||||||
|
.into(twonlyDB.signalContactPreKeys)
|
||||||
|
.insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson()));
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldSignalSessionStores =
|
||||||
|
await oldDatabase.signalSessionStores.select().get();
|
||||||
|
for (final oldSignalSessionStore in oldSignalSessionStores) {
|
||||||
|
try {
|
||||||
|
await twonlyDB.into(twonlyDB.signalSessionStores).insert(
|
||||||
|
SignalSessionStore.fromJson(oldSignalSessionStore.toJson()));
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldSignalSenderKeyStores =
|
||||||
|
await oldDatabase.signalSenderKeyStores.select().get();
|
||||||
|
for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) {
|
||||||
|
try {
|
||||||
|
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
|
||||||
|
SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldSignalPreyKeyStores =
|
||||||
|
await oldDatabase.signalPreKeyStores.select().get();
|
||||||
|
for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) {
|
||||||
|
try {
|
||||||
|
await twonlyDB
|
||||||
|
.into(twonlyDB.signalPreKeyStores)
|
||||||
|
.insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson()));
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldSignalIdentityKeyStores =
|
||||||
|
await oldDatabase.signalIdentityKeyStores.select().get();
|
||||||
|
for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) {
|
||||||
|
try {
|
||||||
|
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
|
||||||
|
SignalIdentityKeyStore.fromJson(
|
||||||
|
oldSignalIdentityKeyStore.toJson()),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldSignalContactSignedPreKeys =
|
||||||
|
await oldDatabase.signalContactSignedPreKeys.select().get();
|
||||||
|
for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) {
|
||||||
|
try {
|
||||||
|
await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert(
|
||||||
|
SignalContactSignedPreKey.fromJson(
|
||||||
|
oldSignalContactSignedPreKey.toJson(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUserdata((u) {
|
||||||
|
u.appVersion = 62;
|
||||||
|
return u;
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isMigratingFinished = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Placeholder();
|
return Scaffold(
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: _isMigratingFinished
|
||||||
|
? ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'Deine Daten wurden migriert.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
...[
|
||||||
|
'${_contactsMigrated} Kontakte',
|
||||||
|
'${_storedMediaFiles} gespeicherte Mediendateien',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Restart.restartApp(
|
||||||
|
notificationTitle: 'Deine Daten wurden migriert.',
|
||||||
|
notificationBody: 'Click here to open the app again',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'App neu starten',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: _isMigrating
|
||||||
|
? ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'Deine Daten werden migriert.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'twonly während der Migration NICHT schließen!',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 20, color: Colors.red),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'Aktueller Status',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
'${_contactsMigrated} Kontakte',
|
||||||
|
'${_storedMediaFiles} gespeicherte Mediendateien',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: ListView(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'twonly. Jetzt besser als je zuvor.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
const Text(
|
||||||
|
'Das sind die neuen Features.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
...[
|
||||||
|
'Gruppen',
|
||||||
|
'Nachrichten bearbeiten & löschen',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Technische Neuerungen',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 17),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.',
|
||||||
|
'Verwendung von UUIDs in der Datenbank',
|
||||||
|
'Von Grund auf neues Datenbank-Schema',
|
||||||
|
'Verbesserung der Zuverlässigkeit von C2C Nachrichten',
|
||||||
|
'Verbesserung von Videos',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
const Text(
|
||||||
|
'Was bedeutet das für dich?',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'Was nach der Migration erhalten bleibt.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 15),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
'Gespeicherte Bilder',
|
||||||
|
'Kontakte',
|
||||||
|
'Flammen',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'Was durch die Migration verloren geht.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 15, color: Colors.red),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
'Text-Nachrichten und Reaktionen',
|
||||||
|
'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.',
|
||||||
|
].map(
|
||||||
|
(e) => Text(
|
||||||
|
e,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: startMigration,
|
||||||
|
child: const Text(
|
||||||
|
'Jetzt starten',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/pow.dart';
|
import 'package:twonly/src/utils/pow.dart';
|
||||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||||
|
|
@ -21,5 +22,43 @@ void main() {
|
||||||
final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]);
|
final list1 = Uint8List.fromList([41, 41, 41, 41, 41, 41, 41]);
|
||||||
expect(list1, hexToUint8List(uint8ListToHex(list1)));
|
expect(list1, hexToUint8List(uint8ListToHex(list1)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Zero inputs produce all-zero UUID', () {
|
||||||
|
expect(
|
||||||
|
getUUIDforDirectChat(0, 0),
|
||||||
|
'00000000-0000-0000-0000-000000000000',
|
||||||
|
);
|
||||||
|
expect(getUUIDforDirectChat(0, 0).length, uuid.v1().length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Max int values (0x7fffffff)', () {
|
||||||
|
const max32 = 0x7fffffff; // 2147483647
|
||||||
|
expect(
|
||||||
|
getUUIDforDirectChat(max32, max32),
|
||||||
|
'00000000-7fff-ffff-0000-00007fffffff',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Bigger goes front', () {
|
||||||
|
expect(
|
||||||
|
getUUIDforDirectChat(1, 0),
|
||||||
|
'00000000-0000-0001-0000-000000000000',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getUUIDforDirectChat(0, 1),
|
||||||
|
'00000000-0000-0001-0000-000000000000',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Arbitrary within 32-bit range', () {
|
||||||
|
expect(
|
||||||
|
getUUIDforDirectChat(0x12345678, 0x0abcdef0),
|
||||||
|
'00000000-1234-5678-0000-00000abcdef0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reject values > 0x7fffffff', () {
|
||||||
|
expect(() => getUUIDforDirectChat(0x80000000, 0), throwsArgumentError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue