diff --git a/.blocked/archives/contacts_model.dart b/.blocked/archives/contacts_model.dart deleted file mode 100644 index 80ce2ec..0000000 --- a/.blocked/archives/contacts_model.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'package:cv/cv.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:logging/logging.dart'; -import 'package:sqflite_sqlcipher/sqflite.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/app.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class Contact { - Contact( - {required this.userId, - required this.displayName, - required this.accepted, - required this.blocked, - required this.verified, - required this.totalMediaCounter, - required this.requested}); - final Int64 userId; - final String displayName; - final bool accepted; - final bool requested; - final bool blocked; - final bool verified; - final int totalMediaCounter; -} - -class DbContacts extends CvModelBase { - static const tableName = "contacts"; - - static const columnUserId = "contact_user_id"; - final userId = CvField(columnUserId); - - static const columnDisplayName = "display_name"; - final displayName = CvField(columnDisplayName); - - static const columnAccepted = "accepted"; - final accepted = CvField(columnAccepted); - - static const columnRequested = "requested"; - final requested = CvField(columnRequested); - - static const columnBlocked = "blocked"; - final blocked = CvField(columnBlocked); - - static const columnVerified = "verified"; - final verified = CvField(columnVerified); - - static const columnTotalMediaCounter = "total_media_counter"; - final totalMediaCounter = CvField(columnTotalMediaCounter); - - static const columnCreatedAt = "created_at"; - final createdAt = CvField(columnCreatedAt); - - static Future setupDatabaseTable(Database db) async { - String createTableString = """ - CREATE TABLE IF NOT EXISTS $tableName ( - $columnUserId INTEGER NOT NULL PRIMARY KEY, - $columnDisplayName TEXT, - $columnAccepted INT NOT NULL DEFAULT 0, - $columnRequested INT NOT NULL DEFAULT 0, - $columnBlocked INT NOT NULL DEFAULT 0, - $columnVerified INTEGER NOT NULL DEFAULT 0, - $columnTotalMediaCounter INT NOT NULL DEFAULT 0, - $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP - ) - """; - await db.execute(createTableString); - - if (!await columnExists(db, tableName, columnVerified)) { - String alterTableString = """ - ALTER TABLE $tableName - ADD COLUMN $columnVerified INTEGER NOT NULL DEFAULT 0 - """; - await db.execute(alterTableString); - } - } - - @override - List get fields => - [userId, displayName, accepted, requested, blocked, createdAt]; - - static Future> getActiveUsers() async { - return (await _getAllUsers()) - .where((u) => u.accepted && !u.blocked) - .toList(); - } - - static Future> getBlockedUsers() async { - return (await _getAllUsers()).where((u) => u.blocked).toList(); - } - - static Future> getUsers() async { - return (await _getAllUsers()).where((u) => !u.blocked).toList(); - } - - static Future> getAllUsers() async { - return await _getAllUsers(); - } - - static Future updateTotalMediaCounter( - int userId, - ) async { - List> result = await dbProvider.db!.query( - tableName, - columns: [columnTotalMediaCounter], - where: '$columnUserId = ?', - whereArgs: [userId], - ); - - if (result.isNotEmpty) { - int totalMediaCounter = result.first.cast()[columnTotalMediaCounter]; - _updateTotalMediaCounter(userId, totalMediaCounter + 1); - globalCallBackOnContactChange(); - } - } - - static List _parseContacts(List users) { - List parsedUsers = []; - for (int i = 0; i < users.length; i++) { - try { - int userId = users.cast()[i][columnUserId]; - parsedUsers.add( - Contact( - userId: Int64(userId), - totalMediaCounter: users.cast()[i][columnTotalMediaCounter], - displayName: users.cast()[i][columnDisplayName], - accepted: users[i][columnAccepted] == 1, - blocked: users[i][columnBlocked] == 1, - verified: users[i][columnVerified] == 1, - requested: users[i][columnRequested] == 1, - ), - ); - } catch (e) { - Logger("contacts_model/parse_single_user").shout("$e"); - return []; - } - } - return parsedUsers; - } - - static Future getUserById(int userId) async { - try { - var user = await dbProvider.db!.query(tableName, - columns: [ - columnUserId, - columnDisplayName, - columnAccepted, - columnRequested, - columnBlocked, - columnVerified, - columnTotalMediaCounter, - columnCreatedAt - ], - where: "$columnUserId = ?", - whereArgs: [userId]); - if (user.isEmpty) return null; - return _parseContacts(user)[0]; - } catch (e) { - Logger("contacts_model/getUserById").shout("$e"); - return null; - } - } - - static Future> _getAllUsers() async { - try { - var users = await dbProvider.db!.query(tableName, columns: [ - columnUserId, - columnDisplayName, - columnAccepted, - columnRequested, - columnBlocked, - columnVerified, - columnTotalMediaCounter, - columnCreatedAt - ]); - if (users.isEmpty) return []; - return _parseContacts(users); - } catch (e) { - Logger("contacts_model/getUsers").shout("$e"); - return []; - } - } - - static Future _update(int userId, Map updates, - {bool notifyFlutter = true}) async { - await dbProvider.db!.update( - tableName, - updates, - where: "$columnUserId = ?", - whereArgs: [userId], - ); - if (notifyFlutter) { - globalCallBackOnContactChange(); - } - } - - static Future changeDisplayName(int userId, String newDisplayName) async { - if (newDisplayName == "") return; - Map updates = { - columnDisplayName: newDisplayName, - }; - await _update(userId, updates); - } - - static Future _updateTotalMediaCounter( - int userId, int totalMediaCounter) async { - Map updates = {columnTotalMediaCounter: totalMediaCounter}; - await _update(userId, updates, notifyFlutter: false); - } - - static Future blockUser(int userId, {bool unblock = false}) async { - Map updates = { - columnBlocked: unblock ? 0 : 1, - }; - await _update(userId, updates); - } - - static Future acceptUser(int userId) async { - Map updates = { - columnAccepted: 1, - columnRequested: 0, - }; - await _update(userId, updates); - } - - static Future updateVerificationStatus(int userId, bool status) async { - Map updates = { - columnVerified: status ? 1 : 0, - }; - await _update(userId, updates); - } - - static Future deleteUser(int userId) async { - await dbProvider.db!.delete( - tableName, - where: "$columnUserId = ?", - whereArgs: [userId], - ); - globalCallBackOnContactChange(); - } - - static Future insertNewContact( - String username, int userId, bool requested) async { - try { - int a = requested ? 1 : 0; - await dbProvider.db!.insert(DbContacts.tableName, { - DbContacts.columnDisplayName: username, - DbContacts.columnUserId: userId, - DbContacts.columnRequested: a - }); - globalCallBackOnContactChange(); - return true; - } catch (e) { - Logger("contacts_model/getUsers").shout("$e"); - return false; - } - } -} diff --git a/.blocked/archives/messages_model.dart b/.blocked/archives/messages_model.dart deleted file mode 100644 index 648db17..0000000 --- a/.blocked/archives/messages_model.dart +++ /dev/null @@ -1,444 +0,0 @@ -import 'dart:convert'; - -import 'package:cv/cv.dart'; -import 'package:logging/logging.dart'; -import 'package:sqflite_sqlcipher/sqflite.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/app.dart'; -import 'package:twonly/src/components/message_send_state_icon.dart'; -import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/providers/api/api.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class DbMessage { - DbMessage({ - required this.messageId, - required this.messageOtherId, - required this.otherUserId, - required this.messageContent, - required this.messageOpenedAt, - required this.messageAcknowledgeByUser, - required this.isDownloaded, - required this.messageAcknowledgeByServer, - required this.sendAt, - }); - - int messageId; - // is this null then the message was sent from the user itself - int? messageOtherId; - int otherUserId; - MessageContent messageContent; - DateTime? messageOpenedAt; - bool messageAcknowledgeByUser; - bool isDownloaded; - bool messageAcknowledgeByServer; - DateTime sendAt; - - bool containsOtherMedia() { - if (messageOtherId == null) return false; - return isMedia(); - } - - bool get messageReceived => messageOtherId != null; - - bool isRealTwonly() { - final content = messageContent; - if (content is MediaMessageContent) { - if (content.isRealTwonly) { - return true; - } - } - return false; - } - - bool isMedia() { - return messageContent is MediaMessageContent; - } - - MessageSendState getSendState() { - MessageSendState state; - if (!messageAcknowledgeByServer) { - state = MessageSendState.sending; - } else { - if (messageOtherId == null) { - // message send - if (messageOpenedAt == null) { - state = MessageSendState.send; - } else { - state = MessageSendState.sendOpened; - } - } else { - // message received - if (messageOpenedAt == null) { - state = MessageSendState.received; - } else { - state = MessageSendState.receivedOpened; - } - } - } - return state; - } -} - -class DbMessages extends CvModelBase { - static const tableName = "messages"; - - static const columnMessageId = "id"; - final messageId = CvField(columnMessageId); - - static const columnMessageOtherId = "message_other_id"; - final messageOtherId = CvField(columnMessageOtherId); - - static const columnOtherUserId = "other_user_id"; - final otherUserId = CvField(columnOtherUserId); - - static const columnMessageKind = "message_kind"; - final messageKind = CvField(columnMessageKind); - - static const columnMessageContentJson = "message_json"; - final messageContentJson = CvField(columnMessageContentJson); - - static const columnMessageOpenedAt = "message_opened_at"; - final messageOpenedAt = CvField(columnMessageOpenedAt); - - static const columnMessageAcknowledgeByUser = "message_acknowledged_by_user"; - final messageAcknowledgeByUser = CvField(columnMessageAcknowledgeByUser); - - static const columnMessageAcknowledgeByServer = - "message_acknowledged_by_server"; - final messageAcknowledgeByServer = - CvField(columnMessageAcknowledgeByServer); - - static const columnSendAt = "message_send_or_received_at"; - final sendAt = CvField(columnSendAt); - - static const columnUpdatedAt = "updated_at"; - final updatedAt = CvField(columnUpdatedAt); - - static Future setupDatabaseTable(Database db) async { - String createTableString = """ - CREATE TABLE IF NOT EXISTS $tableName ( - $columnMessageId INTEGER NOT NULL PRIMARY KEY, - $columnMessageOtherId INTEGER DEFAULT NULL, - $columnOtherUserId INTEGER NOT NULL, - $columnMessageKind INTEGER NOT NULL, - $columnMessageAcknowledgeByUser INTEGER NOT NULL DEFAULT 0, - $columnMessageAcknowledgeByServer INTEGER NOT NULL DEFAULT 0, - $columnMessageContentJson TEXT NOT NULL, - $columnMessageOpenedAt DATETIME DEFAULT NULL, - $columnSendAt DATETIME DEFAULT CURRENT_TIMESTAMP, - $columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP - ) - """; - await db.execute(createTableString); - } - - static Future> getMessageDates(int otherUserId) async { - final List> maps = await dbProvider.db!.rawQuery(''' - SELECT $columnSendAt, $columnMessageOtherId - FROM $tableName - WHERE $columnOtherUserId = ? AND ($columnMessageKind = ? OR $columnMessageKind = ?) - ORDER BY $columnSendAt DESC; - ''', [otherUserId, MessageKind.image.index, MessageKind.video.index]); - - try { - return List.generate(maps.length, (i) { - return ( - DateTime.tryParse(maps[i][columnSendAt])!, - maps[i][columnMessageOtherId] - ); - }); - } catch (e) { - Logger("error parsing datetime: $e"); - return []; - } - } - - static Future deleteMessageById(int messageId) async { - await dbProvider.db!.delete( - tableName, - where: '$columnMessageId = ?', - whereArgs: [messageId], - ); - int? fromUserId = await getFromUserIdByMessageId(messageId); - if (fromUserId != null) { - globalCallBackOnMessageChange(fromUserId, messageId); - } - return fromUserId; - } - - static Future getFromUserIdByMessageId(int messageId) async { - List> result = await dbProvider.db!.query( - tableName, - columns: [columnOtherUserId], - where: '$columnMessageId = ?', - whereArgs: [messageId], - ); - if (result.isNotEmpty) { - return result.first[columnOtherUserId] as int?; - } - return null; - } - - static Future insertMyMessage(int userIdFrom, MessageKind kind, - MessageContent content, DateTime messageSendAt) async { - try { - int messageId = await dbProvider.db!.insert(tableName, { - columnMessageKind: kind.index, - columnMessageContentJson: jsonEncode(content.toJson()), - columnOtherUserId: userIdFrom, - columnSendAt: messageSendAt.toIso8601String() - }); - globalCallBackOnMessageChange(userIdFrom, messageId); - return messageId; - } catch (e) { - Logger("messsage_model/insertMyMessage").shout("$e"); - return null; - } - } - - static Future insertOtherMessage(int userIdFrom, MessageKind kind, - int messageOtherId, String jsonContent, DateTime messageSendAt) async { - try { - int messageId = await dbProvider.db!.insert(tableName, { - columnMessageOtherId: messageOtherId, - columnMessageKind: kind.index, - columnMessageContentJson: jsonContent, - columnMessageAcknowledgeByServer: 1, - columnMessageAcknowledgeByUser: - 0, // ack in case of sending corresponds to the opened flag - columnOtherUserId: userIdFrom, - columnSendAt: messageSendAt.toIso8601String() - }); - globalCallBackOnMessageChange(userIdFrom, messageId); - return messageId; - } catch (e) { - Logger("messsage_model/insertOtherMessage").shout("$e"); - return null; - } - } - - static Future> getAllMessagesForUserWithHigherMessageId( - int otherUserId, int lastMessageId) async { - var rows = await dbProvider.db!.query( - tableName, - where: "$columnOtherUserId = ? AND $columnMessageId > ?", - whereArgs: [otherUserId, lastMessageId], - orderBy: "$columnUpdatedAt DESC", - ); - - List messages = await convertToDbMessage(rows); - - return messages; - } - - static Future> getAllMessagesForUser(int otherUserId) async { - var rows = await dbProvider.db!.query( - tableName, - where: "$columnOtherUserId = ?", - whereArgs: [otherUserId], - orderBy: "$columnSendAt DESC", - ); - - List messages = await convertToDbMessage(rows); - - return messages; - } - - static Future getMessageById(int messageId) async { - var rows = await dbProvider.db!.query(tableName, - where: "$columnMessageId = ?", whereArgs: [messageId]); - List messages = await convertToDbMessage(rows); - return messages.firstOrNull; - } - - static Future> getAllMessagesForRetransmitting() async { - var rows = await dbProvider.db!.query( - tableName, - where: "$columnMessageAcknowledgeByServer = 0", - ); - List messages = await convertToDbMessage(rows); - return messages; - } - - static Future getLastMessagesForPreviewForUser( - int otherUserId) async { - var rows = await dbProvider.db!.query( - tableName, - where: "$columnOtherUserId = ?", - whereArgs: [otherUserId], - orderBy: "$columnUpdatedAt DESC", - limit: 10, - ); - - List messages = await convertToDbMessage(rows); - - // check if you received a message which the user has not already opened - List receivedByOther = messages - .where((c) => c.messageOtherId != null && c.messageOpenedAt == null) - .toList(); - if (receivedByOther.isNotEmpty) { - return receivedByOther[receivedByOther.length - 1]; - } - - // check if there is a message which was not ack by the server - List notAckByServer = - messages.where((c) => !c.messageAcknowledgeByServer).toList(); - if (notAckByServer.isNotEmpty) return notAckByServer[0]; - - // check if there is a message which was not ack by the user - List notAckByUser = - messages.where((c) => !c.messageAcknowledgeByUser).toList(); - if (notAckByUser.isNotEmpty) return notAckByUser[0]; - - if (messages.isEmpty) return null; - return messages[0]; - } - - static Future _updateByMessageId(int messageId, Map data, - {bool notifyFlutterState = true}) async { - await dbProvider.db!.update( - tableName, - data, - where: "$columnMessageId = ?", - whereArgs: [messageId], - ); - if (notifyFlutterState) { - int? fromUserId = await getFromUserIdByMessageId(messageId); - if (fromUserId != null) { - globalCallBackOnMessageChange(fromUserId, messageId); - } - } - } - - static Future _updateByOtherMessageId( - int fromUserId, int messageId, Map data) async { - await dbProvider.db!.update( - tableName, - data, - where: "$columnMessageOtherId = ?", - whereArgs: [messageId], - ); - globalCallBackOnMessageChange(fromUserId, messageId); - } - - // this ensures that the message id can be spoofed by another person - static Future _updateByMessageIdOther( - int fromUserId, int messageId, Map data) async { - await dbProvider.db!.update( - tableName, - data, - where: "$columnMessageId = ? AND $columnOtherUserId = ?", - whereArgs: [messageId, fromUserId], - ); - globalCallBackOnMessageChange(fromUserId, messageId); - } - - static Future userOpenedOtherMessage( - int fromUserId, int otherMessageId) async { - Map data = { - columnMessageOpenedAt: DateTime.now().toIso8601String(), - }; - await _updateByOtherMessageId(fromUserId, otherMessageId, data); - } - - static Future otherUserOpenedMyMessage( - int fromUserId, int messageId, DateTime openedAt) async { - Map data = { - columnMessageOpenedAt: openedAt.toIso8601String(), - }; - await _updateByMessageIdOther(fromUserId, messageId, data); - } - - static Future acknowledgeMessageByServer(int messageId) async { - Map data = { - columnMessageAcknowledgeByServer: 1, - }; - await _updateByMessageId(messageId, data); - } - - // check fromUserId to prevent spoofing - static Future acknowledgeMessageByUser(int fromUserId, int messageId) async { - Map valuesToUpdate = { - columnMessageAcknowledgeByUser: 1, - }; - await dbProvider.db!.update( - tableName, - valuesToUpdate, - where: "$messageId = ? AND $columnOtherUserId = ?", - whereArgs: [messageId, fromUserId], - ); - globalCallBackOnMessageChange(fromUserId, messageId); - } - - @override - List get fields => - [messageId, messageKind, messageContentJson, messageOpenedAt, sendAt]; - - // TODO: The message meta is needed to maintain the flame. Delete if not. - // This function should calculate if this message is needed for the flame calculation and delete the message complete and not only - // the message content. - static Future deleteTextContent( - int messageId, TextMessageContent oldMessage) async { - oldMessage.text = ""; - Map data = { - columnMessageContentJson: jsonEncode(oldMessage.toJson()), - }; - await _updateByMessageId(messageId, data, notifyFlutterState: false); - } - - static Future> convertToDbMessage( - List fromDb) async { - try { - List parsedUsers = []; - final box = await getMediaStorage(); - for (int i = 0; i < fromDb.length; i++) { - dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt]; - - MessageContent content = MessageContent.fromJson( - jsonDecode(fromDb[i][columnMessageContentJson])); - - var tmp = content; - if (messageOpenedAt != null) { - messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]); - if (tmp is TextMessageContent && messageOpenedAt != null) { - if (calculateTimeDifference(DateTime.now(), messageOpenedAt) - .inHours >= - 24) { - deleteTextContent(fromDb[i][columnMessageId], tmp); - } - } - } - int? messageOtherId = fromDb[i][columnMessageOtherId]; - - bool isDownloaded = true; - if (messageOtherId != null) { - if (content is MediaMessageContent) { - // when the media was send from the user itself the content is null - isDownloaded = - box.containsKey("${content.downloadToken}_downloaded"); - } - } - parsedUsers.add( - DbMessage( - sendAt: DateTime.tryParse(fromDb[i][columnSendAt])!, - messageId: fromDb[i][columnMessageId], - messageOtherId: messageOtherId, - otherUserId: fromDb[i][columnOtherUserId], - messageContent: content, - isDownloaded: isDownloaded, - messageOpenedAt: messageOpenedAt, - messageAcknowledgeByUser: - fromDb[i][columnMessageAcknowledgeByUser] == 1, - messageAcknowledgeByServer: - fromDb[i][columnMessageAcknowledgeByServer] == 1, - ), - ); - } - return parsedUsers; - } catch (e) { - Logger("messages_model/convertToDbMessage").shout("$e"); - return []; - } - } -} diff --git a/lib/src/components/message_send_state_icon.dart b/lib/src/components/message_send_state_icon.dart index a3318a2..1c7da64 100644 --- a/lib/src/components/message_send_state_icon.dart +++ b/lib/src/components/message_send_state_icon.dart @@ -20,7 +20,11 @@ MessageSendState messageSendStateFromMessage(Message msg) { MessageSendState state; if (!msg.acknowledgeByServer) { - state = MessageSendState.sending; + if (msg.messageOtherId == null) { + state = MessageSendState.sending; + } else { + state = MessageSendState.receiving; + } } else { if (msg.messageOtherId == null) { // message send @@ -66,15 +70,12 @@ class _MessageSendStateIconState extends State { textMsg = msg; } if (msg.kind == MessageKind.media) { - MessageJson message = - MessageJson.fromJson(jsonDecode(msg.contentJson!)); - final content = message.content; - if (content is MediaMessageContent) { - if (content.isVideo) { - videoMsg = msg; - } else { - imageMsg = msg; - } + MediaMessageContent content = + MediaMessageContent.fromJson(jsonDecode(msg.contentJson!)); + if (content.isVideo) { + videoMsg = msg; + } else { + imageMsg = msg; } } } @@ -110,9 +111,10 @@ class _MessageSendStateIconState extends State { Widget icon = Placeholder(); MessageSendState state = messageSendStateFromMessage(message); - MessageJson msg = MessageJson.fromJson(jsonDecode(message.contentJson!)); - if (msg.content == null) continue; - Color color = getMessageColorFromType(msg.content!, twonlyColor); + MessageContent? content = MessageContent.fromJson( + message.kind, jsonDecode(message.contentJson!)); + if (content == null) continue; + Color color = getMessageColorFromType(content, twonlyColor); switch (state) { case MessageSendState.receivedOpened: @@ -139,16 +141,20 @@ class _MessageSendStateIconState extends State { break; } - if (message.downloadState == DownloadState.pending) { - text = context.lang.messageSendState_TapToLoad; - } - if (message.downloadState == DownloadState.downloaded) { - text = context.lang.messageSendState_Loading; - icon = getLoaderIcon(color); + if (message.kind == MessageKind.media) { + if (message.downloadState == DownloadState.pending) { + text = context.lang.messageSendState_TapToLoad; + } + if (message.downloadState == DownloadState.downloaded) { + text = context.lang.messageSendState_Loading; + icon = getLoaderIcon(color); + } } icons.add(icon); } + if (icons.isEmpty) return Container(); + Widget icon = icons[0]; if (icons.length == 2) { diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 93d3cc6..7a6ade1 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -29,23 +29,34 @@ class TwonlyDatabase extends _$TwonlyDatabase { Stream> watchMessageNotOpened(int contactId) { return (select(messages) - ..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))) + ..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId)) + ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) .watch(); } - Stream watchLastMessage(int contactId) { + Stream> watchLastMessage(int contactId) { return (select(messages) ..where((t) => t.contactId.equals(contactId)) ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) ..limit(1)) - .watchSingleOrNull(); + .watch(); } Stream> watchAllMessagesFrom(int contactId) { - return (select(messages)..where((t) => t.contactId.equals(contactId))) + return (select(messages) + ..where((t) => t.contactId.equals(contactId)) + ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) .watch(); } + Future> getAllMessagesPendingDownloading() { + return (select(messages) + ..where((t) => + t.downloadState.equals(DownloadState.downloaded.index).not() & + t.kind.equals(MessageKind.media.name))) + .get(); + } + Future> getAllMessagesForRetransmitting() { return (select(messages)..where((t) => t.acknowledgeByServer.equals(false))) .get(); @@ -168,7 +179,9 @@ class TwonlyDatabase extends _$TwonlyDatabase { Stream watchContactsRequested() { final count = contacts.requested.count(distinct: true); - final query = selectOnly(contacts)..where(contacts.requested.equals(true)); + final query = selectOnly(contacts) + ..where(contacts.requested.equals(true) & + contacts.accepted.equals(true).not()); query.addColumns([count]); return query.map((row) => row.read(count)).watchSingle(); } diff --git a/lib/src/database/database.g.dart b/lib/src/database/database.g.dart index 7310ce9..a52e337 100644 --- a/lib/src/database/database.g.dart +++ b/lib/src/database/database.g.dart @@ -756,7 +756,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { downloadState = GeneratedColumn('download_state', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: Constant(DownloadState.pending.index)) + defaultValue: Constant(DownloadState.downloaded.index)) .withConverter($MessagesTable.$converterdownloadState); static const VerificationMeta _acknowledgeByServerMeta = const VerificationMeta('acknowledgeByServer'); diff --git a/lib/src/database/messages_db.dart b/lib/src/database/messages_db.dart index 34fca09..e6be404 100644 --- a/lib/src/database/messages_db.dart +++ b/lib/src/database/messages_db.dart @@ -19,7 +19,7 @@ class Messages extends Table { BoolColumn get acknowledgeByUser => boolean().withDefault(Constant(false))(); IntColumn get downloadState => intEnum() - .withDefault(Constant(DownloadState.pending.index))(); + .withDefault(Constant(DownloadState.downloaded.index))(); BoolColumn get acknowledgeByServer => boolean().withDefault(Constant(false))(); diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 9133d0a..176c677 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -15,6 +15,25 @@ import 'package:twonly/src/utils/misc.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; + +Future tryDownloadAllMediaFiles() async { + + if (!await isAllowedToDownload()) { + return; + } + List messages = + await twonlyDatabase.getAllMessagesPendingDownloading(); + + for (Message message in messages) { + MessageContent? content = MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); + + if (content is MediaMessageContent) { + tryDownloadMedia(message.messageId, message.contactId, content.downloadToken); + } + } + +} + Future tryTransmitMessages() async { List retransmit = await twonlyDatabase.getAllMessagesForRetransmitting(); @@ -36,10 +55,10 @@ Future tryTransmitMessages() async { ); if (resp.isSuccess) { - await twonlyDatabase.updateMessageByMessageId( - msgId, - MessagesCompanion(acknowledgeByServer: Value(true)) - ); + await twonlyDatabase.updateMessageByMessageId( + msgId, + MessagesCompanion(acknowledgeByServer: Value(true)) + ); box.delete("retransmit-$msgId-textmessage"); } else { @@ -49,11 +68,9 @@ Future tryTransmitMessages() async { Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media"); if (encryptedMedia != null) { - final content = MessageJson.fromJson(jsonDecode(retransmit[i].contentJson!)).content; - if (content is MediaMessageContent) { + MediaMessageContent content = MediaMessageContent.fromJson(jsonDecode(retransmit[i].contentJson!)); uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia, content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt); - } } } } diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index 05fc458..b5969ca 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -185,6 +185,7 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { kind: Value(message.kind), messageOtherId: Value(message.messageId), contentJson: Value(content), + downloadState: Value(DownloadState.downloaded), sendAt: Value(message.timestamp), ); diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 39b5aac..1664e54 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -70,10 +70,10 @@ class ApiProvider { Future onConnected() async { await authenticate(); globalCallbackConnectionState(true); - // _reconnectionDelay = 5; if (!globalIsAppInBackground) { tryTransmitMessages(); + tryDownloadAllMediaFiles(); } } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b55de21..269bb8b 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'dart:math'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; @@ -235,3 +236,13 @@ Future authenticateUser(String localizedReason, } return false; } + +Future isAllowedToDownload() async { + final List connectivityResult = + await (Connectivity().checkConnectivity()); + if (connectivityResult.contains(ConnectivityResult.mobile)) { + Logger("tryDownloadMedia").info("abort download over mobile connection"); + return false; + } + return true; +} diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 0c4d524..42c9c92 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -32,6 +33,12 @@ Future deleteLocalUserData() async { final storage = getSecureStorage(); var password = await storage.read(key: "sqflite_database_password"); await dbProvider.remove(); + + final appDir = await getApplicationSupportDirectory(); + if (appDir.existsSync()) { + appDir.deleteSync(recursive: true); + } + await storage.write(key: "sqflite_database_password", value: password); await storage.deleteAll(); return true; diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 276460c..68264d2 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -36,8 +36,9 @@ class ChatListEntry extends StatelessWidget { bool isDownloading = false; List token = []; - final messageJson = MessageJson.fromJson(jsonDecode(message.contentJson!)); - final content = messageJson.content; + MessageContent? content = + MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); + if (message.messageOtherId != null && content is MediaMessageContent) { token = content.downloadToken; isDownloading = message.downloadState == DownloadState.downloading; diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index 0964453..22c7100 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -36,9 +36,45 @@ class _ChatListViewState extends State { Stream> contacts = twonlyDatabase.watchContactsForChatList(); return Scaffold( - appBar: AppBar( - title: GestureDetector( - onTap: () { + appBar: AppBar( + title: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProfileView(), + ), + ); + }, + child: Text("twonly"), + ), + // title: + actions: [ + StreamBuilder( + stream: twonlyDatabase.watchContactsRequested(), + builder: (context, snapshot) { + var count = 0; + if (snapshot.hasData && snapshot.data != null) { + count = snapshot.data!; + } + return NotificationBadge( + count: count.toString(), + child: IconButton( + icon: FaIcon(FontAwesomeIcons.userPlus, size: 18), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ); + }, + ), + ); + }, + ), + IconButton( + onPressed: () { Navigator.push( context, MaterialPageRoute( @@ -46,21 +82,24 @@ class _ChatListViewState extends State { ), ); }, - child: Text("twonly"), - ), - // title: - actions: [ - StreamBuilder( - stream: twonlyDatabase.watchContactsRequested(), - builder: (context, snapshot) { - var count = 0; - if (snapshot.hasData && snapshot.data != null) { - count = snapshot.data!; - } - return NotificationBadge( - count: count.toString(), - child: IconButton( - icon: FaIcon(FontAwesomeIcons.userPlus, size: 18), + icon: FaIcon(FontAwesomeIcons.gear, size: 19), + ) + ], + ), + body: StreamBuilder( + stream: contacts, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == null) { + return Container(); + } + + final contacts = snapshot.data!; + if (contacts.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: OutlinedButton.icon( + icon: Icon(Icons.person_add), onPressed: () { Navigator.push( context, @@ -69,68 +108,32 @@ class _ChatListViewState extends State { ), ); }, - ), - ); - }, - ), - IconButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ProfileView(), - ), - ); - }, - icon: FaIcon(FontAwesomeIcons.gear, size: 19), - ) - ], - ), - body: StreamBuilder( - stream: contacts, - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data == null) { - return Container(); - } - - final contacts = snapshot.data!; - if (contacts.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: OutlinedButton.icon( - icon: Icon(Icons.person_add), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SearchUsernameView())); - }, - label: Text(context.lang.chatListViewSearchUserNameBtn)), - ), - ); - } - - int maxTotalMediaCounter = 0; - if (contacts.isNotEmpty) { - maxTotalMediaCounter = contacts - .map((x) => x.totalMediaCounter) - .reduce((a, b) => a > b ? a : b); - } - - return ListView.builder( - restorationId: 'chat_list_view', - itemCount: contacts.length, - itemBuilder: (BuildContext context, int index) { - final user = contacts[index]; - return UserListItem( - user: user, - maxTotalMediaCounter: maxTotalMediaCounter, - ); - }, + label: Text(context.lang.chatListViewSearchUserNameBtn)), + ), ); - }, - )); + } + + int maxTotalMediaCounter = 0; + if (contacts.isNotEmpty) { + maxTotalMediaCounter = contacts + .map((x) => x.totalMediaCounter) + .reduce((a, b) => a > b ? a : b); + } + + return ListView.builder( + restorationId: 'chat_list_view', + itemCount: contacts.length, + itemBuilder: (BuildContext context, int index) { + final user = contacts[index]; + return UserListItem( + user: user, + maxTotalMediaCounter: maxTotalMediaCounter, + ); + }, + ); + }, + ), + ); } } @@ -156,22 +159,9 @@ class _UserListItem extends State { @override void initState() { super.initState(); - // initAsync(); lastUpdateTime(); } - // Future initAsync() async { - // if (currentMessage != null) { - // if (currentMessage!.downloadState != DownloadState.downloading) { - // final content = widget.lastMessage!.messageContent; - // if (content is MediaMessageContent) { - // tryDownloadMedia(widget.lastMessage!.messageId, - // widget.lastMessage!.otherUserId, content.downloadToken); - // } - // } - // } - // } - void lastUpdateTime() { // Change the color every 200 milliseconds updateTime = Timer.periodic(Duration(milliseconds: 200), (timer) { @@ -193,25 +183,6 @@ class _UserListItem extends State { @override Widget build(BuildContext context) { - final notOpenedMessages = - twonlyDatabase.watchMessageNotOpened(widget.user.userId); - final lastMessage = twonlyDatabase.watchLastMessage(widget.user.userId); - - // if (widget.lastMessage != null) { - // state = widget.lastMessage!.getSendState(); - - // final content = widget.lastMessage!.messageContent; - - // if (widget.lastMessage!.messageReceived && - // content is MediaMessageContent) { - // token = content.downloadToken; - // isDownloading = context - // .watch() - // .currentlyDownloading - // .contains(token.toString()); - // } - // } - int flameCounter = getFlameCounterFromContact(widget.user); return UserContextMenu( @@ -219,25 +190,35 @@ class _UserListItem extends State { child: ListTile( title: Text(getContactDisplayName(widget.user)), subtitle: StreamBuilder( - stream: lastMessage, + stream: twonlyDatabase.watchLastMessage(widget.user.userId), builder: (context, lastMessageSnapshot) { if (!lastMessageSnapshot.hasData) { return Container(); } - if (lastMessageSnapshot.data == null) { + if (lastMessageSnapshot.data!.isEmpty) { return Text(context.lang.chatsTapToSend); } - final lastMessage = lastMessageSnapshot.data!; + final lastMessage = lastMessageSnapshot.data!.first; return StreamBuilder( - stream: notOpenedMessages, + stream: twonlyDatabase.watchMessageNotOpened(widget.user.userId), builder: (context, notOpenedMessagesSnapshot) { if (!lastMessageSnapshot.hasData) { return Container(); } var lastMessages = [lastMessage]; - if (notOpenedMessagesSnapshot.data != null) { + if (notOpenedMessagesSnapshot.data != null && + notOpenedMessagesSnapshot.data!.isNotEmpty) { lastMessages = notOpenedMessagesSnapshot.data!; + var media = + lastMessages.where((x) => x.kind == MessageKind.media); + if (media.isNotEmpty) { + currentMessage = media.first; + } else { + currentMessage = lastMessages.first; + } + } else { + currentMessage = lastMessage; } return Row( diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index b8ea1f3..0e84295 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -86,10 +86,9 @@ class _MediaViewerViewState extends State { await _noScreenshot.screenshotOff(); if (!context.mounted || allMediaFiles.isEmpty) return; - final Message current = allMediaFiles.first; - final MessageJson messageJson = - MessageJson.fromJson(jsonDecode(current.contentJson!)); - final MessageContent? content = messageJson.content; + final current = allMediaFiles.first; + final MediaMessageContent content = + MediaMessageContent.fromJson(jsonDecode(current.contentJson!)); setState(() { // reset current image values @@ -101,17 +100,16 @@ class _MediaViewerViewState extends State { isRealTwonly = false; }); - if (content is MediaMessageContent) { - if (content.isRealTwonly) { - setState(() { - isRealTwonly = true; - }); - if (!showTwonly) { - return; - } + if (content.isRealTwonly) { + setState(() { + isRealTwonly = true; + }); + if (!showTwonly) { + return; } if (isRealTwonly) { + if (!context.mounted) return; bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason, force: false); if (!isAuth) { diff --git a/lib/src/views/contact/contact_view.dart b/lib/src/views/contact/contact_view.dart index e8b3c46..aff6705 100644 --- a/lib/src/views/contact/contact_view.dart +++ b/lib/src/views/contact/contact_view.dart @@ -67,6 +67,8 @@ class _ContactViewState extends State { ), ], ), + if (getContactDisplayName(contact) != contact.username) + Center(child: Text("(${contact.username})")), SizedBox(height: 50), BetterListTile( icon: FontAwesomeIcons.pencil, @@ -127,7 +129,7 @@ class _ContactViewState extends State { Future showNicknameChangeDialog( BuildContext context, Contact contact) { final TextEditingController controller = - TextEditingController(text: contact.displayName); + TextEditingController(text: getContactDisplayName(contact)); return showDialog( context: context,