From 2f3af427712d555e2466bc59a8b15c9963a56143 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 27 Jan 2025 01:43:52 +0100 Subject: [PATCH] start with chat view --- lib/src/app.dart | 17 ++ .../components/message_send_state_icon.dart | 10 +- lib/src/model/contacts_model.dart | 2 +- lib/src/model/identity_key_store_model.dart | 2 +- lib/src/model/json/message.dart | 101 ++++------- lib/src/model/json/message.g.dart | 25 ++- lib/src/model/messages_model.dart | 169 ++++++++++++++++-- lib/src/model/pre_key_model.dart | 2 +- lib/src/model/sender_key_store_model.dart | 2 +- lib/src/model/session_store_model.dart | 2 +- lib/src/providers/api_provider.dart | 49 +++-- lib/src/providers/db_provider.dart | 12 +- lib/src/providers/notify_provider.dart | 42 +++-- lib/src/utils/api.dart | 74 +++++--- lib/src/utils/signal.dart | 7 +- lib/src/views/chat_list_view.dart | 145 ++++++++------- lib/src/views/share_image_view.dart | 5 + 17 files changed, 440 insertions(+), 226 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index aa076af..a080b86 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,3 +1,4 @@ +import 'package:fixnum/fixnum.dart'; import 'package:provider/provider.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/providers/notify_provider.dart'; @@ -11,6 +12,10 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'dart:async'; import 'settings/settings_controller.dart'; +Function(Int64) addSendingTo = (a) {}; +Function(Int64) removeSendingTo = (a) {}; +Function() updateNotifyProvider = () {}; + /// The Widget that configures your application. class MyApp extends StatefulWidget { const MyApp({super.key, required this.settingsController}); @@ -43,6 +48,18 @@ class _MyAppState extends State { context.read().update(); }); + addSendingTo = (a) { + context.read().addSendingTo(a); + }; + + removeSendingTo = (a) { + context.read().removeSendingTo(a); + }; + + updateNotifyProvider = () { + context.read().update(); + }; + context.read().update(); apiProvider.connect(); } diff --git a/lib/src/components/message_send_state_icon.dart b/lib/src/components/message_send_state_icon.dart index 5d2e63f..42583aa 100644 --- a/lib/src/components/message_send_state_icon.dart +++ b/lib/src/components/message_send_state_icon.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; enum MessageSendState { - opened, received, + receivedOpened, + receiving, send, + sendOpened, sending, } class MessageSendStateIcon extends StatelessWidget { final MessageSendState state; - const MessageSendStateIcon({super.key, required this.state}); + const MessageSendStateIcon(this.state, {super.key}); @override Widget build(BuildContext context) { @@ -18,7 +20,8 @@ class MessageSendStateIcon extends StatelessWidget { String text = ""; switch (state) { - case MessageSendState.opened: + case MessageSendState.receivedOpened: + case MessageSendState.sendOpened: icon = Icon( Icons.crop_square, size: 14, @@ -42,6 +45,7 @@ class MessageSendStateIcon extends StatelessWidget { text = "Send"; break; case MessageSendState.sending: + case MessageSendState.receiving: icon = Row( children: [ SizedBox( diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index 1d8fea8..42bfe70 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -38,7 +38,7 @@ class DbContacts extends CvModelBase { static String getCreateTableString() { return """ - CREATE TABLE $tableName ( + CREATE TABLE IF NOT EXISTS $tableName ( $columnUserId INTEGER NOT NULL PRIMARY KEY, $columnDisplayName TEXT, $columnAccepted INT NOT NULL DEFAULT 0, diff --git a/lib/src/model/identity_key_store_model.dart b/lib/src/model/identity_key_store_model.dart index 313c2be..3a6c395 100644 --- a/lib/src/model/identity_key_store_model.dart +++ b/lib/src/model/identity_key_store_model.dart @@ -19,7 +19,7 @@ class DbSignalIdentityKeyStore extends CvModelBase { static String getCreateTableString() { return """ - CREATE TABLE $tableName ( + CREATE TABLE IF NOT EXISTS $tableName ( $columnDeviceId INTEGER NOT NULL, $columnName TEXT NOT NULL, $columnIdentityKey BLOB NOT NULL, diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 7eac3e4..05e46e4 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:json_annotation/json_annotation.dart'; import 'package:twonly/src/utils/json.dart'; part 'message.g.dart'; @@ -9,7 +8,22 @@ enum MessageKind { video, contactRequest, rejectRequest, - acceptRequest + acceptRequest, + ack +} + +extension MessageKindExtension on MessageKind { + String get name => toString().split('.').last; + + static MessageKind fromString(String name) { + return MessageKind.values.firstWhere((e) => e.name == name); + } + + int get index => this.index; + + static MessageKind fromIndex(int index) { + return MessageKind.values[index]; + } } // so _$MessageKindEnumMap gets generated @@ -23,84 +37,33 @@ class Message { @Int64Converter() final MessageKind kind; final MessageContent? content; + final int? messageId; DateTime timestamp; - Message({required this.kind, this.content, required this.timestamp}); + Message( + {required this.kind, + this.messageId, + this.content, + required this.timestamp}); @override String toString() { return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; } - static Message fromJson(String jsonString) { - Map json = jsonDecode(jsonString); - dynamic content; - MessageKind kind = $enumDecode(_$MessageKindEnumMap, json['kind']); - switch (kind) { - case MessageKind.textMessage: - content = TextContent.fromJson(json["content"]); - break; - case MessageKind.image: - content = ImageContent.fromJson(json["content"]); - break; - default: - } - - return Message( - kind: kind, - timestamp: DateTime.parse(json['timestamp'] as String), - content: content, - ); - } - - String toJson() { - var json = { - 'kind': _$MessageKindEnumMap[kind]!, - 'timestamp': timestamp.toIso8601String(), - 'content': content - }; - return jsonEncode(json); - } -} - -abstract class MessageContent { - MessageContent(); - factory MessageContent.fromJson(Map json) { - return TextContent(""); - } - - Map toJson(); -} -// factory MessageContent.fromJson(Map json) => -// _$MessageContentFromJson(json); - -@JsonSerializable() -class TextContent extends MessageContent { - final String text; - - TextContent(this.text); - - factory TextContent.fromJson(Map json) => - _$TextContentFromJson(json); - @override - Map toJson() => _$TextContentToJson(this); + factory Message.fromJson(Map json) => + _$MessageFromJson(json); + Map toJson() => _$MessageToJson(this); } @JsonSerializable() -class ImageContent extends MessageContent { - final List imageToken; +class MessageContent { + final String? text; + final List? downloadToken; - ImageContent(this.imageToken); + MessageContent({required this.text, required this.downloadToken}); - factory ImageContent.fromJson(Map json) => - _$ImageContentFromJson(json); - @override - Map toJson() => _$ImageContentToJson(this); + factory MessageContent.fromJson(Map json) => + _$MessageContentFromJson(json); + Map toJson() => _$MessageContentToJson(this); } - -// @JsonSerializable() -// class VideoContent extends MessageContent { -// final String videoUrl; - -// VideoContent(this.videoUrl); -// } diff --git a/lib/src/model/json/message.g.dart b/lib/src/model/json/message.g.dart index b6fb76a..2e720b8 100644 --- a/lib/src/model/json/message.g.dart +++ b/lib/src/model/json/message.g.dart @@ -21,10 +21,12 @@ const _$MessageKindEnumMap = { MessageKind.contactRequest: 'contactRequest', MessageKind.rejectRequest: 'rejectRequest', MessageKind.acceptRequest: 'acceptRequest', + MessageKind.ack: 'ack', }; Message _$MessageFromJson(Map json) => Message( kind: $enumDecode(_$MessageKindEnumMap, json['kind']), + messageId: (json['messageId'] as num?)?.toInt(), content: json['content'] == null ? null : MessageContent.fromJson(json['content'] as Map), @@ -34,25 +36,20 @@ Message _$MessageFromJson(Map json) => Message( Map _$MessageToJson(Message instance) => { 'kind': _$MessageKindEnumMap[instance.kind]!, 'content': instance.content, + 'messageId': instance.messageId, 'timestamp': instance.timestamp.toIso8601String(), }; -TextContent _$TextContentFromJson(Map json) => TextContent( - json['text'] as String, - ); - -Map _$TextContentToJson(TextContent instance) => - { - 'text': instance.text, - }; - -ImageContent _$ImageContentFromJson(Map json) => ImageContent( - (json['imageToken'] as List) - .map((e) => (e as num).toInt()) +MessageContent _$MessageContentFromJson(Map json) => + MessageContent( + text: json['text'] as String?, + downloadToken: (json['downloadToken'] as List?) + ?.map((e) => (e as num).toInt()) .toList(), ); -Map _$ImageContentToJson(ImageContent instance) => +Map _$MessageContentToJson(MessageContent instance) => { - 'imageToken': instance.imageToken, + 'text': instance.text, + 'downloadToken': instance.downloadToken, }; diff --git a/lib/src/model/messages_model.dart b/lib/src/model/messages_model.dart index 378cd27..eb547c5 100644 --- a/lib/src/model/messages_model.dart +++ b/lib/src/model/messages_model.dart @@ -1,27 +1,176 @@ +import 'dart:convert'; + import 'package:cv/cv.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/main.dart'; +import 'package:twonly/src/model/json/message.dart'; + +class DbMessage { + DbMessage({ + required this.messageId, + required this.messageOtherId, + required this.otherUserId, + required this.messageMessageKind, + required this.messageContent, + required this.messageOpenedAt, + required this.messageAcknowledge, + required this.sendOrReceivedAt, + }); + + int messageId; + // is this null then the message was sent from the user itself + int? messageOtherId; + int otherUserId; + MessageKind messageMessageKind; + MessageContent messageContent; + DateTime? messageOpenedAt; + bool messageAcknowledge; + DateTime sendOrReceivedAt; +} class DbMessages extends CvModelBase { static const tableName = "messages"; - static const columnMessageId = "messageId"; + static const columnMessageId = "id"; final messageId = CvField(columnMessageId); - static const columnBody = "body"; - final messageBody = CvField(columnBody); + static const columnMessageOtherId = "message_other_id"; + final messageOtherId = CvField(columnMessageOtherId); - static const columnCreatedAt = "created_at"; - final createdAt = CvField(columnCreatedAt); + static const columnOtherUserId = "other_user_id"; + final otherUserId = CvField(columnOtherUserId); + + static const columnMessageKind = "message_kind"; + final messageMessageKind = CvField(columnMessageKind); + + static const columnMessageContentJson = "message_json"; + final messageContentJson = CvField(columnMessageContentJson); + + static const columnMessageOpenedAt = "message_opened_at"; + final messageOpenedAt = CvField(columnMessageOpenedAt); + + static const columnMessageAcknowledge = "message_acknowledged"; + final messageAcknowledge = CvField(columnMessageAcknowledge); + + static const columnSendOrReceivedAt = "message_send_or_received_at"; + final sendOrReceivedAt = CvField(columnSendOrReceivedAt); + + static const columnUpdatedAt = "updated_at"; + final updatedAt = CvField(columnUpdatedAt); static String getCreateTableString() { return """ - CREATE TABLE $tableName ( - $columnMessageId INTEGER NOT NULL, - $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY ($columnMessageId) + CREATE TABLE IF NOT EXISTS $tableName ( + $columnMessageId INTEGER NOT NULL PRIMARY KEY, + $columnMessageOtherId INTEGER DEFAULT NULL, + $columnOtherUserId INTEGER DEFAULT NULL, + $columnMessageKind INTEGER NOT NULL, + $columnMessageAcknowledge INTEGER NOT NULL DEFAULT 0, + $columnMessageContentJson TEXT NOT NULL, + $columnMessageOpenedAt DATETIME DEFAULT NULL, + $columnSendOrReceivedAt DATETIME DEFAULT CURRENT_TIMESTAMP, + $columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ) """; } + static Future insertMyMessage( + int userIdFrom, MessageKind kind, String jsonContent) async { + try { + int messageId = await dbProvider.db!.insert(tableName, { + columnMessageKind: kind.index, + columnMessageContentJson: jsonContent, + columnOtherUserId: userIdFrom, + columnSendOrReceivedAt: DateTime.now().toIso8601String() + }); + return messageId; + } catch (e) { + Logger("contacts_model/getUsers").shout("$e"); + return null; + } + } + + static Future insertOtherMessage(int userIdFrom, MessageKind kind, + int messageId, String jsonContent) async { + try { + await dbProvider.db!.insert(tableName, { + columnMessageOtherId: messageId, + columnMessageKind: kind.index, + columnMessageContentJson: jsonContent, + columnOtherUserId: userIdFrom + }); + return true; + } catch (e) { + Logger("contacts_model/getUsers").shout("$e"); + return false; + } + } + + static List convertToDbMessage(List fromDb) { + try { + List parsedUsers = []; + for (int i = 0; i < fromDb.length; i++) { + dynamic messageOpenedAt = fromDb[i][columnMessageOpenedAt]; + if (messageOpenedAt != null) { + messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]); + } + print("Datetime: ${fromDb[i][columnSendOrReceivedAt]}"); + print( + "Datetime parsed: ${DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])}"); + parsedUsers.add( + DbMessage( + sendOrReceivedAt: + DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!, + messageId: fromDb[i][columnMessageId], + messageOtherId: fromDb[i][columnMessageOtherId], + otherUserId: fromDb[i][columnOtherUserId], + messageMessageKind: + MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]), + messageContent: MessageContent.fromJson( + jsonDecode(fromDb[i][columnMessageContentJson])), + messageOpenedAt: messageOpenedAt, + messageAcknowledge: fromDb[i][columnMessageAcknowledge] == 1, + ), + ); + } + return parsedUsers; + } catch (e) { + Logger("messages_model/convertToDbMessage").shout("$e"); + return []; + } + } + + static Future getLastMessagesForPreviewForUser( + int otherUserId) async { + var rows = await dbProvider.db!.query(tableName, + where: "$columnOtherUserId = ?", + whereArgs: [otherUserId], + orderBy: "$columnUpdatedAt DESC", + limit: 1); + + List messages = convertToDbMessage(rows); + if (messages.isEmpty) return null; + return messages[0]; + } + + static Future acknowledgeMessage(int fromUserId, int messageId) async { + Map valuesToUpdate = { + columnMessageAcknowledge: 1, + }; + await dbProvider.db!.update( + tableName, + valuesToUpdate, + where: "$messageId = ? AND $columnOtherUserId = ?", + whereArgs: [messageId, fromUserId], + ); + } + @override - List get fields => [messageId, createdAt]; + List get fields => [ + messageId, + messageMessageKind, + messageContentJson, + messageOpenedAt, + sendOrReceivedAt + ]; } diff --git a/lib/src/model/pre_key_model.dart b/lib/src/model/pre_key_model.dart index 2f66de4..cfd8c1f 100644 --- a/lib/src/model/pre_key_model.dart +++ b/lib/src/model/pre_key_model.dart @@ -15,7 +15,7 @@ class DbSignalPreKeyStore extends CvModelBase { static String getCreateTableString() { return """ - CREATE TABLE $tableName ( + CREATE TABLE IF NOT EXISTS $tableName ( $columnPreKeyId INTEGER NOT NULL, $columnPreKey BLOB NOT NULL, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, diff --git a/lib/src/model/sender_key_store_model.dart b/lib/src/model/sender_key_store_model.dart index 792bce8..88b8703 100644 --- a/lib/src/model/sender_key_store_model.dart +++ b/lib/src/model/sender_key_store_model.dart @@ -15,7 +15,7 @@ class DbSignalSenderKeyStore extends CvModelBase { static String getCreateTableString() { return """ - CREATE TABLE $tableName ( + CREATE TABLE IF NOT EXISTS $tableName ( $columnSenderKeyName TEXT NOT NULL, $columnSenderKey BLOB NOT NULL, $columnCreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP, diff --git a/lib/src/model/session_store_model.dart b/lib/src/model/session_store_model.dart index 8e05417..db4d142 100644 --- a/lib/src/model/session_store_model.dart +++ b/lib/src/model/session_store_model.dart @@ -18,7 +18,7 @@ class DbSignalSessionStore extends CvModelBase { static String getCreateTableString() { return """ - CREATE TABLE $tableName ( + CREATE TABLE IF NOT EXISTS $tableName ( $columnDeviceId INTEGER NOT NULL, $columnName TEXT NOT NULL, $columnSessionRecord BLOB NOT NULL, diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 77b4c66..65d310b 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -7,10 +7,12 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; +import 'package:twonly/src/utils/api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; // ignore: library_prefixes @@ -167,23 +169,47 @@ class ApiProvider { Uint8List name = username.value.userdata.username; DbContacts.insertNewContact( utf8.decode(name), fromUserId.toInt(), true); - updateNotifier(); } break; case MessageKind.rejectRequest: DbContacts.deleteUser(fromUserId.toInt()); - updateNotifier(); break; case MessageKind.acceptRequest: DbContacts.acceptUser(fromUserId.toInt()); - updateNotifier(); break; - case MessageKind.image: - log.info("Got image: ${message.content}"); + case MessageKind.ack: + DbMessages.acknowledgeMessage( + fromUserId.toInt(), message.messageId!); + break; default: - log.shout("Got unknown MessageKind $message"); + if (message.kind != MessageKind.textMessage && + message.kind != MessageKind.video && + message.kind != MessageKind.image) { + log.shout("Got unknown MessageKind $message"); + } else { + String content = jsonEncode(message.content!.toJson()); + await DbMessages.insertOtherMessage(fromUserId.toInt(), + message.kind, message.messageId!, content); + + encryptAndSendMessage( + fromUserId, + Message( + kind: MessageKind.ack, + messageId: message.messageId!, + timestamp: DateTime.now(), + ), + ); + + if (message.kind == MessageKind.video || + message.kind == MessageKind.image) { + dynamic content = message.content!; + List downloadToken = content.downloadToken; + tryDownloadMedia(downloadToken); + } + } } } + updateNotifier(); var ok = client.Response_Ok()..none = true; response = client.Response()..ok = ok; } else { @@ -373,16 +399,7 @@ class ApiProvider { return _asResult(resp); } - Future?> uploadData(Uint8List data) async { - Result res = await getUploadToken(data.length); - - if (res.isError || !res.value.hasUploadtoken()) { - Logger("api.dart").shout("Error getting upload token!"); - return null; - } - List uploadToken = res.value.uploadtoken; - log.info("Got token: $uploadToken"); - + Future?> uploadData(List uploadToken, Uint8List data) async { log.shout("fragmentate the data"); var get = ApplicationData_UploadData() diff --git a/lib/src/providers/db_provider.dart b/lib/src/providers/db_provider.dart index 4e10685..8954380 100644 --- a/lib/src/providers/db_provider.dart +++ b/lib/src/providers/db_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/identity_key_store_model.dart'; +import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/model_constants.dart'; import 'package:twonly/src/model/pre_key_model.dart'; import 'package:twonly/src/model/sender_key_store_model.dart'; @@ -53,21 +54,12 @@ class DbProvider { } Future _createDb(Database db) async { - await db.execute('DROP TABLE If EXISTS ${DbSignalSessionStore.tableName}'); await db.execute(DbSignalSessionStore.getCreateTableString()); - - await db.execute('DROP TABLE If EXISTS ${DbSignalPreKeyStore.tableName}'); await db.execute(DbSignalPreKeyStore.getCreateTableString()); - - await db - .execute('DROP TABLE If EXISTS ${DbSignalSenderKeyStore.tableName}'); await db.execute(DbSignalSenderKeyStore.getCreateTableString()); - - await db - .execute('DROP TABLE If EXISTS ${DbSignalIdentityKeyStore.tableName}'); await db.execute(DbSignalIdentityKeyStore.getCreateTableString()); - await db.execute('DROP TABLE If EXISTS ${DbContacts.tableName}'); await db.execute(DbContacts.getCreateTableString()); + await db.execute(DbMessages.getCreateTableString()); } Future open() async { diff --git a/lib/src/providers/notify_provider.dart b/lib/src/providers/notify_provider.dart index 26ea700..bf9d570 100644 --- a/lib/src/providers/notify_provider.dart +++ b/lib/src/providers/notify_provider.dart @@ -1,5 +1,9 @@ +import 'dart:collection'; + +import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/messages_model.dart'; /// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool // ignore: prefer_mixin @@ -7,14 +11,19 @@ class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin { // The page index of the HomeView widget int _activePageIdx = 0; - int _newContactRequests = 0; List _allContacts = []; + final Map _lastMessagesGroupedByUser = {}; - List _sendingCurrentlyTo = []; + final List _sendingCurrentlyTo = []; - int get newContactRequests => _newContactRequests; + int get newContactRequests => _allContacts + .where((contact) => !contact.accepted && contact.requested) + .length; List get allContacts => _allContacts; - List get sendingCurrentlyTo => _sendingCurrentlyTo; + Map get lastMessagesGroupedByUser => + _lastMessagesGroupedByUser; + HashSet get sendingCurrentlyTo => + HashSet.from(_sendingCurrentlyTo); int get activePageIdx => _activePageIdx; @@ -23,18 +32,29 @@ class NotifyProvider with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); } - void addSendingTo(List users) { - _sendingCurrentlyTo.addAll(users); + void addSendingTo(Int64 user) { + _sendingCurrentlyTo.add(user); + notifyListeners(); + } + + // removes the first occurrence of the user + void removeSendingTo(Int64 user) { + int index = _sendingCurrentlyTo.indexOf(user); + if (index != -1) { + _sendingCurrentlyTo.removeAt(index); + } notifyListeners(); } void update() async { _allContacts = await DbContacts.getUsers(); - - _newContactRequests = _allContacts - .where((contact) => !contact.accepted && contact.requested) - .length; - print(_newContactRequests); + for (Contact contact in _allContacts) { + DbMessage? last = await DbMessages.getLastMessagesForPreviewForUser( + contact.userId.toInt()); + if (last != null) { + _lastMessagesGroupedByUser[last.otherUserId] = last; + } + } notifyListeners(); } diff --git a/lib/src/utils/api.dart b/lib/src/utils/api.dart index fed91ca..0a1ef22 100644 --- a/lib/src/utils/api.dart +++ b/lib/src/utils/api.dart @@ -6,8 +6,10 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:twonly/main.dart'; +import 'package:twonly/src/app.dart'; import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/notify_provider.dart'; @@ -25,7 +27,7 @@ Future addNewContact(String username) async { if (!added) { print("RETURN FALSE HIER!!!"); - // return false; + return false; } if (await SignalHelper.addNewContact(res.value.userdata)) { @@ -59,8 +61,11 @@ Future encryptAndSendMessage(Int64 userId, Message msg) async { Result resp = await apiProvider.sendTextMessage(userId, bytes); - Logger("encryptAndSendMessage") - .shout("handle errors here and store them in the database"); + if (resp.isError) { + // TODO: + Logger("encryptAndSendMessage") + .shout("handle errors here and store them in the database"); + } return resp; } @@ -94,12 +99,49 @@ Future createNewUser(String username, String inviteCode) async { return res; } +Future sendImageToSingleTarget( + BuildContext context, Int64 target, Uint8List encryptBytes) async { + Result res = await apiProvider.getUploadToken(encryptBytes.length); + + if (res.isError || !res.value.hasUploadtoken()) { + Logger("api.dart").shout("Error getting upload token!"); + return null; + } + List uploadToken = res.value.uploadtoken; + Logger("sendImageToSingleTarget").info("Got token: $uploadToken"); + + MessageContent content = + MessageContent(text: null, downloadToken: uploadToken); + int? messageId = await DbMessages.insertMyMessage( + target.toInt(), MessageKind.image, jsonEncode(content.toJson())); + if (messageId == null) return; + + updateNotifyProvider(); + + List? imageToken = + await apiProvider.uploadData(uploadToken, encryptBytes); + if (imageToken == null) { + Logger("api.dart").shout("handle error uploading like saving..."); + return; + } + + print("TODO: insert into DB and then create this MESSAGE"); + + Message msg = Message( + kind: MessageKind.image, + messageId: messageId, + content: content, + timestamp: DateTime.now(), + ); + + await encryptAndSendMessage(target, msg); + removeSendingTo(target); +} + Future sendImage( BuildContext context, List users, String imagePath) async { // 1. set notifier provider - context.read().addSendingTo(users); - File imageFile = File(imagePath); Uint8List? imageBytes = await getCompressedImage(imageFile); @@ -116,20 +158,12 @@ Future sendImage( Logger("api.dart").shout("Error encrypting image!"); continue; } - - List? imageToken = await apiProvider.uploadData(encryptedImage); - if (imageToken == null) { - Logger("api.dart").shout("handle error uploading like saving..."); - continue; - } - - Message msg = Message( - kind: MessageKind.image, - content: ImageContent(imageToken), - timestamp: DateTime.timestamp(), - ); - - print("Send image to $target"); - encryptAndSendMessage(target, msg); + sendImageToSingleTarget(context, target, encryptedImage); } } + +Future tryDownloadMedia(List imageToken, {bool force = false}) async { + print("check if free network connection"); + + print("Downloading: " + imageToken.toString()); +} diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index 018f3b8..af7cf7b 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -218,8 +218,8 @@ Future encryptMessage(Message msg, Int64 target) async { SessionCipher session = SessionCipher.fromStore( signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId)); - final ciphertext = await session - .encrypt(Uint8List.fromList(gzip.encode(utf8.encode(msg.toJson())))); + final ciphertext = await session.encrypt( + Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))))); var b = BytesBuilder(); b.add(ciphertext.serialize()); @@ -256,7 +256,8 @@ Future getDecryptedText(Int64 source, Uint8List msg) async { } else { return null; } - Message dectext = Message.fromJson(utf8.decode(gzip.decode(plaintext))); + Message dectext = + Message.fromJson(jsonDecode(utf8.decode(gzip.decode(plaintext)))); return dectext; } catch (e) { Logger("utils/signal").shout(e.toString()); diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 65122ef..05fdf47 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -1,9 +1,13 @@ +import 'dart:collection'; + +import 'package:fixnum/fixnum.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/providers/notify_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chat_item_details_view.dart'; @@ -35,40 +39,19 @@ class ChatListView extends StatefulWidget { } class _ChatListViewState extends State { - int _secondsSinceOpen = 0; - late Timer _timer; - List _activeUsers = []; - - @override - void initState() { - super.initState(); - _startTimer(); - _loadActiveUsers(); - } - - Future _loadActiveUsers() async { - _activeUsers = context.read().allContacts; - } - - void _startTimer() { - _timer = Timer.periodic(Duration(seconds: 1), (timer) { - setState(() { - _secondsSinceOpen++; - }); - }); - } - - @override - void dispose() { - _timer.cancel(); // Cancel the timer when the widget is disposed - super.dispose(); - } - @override Widget build(BuildContext context) { - List sendingCurrentlyTo = + Map lastMessages = + context.watch().lastMessagesGroupedByUser; + + HashSet sendingCurrentlyTo = context.watch().sendingCurrentlyTo; + List activeUsers = context + .read() + .allContacts + .where((x) => lastMessages.containsKey(x.userId.toInt())) + .toList(); return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.chatsTitle), @@ -91,39 +74,44 @@ class _ChatListViewState extends State { ), body: Column( children: [ - if (sendingCurrentlyTo.isNotEmpty) - Container( - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: ListTile( - leading: Stack( - // child: Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator( - strokeWidth: 1, - ), - Icon( - Icons.send, // Replace with your desired icon - color: Theme.of(context).colorScheme.primary, - size: 20, // Adjust the size as needed - ), - ], - // ), - ), - title: Text(sendingCurrentlyTo - .map((e) => e.displayName) - .toList() - .join(", ")), - ), - ), + // if (sendingCurrentlyTo.isNotEmpty) + // Container( + // padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), + // child: ListTile( + // leading: Stack( + // // child: Stack( + // alignment: Alignment.center, + // children: [ + // CircularProgressIndicator( + // strokeWidth: 1, + // ), + // Icon( + // Icons.send, // Replace with your desired icon + // color: Theme.of(context).colorScheme.primary, + // size: 20, // Adjust the size as needed + // ), + // ], + // // ), + // ), + // title: Text(sendingCurrentlyTo + // .map((e) => _activeUsers + // .where((c) => c.userId == e) + // .map((c) => c.displayName)) + // .toList() + // .join(", ")), + // ), + // ), // Expanded( child: ListView.builder( restorationId: 'chat_list_view', - itemCount: _activeUsers.length, + itemCount: activeUsers.length, itemBuilder: (BuildContext context, int index) { - final user = _activeUsers[index]; + final user = activeUsers[index]; return UserListItem( - user: user, secondsSinceOpen: _secondsSinceOpen); + user: user, + lastMessage: lastMessages[user.userId.toInt()]!, + isSending: sendingCurrentlyTo.contains(user.userId), + ); }, ), ) @@ -134,12 +122,14 @@ class _ChatListViewState extends State { class UserListItem extends StatefulWidget { final Contact user; - final int secondsSinceOpen; + final DbMessage lastMessage; + final bool isSending; const UserListItem({ super.key, required this.user, - required this.secondsSinceOpen, + required this.isSending, + required this.lastMessage, }); @override @@ -153,7 +143,7 @@ class _UserListItem extends State { @override void initState() { super.initState(); - _loadAsync(); + //_loadAsync(); } Future _loadAsync() async { @@ -164,19 +154,44 @@ class _UserListItem extends State { @override Widget build(BuildContext context) { + MessageSendState state; + // int lastMessageInSeconds = widget.lastMessage.sendOrReceivedAt; + //print(widget.lastMessage.sendOrReceivedAt); + int lastMessageInSeconds = DateTime.now() + .difference(widget.lastMessage.sendOrReceivedAt) + .inSeconds; + + if (widget.isSending) { + state = MessageSendState.sending; + } else { + if (widget.lastMessage.messageOtherId == null) { + // message send + if (widget.lastMessage.messageOpenedAt == null) { + state = MessageSendState.send; + } else { + state = MessageSendState.sendOpened; + } + } else { + // message received + if (widget.lastMessage.messageOpenedAt == null) { + state = MessageSendState.received; + } else { + state = MessageSendState.receivedOpened; + } + } + } + return UserContextMenu( user: widget.user, child: ListTile( title: Text(widget.user.displayName), subtitle: Row( children: [ - // MessageSendStateIcon( - // state: widget.user.state, - // ), + MessageSendStateIcon(state), Text("•"), const SizedBox(width: 5), Text( - formatDuration(lastMessageInSeconds + widget.secondsSinceOpen), + formatDuration(lastMessageInSeconds), style: TextStyle(fontSize: 12), ), if (flames > 0) diff --git a/lib/src/views/share_image_view.dart b/lib/src/views/share_image_view.dart index bcf336c..cf2c57c 100644 --- a/lib/src/views/share_image_view.dart +++ b/lib/src/views/share_image_view.dart @@ -3,6 +3,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/src/app.dart'; import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/initialsavatar.dart'; @@ -111,6 +112,10 @@ class _ShareImageView extends State { FilledButton.icon( icon: Icon(Icons.send), onPressed: () async { + for (Int64 a in _selectedUserIds) { + addSendingTo(a); + } + sendImage( context, _users