diff --git a/lib/src/model/messages_model.dart b/lib/src/model/messages_model.dart index 8e5e2ac..45ae7cd 100644 --- a/lib/src/model/messages_model.dart +++ b/lib/src/model/messages_model.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:twonly/main.dart'; import 'package:twonly/src/app.dart'; import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/providers/api/api.dart'; class DbMessage { DbMessage({ @@ -15,6 +16,7 @@ class DbMessage { required this.messageContent, required this.messageOpenedAt, required this.messageAcknowledgeByUser, + required this.isDownloaded, required this.messageAcknowledgeByServer, required this.sendOrReceivedAt, }); @@ -27,6 +29,7 @@ class DbMessage { MessageContent? messageContent; DateTime? messageOpenedAt; bool messageAcknowledgeByUser; + bool isDownloaded; bool messageAcknowledgeByServer; DateTime sendOrReceivedAt; @@ -130,11 +133,11 @@ class DbMessages extends CvModelBase { } } - static Future insertOtherMessage(int userIdFrom, MessageKind kind, - int messageId, String jsonContent) async { + static Future insertOtherMessage(int userIdFrom, MessageKind kind, + int messageOtherId, String jsonContent) async { try { - await dbProvider.db!.insert(tableName, { - columnMessageOtherId: messageId, + int messageId = await dbProvider.db!.insert(tableName, { + columnMessageOtherId: messageOtherId, columnMessageKind: kind.index, columnMessageContentJson: jsonContent, columnMessageAcknowledgeByServer: 1, @@ -144,13 +147,26 @@ class DbMessages extends CvModelBase { columnSendOrReceivedAt: DateTime.now().toIso8601String() }); globalCallBackOnMessageChange(userIdFrom); - return true; + return messageId; } catch (e) { Logger("contacts_model/getUsers").shout("$e"); - return false; + return null; } } + static Future> getAllMessagesForUser(int otherUserId) async { + var rows = await dbProvider.db!.query( + tableName, + where: "$columnOtherUserId = ?", + whereArgs: [otherUserId], + orderBy: "$columnUpdatedAt DESC", + ); + + List messages = await convertToDbMessage(rows); + + return messages; + } + static Future getLastMessagesForPreviewForUser( int otherUserId) async { var rows = await dbProvider.db!.query( @@ -161,7 +177,7 @@ class DbMessages extends CvModelBase { limit: 10, ); - List messages = convertToDbMessage(rows); + List messages = await convertToDbMessage(rows); // check if there is a message which was not ack by the server List notAckByServer = @@ -177,13 +193,11 @@ class DbMessages extends CvModelBase { return messages[0]; } - static Future acknowledgeMessageByServer(int messageId) async { - Map valuesToUpdate = { - columnMessageAcknowledgeByServer: 1, - }; + static Future _updateByMessageId( + int messageId, Map data) async { await dbProvider.db!.update( tableName, - valuesToUpdate, + data, where: "$messageId = ?", whereArgs: [messageId], ); @@ -193,6 +207,20 @@ class DbMessages extends CvModelBase { } } + static Future userOpenedMessage(int messageId) async { + Map data = { + columnMessageOpenedAt: DateTime.now().toIso8601String(), + }; + await _updateByMessageId(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 = { @@ -216,7 +244,8 @@ class DbMessages extends CvModelBase { sendOrReceivedAt ]; - static List convertToDbMessage(List fromDb) { + static Future> convertToDbMessage( + List fromDb) async { try { List parsedUsers = []; for (int i = 0; i < fromDb.length; i++) { @@ -229,6 +258,16 @@ class DbMessages extends CvModelBase { content = MessageContent.fromJson( jsonDecode(fromDb[i][columnMessageContentJson])); } + MessageKind messageKind = + MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]); + bool isDownloaded = true; + if (messageKind == MessageKind.image || + messageKind == MessageKind.video) { + // when the media was send from the user itself the content is null + if (content != null) { + isDownloaded = await isMediaDownloaded(content.downloadToken!); + } + } parsedUsers.add( DbMessage( sendOrReceivedAt: @@ -236,9 +275,9 @@ class DbMessages extends CvModelBase { messageId: fromDb[i][columnMessageId], messageOtherId: fromDb[i][columnMessageOtherId], otherUserId: fromDb[i][columnOtherUserId], - messageKind: - MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]), + messageKind: messageKind, messageContent: content, + isDownloaded: isDownloaded, messageOpenedAt: messageOpenedAt, messageAcknowledgeByUser: fromDb[i][columnMessageAcknowledgeByUser] == 1, diff --git a/lib/src/proto/api/server_to_client.pb.dart b/lib/src/proto/api/server_to_client.pb.dart index df5a0f5..f6f6d03 100644 --- a/lib/src/proto/api/server_to_client.pb.dart +++ b/lib/src/proto/api/server_to_client.pb.dart @@ -281,6 +281,7 @@ class DownloadData extends $pb.GeneratedMessage { $core.List<$core.int>? uploadToken, $core.int? offset, $core.List<$core.int>? data, + $core.bool? fin, }) { final $result = create(); if (uploadToken != null) { @@ -292,6 +293,9 @@ class DownloadData extends $pb.GeneratedMessage { if (data != null) { $result.data = data; } + if (fin != null) { + $result.fin = fin; + } return $result; } DownloadData._() : super(); @@ -302,6 +306,7 @@ class DownloadData extends $pb.GeneratedMessage { ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY) ..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3) ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) + ..aOB(4, _omitFieldNames ? '' : 'fin') ..hasRequiredFields = false ; @@ -352,6 +357,15 @@ class DownloadData extends $pb.GeneratedMessage { $core.bool hasData() => $_has(2); @$pb.TagNumber(3) void clearData() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get fin => $_getBF(3); + @$pb.TagNumber(4) + set fin($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasFin() => $_has(3); + @$pb.TagNumber(4) + void clearFin() => clearField(4); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/proto/api/server_to_client.pbjson.dart b/lib/src/proto/api/server_to_client.pbjson.dart index acfbda6..0a799ae 100644 --- a/lib/src/proto/api/server_to_client.pbjson.dart +++ b/lib/src/proto/api/server_to_client.pbjson.dart @@ -73,13 +73,14 @@ const DownloadData$json = { {'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'}, {'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'}, {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, + {'1': 'fin', '3': 4, '4': 1, '5': 8, '10': 'fin'}, ], }; /// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode( 'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm' - 'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh'); + 'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 4d1443b..067243c 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -104,24 +105,34 @@ Future sendImage(List userIds, String imagePath) async { } Future tryDownloadMedia(List imageToken, {bool force = false}) async { - print("check if free network connection"); - - print("Downloading: " + imageToken.toString()); + if (!force) { + // TODO: create option to enable download via mobile data + final List connectivityResult = + await (Connectivity().checkConnectivity()); + if (connectivityResult.contains(ConnectivityResult.mobile)) { + Logger("tryDownloadMedia").info("abort download over mobile connection"); + return; + } + } + Logger("tryDownloadMedia").info("Downloading: $imageToken"); + apiProvider.triggerDownload(imageToken); +} +Future getDownloadedMedia( + List mediaToken, int messageId) async { final box = await getMediaStorage(); + Uint8List? media = box.get("${mediaToken}_downloaded"); + // box.delete(mediaToken.toString()); + // box.delete("${mediaToken}_downloaded"); + // box.delete("${mediaToken}_fromUserId"); + // await DbMessages.userOpenedMessage(messageId); - // Uint8List imageBytes = Uint8List.fromList([0]); - - // box.put(imageToken.toString(), imageBytes); - box.close(); + return media; } Future isMediaDownloaded(List mediaToken) async { final box = await getMediaStorage(); - - // box.put('secret', 'Hive is awesome'); - - return box.containsKey(mediaToken.toString()); + return box.containsKey("${mediaToken}_downloaded"); } Future initMediaStorage() async { diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index b50e1f1..6ebc449 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -1,15 +1,20 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:logging/logging.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/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/proto/api/server_to_client.pbserver.dart'; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; // ignore: library_prefixes @@ -18,77 +23,22 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper; Future handleServerMessage(server.ServerToClient msg) async { client.Response? response; - if (msg.v0.hasRequestNewPreKeys()) { - List localPreKeys = await SignalHelper.getPreKeys(); - - List prekeysList = []; - for (int i = 0; i < localPreKeys.length; i++) { - prekeysList.add(client.Response_PreKey() - ..id = Int64(localPreKeys[i].id) - ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); + try { + if (msg.v0.hasRequestNewPreKeys()) { + response = await handleRequestNewPreKey(); + } else if (msg.v0.hasNewMessage()) { + Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); + Int64 fromUserId = msg.v0.newMessage.fromUserId; + response = await handleNewMessage(fromUserId, body); + } else if (msg.v0.hasDownloaddata()) { + response = await handleDownloadData(msg.v0.downloaddata); + } else { + Logger("handleServerMessage") + .shout("Got a new message from the server: $msg"); + return; } - var prekeys = client.Response_Prekeys(prekeys: prekeysList); - var ok = client.Response_Ok()..prekeys = prekeys; - response = client.Response()..ok = ok; - } else if (msg.v0.hasNewMessage()) { - Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); - Int64 fromUserId = msg.v0.newMessage.fromUserId; - Message? message = await SignalHelper.getDecryptedText(fromUserId, body); - if (message != null) { - switch (message.kind) { - case MessageKind.contactRequest: - Result username = await apiProvider.getUsername(fromUserId); - if (username.isSuccess) { - Uint8List name = username.value.userdata.username; - DbContacts.insertNewContact( - utf8.decode(name), fromUserId.toInt(), true); - } - break; - case MessageKind.rejectRequest: - DbContacts.deleteUser(fromUserId.toInt()); - break; - case MessageKind.acceptRequest: - DbContacts.acceptUser(fromUserId.toInt()); - break; - case MessageKind.ack: - DbMessages.acknowledgeMessageByUser( - fromUserId.toInt(), message.messageId!); - break; - default: - if (message.kind != MessageKind.textMessage && - message.kind != MessageKind.video && - message.kind != MessageKind.image) { - Logger("handleServerMessages") - .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); - } - } - } - } - var ok = client.Response_Ok()..none = true; - response = client.Response()..ok = ok; - } else { - Logger("handleServerMessage") - .shout("Got a new message from the server: $msg"); - return; + } catch (e) { + response = client.Response()..error = ErrorCode.InternalError; } var v0 = client.V0() @@ -97,3 +47,121 @@ Future handleServerMessage(server.ServerToClient msg) async { apiProvider.sendResponse(ClientToServer()..v0 = v0); } + +Future handleDownloadData(DownloadData data) async { + debugPrint("Downloading: ${data.uploadToken} ${data.fin}"); + final box = await getMediaStorage(); + + String boxId = data.uploadToken.toString(); + Uint8List? buffered = box.get(boxId); + Uint8List downloadedBytes; + if (buffered != null) { + if (data.offset != buffered.length) { + return client.Response()..error = ErrorCode.BadRequest; + } + var b = BytesBuilder(); + b.add(buffered); + b.add(data.data); + + downloadedBytes = b.takeBytes(); + } else { + downloadedBytes = Uint8List.fromList(data.data); + } + + if (data.fin) { + SignalHelper.getSignalStore(); + int fromUserId = box.get("${data.uploadToken}_fromUserId")!; + Uint8List? rawBytes = + await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId)); + + if (rawBytes != null) { + box.put("${data.uploadToken}_downloaded", rawBytes); + } + + box.delete(boxId); + globalCallBackOnMessageChange(fromUserId); + } else { + box.put(boxId, downloadedBytes); + } + + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; +} + +Future handleNewMessage( + Int64 fromUserId, Uint8List body) async { + Message? message = await SignalHelper.getDecryptedText(fromUserId, body); + if (message != null) { + switch (message.kind) { + case MessageKind.contactRequest: + Result username = await apiProvider.getUsername(fromUserId); + if (username.isSuccess) { + Uint8List name = username.value.userdata.username; + DbContacts.insertNewContact( + utf8.decode(name), fromUserId.toInt(), true); + } + break; + case MessageKind.rejectRequest: + DbContacts.deleteUser(fromUserId.toInt()); + break; + case MessageKind.acceptRequest: + DbContacts.acceptUser(fromUserId.toInt()); + break; + case MessageKind.ack: + DbMessages.acknowledgeMessageByUser( + fromUserId.toInt(), message.messageId!); + break; + default: + if (message.kind != MessageKind.textMessage && + message.kind != MessageKind.video && + message.kind != MessageKind.image) { + Logger("handleServerMessages") + .shout("Got unknown MessageKind $message"); + } else { + String content = jsonEncode(message.content!.toJson()); + int? messageId = await DbMessages.insertOtherMessage( + fromUserId.toInt(), message.kind, message.messageId!, content); + + if (messageId == null) { + return client.Response()..error = ErrorCode.InternalError; + } + + 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; + + Box box = await getMediaStorage(); + box.put("${downloadToken}_fromUserId", fromUserId.toInt()); + + tryDownloadMedia(downloadToken); + } + } + } + } + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; +} + +Future handleRequestNewPreKey() async { + List localPreKeys = await SignalHelper.getPreKeys(); + + List prekeysList = []; + for (int i = 0; i < localPreKeys.length; i++) { + prekeysList.add(client.Response_PreKey() + ..id = Int64(localPreKeys[i].id) + ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); + } + var prekeys = client.Response_Prekeys(prekeys: prekeysList); + var ok = client.Response_Ok()..prekeys = prekeys; + return client.Response()..ok = ok; +} diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index f65aad6..d5729d0 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -255,6 +255,13 @@ class ApiProvider { return await _sendRequestV0(req); } + Future triggerDownload(List token) async { + var get = ApplicationData_DownloadData()..uploadToken = token; + var appData = ApplicationData()..downloaddata = get; + var req = createClientToServerFromApplicationData(appData); + return await _sendRequestV0(req); + } + Future?> uploadData(List uploadToken, Uint8List data) async { log.shout("fragmentate the data"); diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index af7cf7b..ae915ac 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -211,6 +211,36 @@ Future encryptBytes(Uint8List bytes, Int64 target) async { } } +Future decryptBytes(Uint8List bytes, Int64 target) async { + try { + ConnectSignalProtocolStore signalStore = (await getSignalStore())!; + + SessionCipher session = SessionCipher.fromStore( + signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId)); + + List? msgs = removeLastFourBytes(bytes); + if (msgs == null) return null; + Uint8List body = msgs[0]; + int type = bytesToInt(msgs[1]); + + Uint8List plaintext; + if (type == CiphertextMessage.prekeyType) { + PreKeySignalMessage pre = PreKeySignalMessage(body); + plaintext = await session.decrypt(pre); + } else if (type == CiphertextMessage.whisperType) { + SignalMessage signalMsg = SignalMessage.fromSerialized(body); + plaintext = await session.decryptFromSignal(signalMsg); + } else { + return null; + } + List? plainBytes = gzip.decode(Uint8List.fromList(plaintext)); + return Uint8List.fromList(plainBytes); + } catch (e) { + Logger("utils/signal").shout(e.toString()); + return null; + } +} + Future encryptMessage(Message msg, Int64 target) async { try { ConnectSignalProtocolStore signalStore = (await getSignalStore())!; diff --git a/lib/src/views/chat_item_details_view.dart b/lib/src/views/chat_item_details_view.dart index 1faa81d..21aa1d8 100644 --- a/lib/src/views/chat_item_details_view.dart +++ b/lib/src/views/chat_item_details_view.dart @@ -1,18 +1,21 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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'; -class AlignedTextBox extends StatelessWidget { - const AlignedTextBox({super.key, required this.text, required this.right}); - final String text; - final bool right; +class ChatListEntry extends StatelessWidget { + const ChatListEntry(this.message, {super.key}); + final DbMessage message; @override Widget build(BuildContext context) { - return Align( - alignment: right ? Alignment.centerRight : Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.all(10), - child: Container( + bool right = message.messageOtherId == null; + + Widget child = Container(); + + switch (message.messageKind) { + case MessageKind.textMessage: + child = Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.8, // Maximum 80% of the screen width @@ -26,64 +29,65 @@ class AlignedTextBox extends StatelessWidget { borderRadius: BorderRadius.circular(12.0), // Set border radius ), child: Text( - text, + message.messageContent!.text!, style: TextStyle( color: Colors.white, // Set text color for contrast fontSize: 17, ), textAlign: TextAlign.left, // Center the text ), - ), - ), + ); + break; + default: + return Container(); + } + + return Align( + alignment: right ? Alignment.centerRight : Alignment.centerLeft, + child: Padding(padding: EdgeInsets.all(10), child: child), ); } } /// Displays detailed information about a SampleItem. -class ChatItemDetailsView extends StatelessWidget { +class ChatItemDetailsView extends StatefulWidget { const ChatItemDetailsView({super.key, required this.user}); final Contact user; + @override + State createState() => _ChatItemDetailsViewState(); +} + +class _ChatItemDetailsViewState extends State { + List _messages = []; + + @override + void initState() { + super.initState(); + _loadAsync(); + } + + Future _loadAsync() async { + _messages = + await DbMessages.getAllMessagesForUser(widget.user.userId.toInt()); + } + @override Widget build(BuildContext context) { - List> messages = [ - ["Hallo", true], - ["Wie geht's?", false], - ["Das ist ein Test.", true], - ["Flutter ist großartig!", false], - ["Ich liebe Programmieren.", true], - ["Das Wetter ist schön.", false], - ["Hast du Pläne für heute?", true], - ["Ich mag Pizza.", false], - ["Lass uns einen Film schauen.", false], - ["Das ist interessant.", false], - ["Ich bin müde.", true], - ["Was machst du gerade?", true], - ["Ich habe ein neues Hobby.", true], - ["Das ist eine lange Nachricht.", false], - ["Ich freue mich auf das Wochenende.", true], - ["Das ist eine zufällige Nachricht.", false], - ["Ich lerne Dart.", true], - ["Wie war dein Tag?", true], - ["Ich genieße die Natur.", true], - ["Das ist ein schöner Ort.", false], - ["Meine letzte Nachricht.", false], - ]; - messages = messages.reversed.toList(); + // messages = messages.reversed.toList(); return Scaffold( appBar: AppBar( - title: Text('Your Chat with ${user.displayName}'), + title: Text('Your Chat with ${widget.user.displayName}'), ), body: Column( children: [ Expanded( child: ListView.builder( - itemCount: messages.length, // Number of items in the list + itemCount: _messages.length, // Number of items in the list reverse: true, itemBuilder: (context, i) { - return AlignedTextBox( - text: messages[i][0], right: messages[i][1]); + return ChatListEntry(_messages[i]); }, ), ), diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index 5040aad..eb8a33e 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -132,23 +132,10 @@ class UserListItem extends StatefulWidget { class _UserListItem extends State { int flames = 0; int lastMessageInSeconds = 0; - bool isDownloaded = true; @override void initState() { super.initState(); - _loadAsync(); - } - - Future _loadAsync() async { - // flames = await widget.user.getFlames(); - // setState(() {}); - - if (widget.lastMessage.containsOtherMedia()) { - isDownloaded = await isMediaDownloaded( - widget.lastMessage.messageContent!.downloadToken!); - setState(() {}); - } } @override @@ -184,8 +171,8 @@ class _UserListItem extends State { title: Text(widget.user.displayName), subtitle: Row( children: [ - MessageSendStateIcon( - state, isDownloaded, widget.lastMessage.messageKind), + MessageSendStateIcon(state, widget.lastMessage.isDownloaded, + widget.lastMessage.messageKind), Text("•"), const SizedBox(width: 5), Text( @@ -213,18 +200,17 @@ class _UserListItem extends State { ), leading: InitialsAvatar(displayName: widget.user.displayName), onTap: () { + if (!widget.lastMessage.isDownloaded) { + List token = widget.lastMessage.messageContent!.downloadToken!; + tryDownloadMedia(token, force: true); + return; + } Navigator.push( context, MaterialPageRoute(builder: (context) { if (state == MessageSendState.received && widget.lastMessage.containsOtherMedia()) { - List token = - widget.lastMessage.messageContent!.downloadToken!; - if (isDownloaded) { - return MediaViewerView(widget.user); - } else { - tryDownloadMedia(token); - } + return MediaViewerView(widget.user, widget.lastMessage); } return ChatItemDetailsView(user: widget.user); }), diff --git a/lib/src/views/media_viewer_view.dart b/lib/src/views/media_viewer_view.dart index fcfe653..121685e 100644 --- a/lib/src/views/media_viewer_view.dart +++ b/lib/src/views/media_viewer_view.dart @@ -1,19 +1,115 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/messages_model.dart'; +import 'package:twonly/src/providers/api/api.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class MediaViewerView extends StatefulWidget { final Contact otherUser; - const MediaViewerView(this.otherUser, {super.key}); + final DbMessage message; + const MediaViewerView(this.otherUser, this.message, {super.key}); @override State createState() => _MediaViewerViewState(); } class _MediaViewerViewState extends State { + Uint8List? _imageByte; + + @override + void initState() { + super.initState(); + _initAsync(); + } + + Future _initAsync() async { + List token = widget.message.messageContent!.downloadToken!; + + _imageByte = await getDownloadedMedia(token, widget.message.messageId); + print(_imageByte); + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( - body: Text(widget.otherUser.displayName), + body: Stack( + fit: StackFit.expand, + children: [ + Positioned( + top: 0, + // bottom: 0, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 50), + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: (_imageByte != null) + ? Image.memory( + _imageByte!, + fit: BoxFit.contain, + ) + : CircularProgressIndicator()), + ), + ), + _imageByte != null + ? Positioned( + left: 10, + top: 60, + child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.close, size: 30), + color: Colors.white, + onPressed: () async { + Navigator.pop(context); + }, + ), + ], + ), + ) + : Container(), + _imageByte != null + ? Positioned( + bottom: 70, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 20), + FilledButton.icon( + icon: FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () async { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => + // ShareImageView(image: widget.image)), + // ); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 30), + ), + ), + label: Text( + AppLocalizations.of(context)! + .shareImagedEditorShareWith, + style: TextStyle(fontSize: 17), + ), + ), + ], + ), + ) + : Container(), + ], + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index c7d7add..321186d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -198,6 +198,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.8" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412" + url: "https://pub.dev" + source: hosted + version: "6.1.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -238,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" device_info_plus: dependency: transitive description: @@ -730,6 +754,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" optional: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ae2592..7bf0fa9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: camerawesome: ^2.1.0 collection: ^1.18.0 + connectivity_plus: ^6.1.2 cv: ^1.1.3 fixnum: ^1.1.1 flutter: