From c9581363dcdf3c61d6001d04c8c3f73fc9e94218 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Fri, 26 Aug 2022 20:15:34 +0930 Subject: [PATCH] WIP - Working on adding attachments to messages --- Backend/Database/Init.go | 1 + Backend/Models/Attachments.go | 8 + mobile/ios/Runner/Info.plist | 6 + mobile/lib/components/file_picker.dart | 97 ++++++ mobile/lib/models/conversations.dart | 5 +- mobile/lib/models/image_message.dart | 131 ++++++++ mobile/lib/models/messages.dart | 111 ++----- mobile/lib/models/text_messages.dart | 131 ++++++++ mobile/lib/utils/storage/messages.dart | 22 +- .../lib/views/main/conversation/detail.dart | 287 +++++++++++++----- .../views/main/conversation/list_item.dart | 27 +- .../lib/views/main/conversation/settings.dart | 6 +- .../conversation/settings_user_list_item.dart | 84 ++--- mobile/pubspec.lock | 49 +++ mobile/pubspec.yaml | 1 + 15 files changed, 725 insertions(+), 241 deletions(-) create mode 100644 Backend/Models/Attachments.go create mode 100644 mobile/lib/components/file_picker.dart create mode 100644 mobile/lib/models/image_message.dart create mode 100644 mobile/lib/models/text_messages.dart diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go index 4481002..f4b6fb9 100644 --- a/Backend/Database/Init.go +++ b/Backend/Database/Init.go @@ -20,6 +20,7 @@ var DB *gorm.DB func getModels() []interface{} { return []interface{}{ &Models.Session{}, + &Models.Attachment{}, &Models.User{}, &Models.FriendRequest{}, &Models.MessageData{}, diff --git a/Backend/Models/Attachments.go b/Backend/Models/Attachments.go new file mode 100644 index 0000000..34304a7 --- /dev/null +++ b/Backend/Models/Attachments.go @@ -0,0 +1,8 @@ +package Models + +// Attachment holds the attachment data +type Attachment struct { + Base + FilePath string `gorm:"not null" json:"-"` + Mimetype string `gorm:"not null" json:"mimetype"` +} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index d3ba628..f2762fc 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -47,5 +47,11 @@ NSCameraUsageDescription This app needs camera access to scan QR codes + NSPhotoLibraryUsageDescription + Upload images for screen background + NSCameraUsageDescription + Upload image from camera for screen background + NSMicrophoneUsageDescription + Post videos to profile diff --git a/mobile/lib/components/file_picker.dart b/mobile/lib/components/file_picker.dart new file mode 100644 index 0000000..6c56310 --- /dev/null +++ b/mobile/lib/components/file_picker.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +class FilePicker extends StatelessWidget { + FilePicker({ + Key? key, + this.cameraHandle, + this.galleryHandleSingle, + this.galleryHandleMultiple, + this.fileHandle, + }) : super(key: key); + + final Function()? cameraHandle; + final Function()? galleryHandleSingle; + final Function(List images)? galleryHandleMultiple; + final Function()? fileHandle; + + final ImagePicker _picker = ImagePicker(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _filePickerSelection( + hasHandle: cameraHandle != null, + icon: Icons.camera_alt, + onTap: () { + }, + context: context, + ), + _filePickerSelection( + hasHandle: galleryHandleSingle != null, + icon: Icons.image, + onTap: () async { + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + print(image); + }, + context: context, + ), + _filePickerSelection( + hasHandle: galleryHandleMultiple != null, + icon: Icons.image, + onTap: () async { + final List? images = await _picker.pickMultiImage(); + if (images == null) { + return; + } + galleryHandleMultiple!(images); + }, + context: context, + ), + _filePickerSelection( + hasHandle: fileHandle != null, + icon: Icons.file_present_sharp, + onTap: () { + }, + context: context, + ), + ], + ) + ); + } + + Widget _filePickerSelection({ + required bool hasHandle, + required IconData icon, + required Function() onTap, + required BuildContext context + }) { + if (!hasHandle) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: GestureDetector( + onTap: onTap, + child: Container( + height: 75, + width: 75, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(25), + ), + child: Icon( + icon, + size: 40, + ), + ), + ), + ); + } +} + diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index e7d760d..9aa7c33 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:Envelope/models/messages.dart'; +import 'package:Envelope/models/text_messages.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; @@ -348,11 +349,11 @@ class Conversation { return null; } - return Message( + return TextMessage( id: maps[0]['id'], symmetricKey: maps[0]['symmetric_key'], userSymmetricKey: maps[0]['user_symmetric_key'], - data: maps[0]['data'], + text: maps[0]['data'], senderId: maps[0]['sender_id'], senderUsername: maps[0]['sender_username'], associationKey: maps[0]['association_key'], diff --git a/mobile/lib/models/image_message.dart b/mobile/lib/models/image_message.dart new file mode 100644 index 0000000..d430e2d --- /dev/null +++ b/mobile/lib/models/image_message.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pointycastle/pointycastle.dart'; +import 'package:uuid/uuid.dart'; + +import '/models/conversations.dart'; +import '/models/messages.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/strings.dart'; + +class ImageMessage extends Message { + String text; + + ImageMessage({ + id, + symmetricKey, + userSymmetricKey, + senderId, + senderUsername, + associationKey, + createdAt, + failedToSend, + required this.text, + }) : super( + id: id, + symmetricKey: symmetricKey, + userSymmetricKey: userSymmetricKey, + senderId: senderId, + senderUsername: senderUsername, + associationKey: associationKey, + createdAt: createdAt, + failedToSend: failedToSend, + ); + + factory ImageMessage.fromJson(Map json, RSAPrivateKey privKey) { + var userSymmetricKey = CryptoUtils.rsaDecrypt( + base64.decode(json['symmetric_key']), + privKey, + ); + + var symmetricKey = AesHelper.aesDecrypt( + userSymmetricKey, + base64.decode(json['message_data']['symmetric_key']), + ); + + var senderId = AesHelper.aesDecrypt( + base64.decode(symmetricKey), + base64.decode(json['message_data']['sender_id']), + ); + + var data = AesHelper.aesDecrypt( + base64.decode(symmetricKey), + base64.decode(json['message_data']['data']), + ); + + return ImageMessage( + id: json['id'], + symmetricKey: symmetricKey, + userSymmetricKey: base64.encode(userSymmetricKey), + senderId: senderId, + senderUsername: 'Unknown', + associationKey: json['association_key'], + createdAt: json['created_at'], + failedToSend: false, + text: data, + ); + } + + Map toMap() { + return { + 'id': id, + 'symmetric_key': symmetricKey, + 'user_symmetric_key': userSymmetricKey, + 'data': text, + 'sender_id': senderId, + 'sender_username': senderUsername, + 'association_key': associationKey, + 'created_at': createdAt, + 'failed_to_send': failedToSend ? 1 : 0, + }; + } + + Future> payloadJson(Conversation conversation, String messageId) async { + final String messageDataId = (const Uuid()).v4(); + + final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); + + List> messages = await super.payloadJsonBase( + symmetricKey, + conversation, + messageId, + messageDataId, + ); + + Map messageData = { + 'id': messageDataId, + 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(text.codeUnits)), + 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)), + 'symmetric_key': AesHelper.aesEncrypt( + userSymmetricKey, + Uint8List.fromList(base64.encode(symmetricKey).codeUnits), + ), + }; + + return { + 'message_data': messageData, + 'message': messages, + }; + } + + @override + String getContent() { + return 'Image'; + } + + @override + String toString() { + return ''' + + + id: $id + data: $text, + senderId: $senderId + senderUsername: $senderUsername + associationKey: $associationKey + createdAt: $createdAt + '''; + } +} diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index e88594f..251236f 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -1,17 +1,16 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:pointycastle/export.dart'; -import 'package:uuid/uuid.dart'; +import 'package:Envelope/models/conversation_users.dart'; +import 'package:Envelope/models/my_profile.dart'; +import 'package:Envelope/models/text_messages.dart'; +import 'package:Envelope/utils/encryption/aes_helper.dart'; +import 'package:Envelope/utils/encryption/crypto_utils.dart'; +import 'package:Envelope/utils/strings.dart'; +import 'package:pointycastle/pointycastle.dart'; -import '/models/conversation_users.dart'; import '/models/conversations.dart'; -import '/models/my_profile.dart'; -import '/models/friends.dart'; -import '/utils/encryption/aes_helper.dart'; -import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; -import '/utils/strings.dart'; const messageTypeReceiver = 'receiver'; const messageTypeSender = 'sender'; @@ -30,11 +29,11 @@ Future> getMessagesForThread(Conversation conversation) async { ); return List.generate(maps.length, (i) { - return Message( + return TextMessage( id: maps[i]['id'], symmetricKey: maps[i]['symmetric_key'], userSymmetricKey: maps[i]['user_symmetric_key'], - data: maps[i]['data'], + text: maps[i]['data'], senderId: maps[i]['sender_id'], senderUsername: maps[i]['sender_username'], associationKey: maps[i]['association_key'], @@ -42,24 +41,22 @@ Future> getMessagesForThread(Conversation conversation) async { failedToSend: maps[i]['failed_to_send'] == 1, ); }); - } class Message { String id; String symmetricKey; String userSymmetricKey; - String data; String senderId; String senderUsername; String associationKey; String createdAt; bool failedToSend; + Message({ required this.id, required this.symmetricKey, required this.userSymmetricKey, - required this.data, required this.senderId, required this.senderUsername, required this.associationKey, @@ -67,42 +64,13 @@ class Message { required this.failedToSend, }); + Future>> payloadJsonBase( + Uint8List symmetricKey, + Conversation conversation, + String messageId, + String messageDataId, + ) async { - factory Message.fromJson(Map json, RSAPrivateKey privKey) { - var userSymmetricKey = CryptoUtils.rsaDecrypt( - base64.decode(json['symmetric_key']), - privKey, - ); - - var symmetricKey = AesHelper.aesDecrypt( - userSymmetricKey, - base64.decode(json['message_data']['symmetric_key']), - ); - - var senderId = AesHelper.aesDecrypt( - base64.decode(symmetricKey), - base64.decode(json['message_data']['sender_id']), - ); - - var data = AesHelper.aesDecrypt( - base64.decode(symmetricKey), - base64.decode(json['message_data']['data']), - ); - - return Message( - id: json['id'], - symmetricKey: symmetricKey, - userSymmetricKey: base64.encode(userSymmetricKey), - data: data, - senderId: senderId, - senderUsername: 'Unknown', - associationKey: json['association_key'], - createdAt: json['created_at'], - failedToSend: false, - ); - } - - Future payloadJson(Conversation conversation, String messageId) async { MyProfile profile = await MyProfile.getProfile(); if (profile.publicKey == null) { throw Exception('Could not get profile.publicKey'); @@ -110,10 +78,7 @@ class Message { RSAPublicKey publicKey = profile.publicKey!; - final String messageDataId = (const Uuid()).v4(); - final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); - final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); List> messages = []; List conversationUsers = await getConversationUsers(conversation); @@ -150,48 +115,10 @@ class Message { }); } - Map messageData = { - 'id': messageDataId, - 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)), - 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)), - 'symmetric_key': AesHelper.aesEncrypt( - userSymmetricKey, - Uint8List.fromList(base64.encode(symmetricKey).codeUnits), - ), - }; - - return jsonEncode({ - 'message_data': messageData, - 'message': messages, - }); + return messages; } - Map toMap() { - return { - 'id': id, - 'symmetric_key': symmetricKey, - 'user_symmetric_key': userSymmetricKey, - 'data': data, - 'sender_id': senderId, - 'sender_username': senderUsername, - 'association_key': associationKey, - 'created_at': createdAt, - 'failed_to_send': failedToSend ? 1 : 0, - }; + String getContent() { + return ''; } - - @override - String toString() { - return ''' - - - id: $id - data: $data - senderId: $senderId - senderUsername: $senderUsername - associationKey: $associationKey - createdAt: $createdAt - '''; - } - } diff --git a/mobile/lib/models/text_messages.dart b/mobile/lib/models/text_messages.dart new file mode 100644 index 0000000..2dbb898 --- /dev/null +++ b/mobile/lib/models/text_messages.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pointycastle/pointycastle.dart'; +import 'package:uuid/uuid.dart'; + +import '/models/conversations.dart'; +import '/models/messages.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/strings.dart'; + +class TextMessage extends Message { + String text; + + TextMessage({ + id, + symmetricKey, + userSymmetricKey, + senderId, + senderUsername, + associationKey, + createdAt, + failedToSend, + required this.text, + }) : super( + id: id, + symmetricKey: symmetricKey, + userSymmetricKey: userSymmetricKey, + senderId: senderId, + senderUsername: senderUsername, + associationKey: associationKey, + createdAt: createdAt, + failedToSend: failedToSend, + ); + + factory TextMessage.fromJson(Map json, RSAPrivateKey privKey) { + var userSymmetricKey = CryptoUtils.rsaDecrypt( + base64.decode(json['symmetric_key']), + privKey, + ); + + var symmetricKey = AesHelper.aesDecrypt( + userSymmetricKey, + base64.decode(json['message_data']['symmetric_key']), + ); + + var senderId = AesHelper.aesDecrypt( + base64.decode(symmetricKey), + base64.decode(json['message_data']['sender_id']), + ); + + var data = AesHelper.aesDecrypt( + base64.decode(symmetricKey), + base64.decode(json['message_data']['data']), + ); + + return TextMessage( + id: json['id'], + symmetricKey: symmetricKey, + userSymmetricKey: base64.encode(userSymmetricKey), + senderId: senderId, + senderUsername: 'Unknown', + associationKey: json['association_key'], + createdAt: json['created_at'], + failedToSend: false, + text: data, + ); + } + + Map toMap() { + return { + 'id': id, + 'symmetric_key': symmetricKey, + 'user_symmetric_key': userSymmetricKey, + 'data': text, + 'sender_id': senderId, + 'sender_username': senderUsername, + 'association_key': associationKey, + 'created_at': createdAt, + 'failed_to_send': failedToSend ? 1 : 0, + }; + } + + Future> payloadJson(Conversation conversation, String messageId) async { + final String messageDataId = (const Uuid()).v4(); + + final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); + + List> messages = await super.payloadJsonBase( + symmetricKey, + conversation, + messageId, + messageDataId, + ); + + Map messageData = { + 'id': messageDataId, + 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(text.codeUnits)), + 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)), + 'symmetric_key': AesHelper.aesEncrypt( + userSymmetricKey, + Uint8List.fromList(base64.encode(symmetricKey).codeUnits), + ), + }; + + return { + 'message_data': messageData, + 'message': messages, + }; + } + + @override + String getContent() { + return text; + } + + @override + String toString() { + return ''' + + + id: $id + data: $text, + senderId: $senderId + senderUsername: $senderUsername + associationKey: $associationKey + createdAt: $createdAt + '''; + } +} diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index da715e0..a8bb5fb 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:Envelope/models/text_messages.dart'; import 'package:http/http.dart' as http; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; @@ -11,7 +13,7 @@ import '/models/my_profile.dart'; import '/utils/storage/database.dart'; import '/utils/storage/session_cookie.dart'; -Future sendMessage(Conversation conversation, String data) async { +Future sendMessage(Conversation conversation, { String? data, List? files }) async { MyProfile profile = await MyProfile.getProfile(); var uuid = const Uuid(); @@ -19,13 +21,25 @@ Future sendMessage(Conversation conversation, String data) async { ConversationUser currentUser = await getConversationUser(conversation, profile.id); - Message message = Message( + List> messagesToAdd = []; + + if (data != null) { + messagesToAdd.add({ 'text': data }); + } + + if (files != null && files.isNotEmpty) { + for (File file in files) { + messagesToAdd.add({ 'file': file }); + } + } + + var message = TextMessage( id: messageId, symmetricKey: '', userSymmetricKey: '', senderId: currentUser.userId, senderUsername: profile.username, - data: data, + text: data!, associationKey: currentUser.associationKey, createdAt: DateTime.now().toIso8601String(), failedToSend: false, @@ -89,7 +103,7 @@ Future updateMessageThread(Conversation conversation, {MyProfile? profile} final db = await getDatabaseConnection(); for (var i = 0; i < messageThreadJson.length; i++) { - Message message = Message.fromJson( + var message = TextMessage.fromJson( messageThreadJson[i] as Map, profile.privateKey!, ); diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index 572f855..be284e4 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -1,6 +1,11 @@ -import 'package:Envelope/components/custom_title_bar.dart'; +import 'dart:io'; + +import 'package:Envelope/models/text_messages.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import '/components/custom_title_bar.dart'; +import '/components/file_picker.dart'; import '/models/conversations.dart'; import '/models/messages.dart'; import '/models/my_profile.dart'; @@ -30,6 +35,9 @@ class _ConversationDetailState extends State { TextEditingController msgController = TextEditingController(); + bool showFilePicker = false; + List selectedImages = []; + @override Widget build(BuildContext context) { return Scaffold( @@ -61,80 +69,7 @@ class _ConversationDetailState extends State { body: Stack( children: [ messagesView(), - Align( - alignment: Alignment.bottomLeft, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 200.0, - ), - child: Container( - padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), - // height: 60, - width: double.infinity, - color: Theme.of(context).backgroundColor, - child: Row( - children: [ - GestureDetector( - onTap: (){ - }, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(30), - ), - child: Icon( - Icons.add, - color: Theme.of(context).colorScheme.onPrimary, - size: 20 - ), - ), - ), - const SizedBox(width: 15,), - Expanded( - child: TextField( - decoration: InputDecoration( - hintText: 'Write message...', - hintStyle: TextStyle( - color: Theme.of(context).hintColor, - ), - border: InputBorder.none, - ), - maxLines: null, - controller: msgController, - ), - ), - const SizedBox(width: 15), - Container( - width: 45, - height: 45, - child: FittedBox( - child: FloatingActionButton( - onPressed: () async { - if (msgController.text == '') { - return; - } - await sendMessage(widget.conversation, msgController.text); - messages = await getMessagesForThread(widget.conversation); - setState(() {}); - msgController.text = ''; - }, - child: Icon( - Icons.send, - color: Theme.of(context).colorScheme.onPrimary, - size: 22 - ), - backgroundColor: Theme.of(context).primaryColor, - ), - ), - ), - const SizedBox(width: 10), - ], - ), - ), - ), - ), + newMessageContent(), ], ), ); @@ -220,15 +155,7 @@ class _ConversationDetailState extends State { ), ), padding: const EdgeInsets.all(12), - child: Text( - messages[index].data, - style: TextStyle( - fontSize: 15, - color: messages[index].senderUsername == profile.username ? - Theme.of(context).colorScheme.onPrimary : - Theme.of(context).colorScheme.onTertiary, - ) - ), + child: messageContent(index), ), const SizedBox(height: 1.5), Row( @@ -269,4 +196,196 @@ class _ConversationDetailState extends State { }, ); } + + Widget messageContent(int index) { + return Text( + messages[index].getContent(), + style: TextStyle( + fontSize: 15, + color: messages[index].senderUsername == profile.username ? + Theme.of(context).colorScheme.onPrimary : + Theme.of(context).colorScheme.onTertiary, + ) + ); + } + + Widget showSelectedImages() { + if (selectedImages.isEmpty) { + return const SizedBox.shrink(); + } + + return SizedBox( + height: 80, + width: double.infinity, + child: ListView.builder( + itemCount: selectedImages.length, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.all(5), + itemBuilder: (context, i) { + + return Stack( + children: [ + Column( + children: [ + const SizedBox(height: 5), + Container( + alignment: Alignment.center, + height: 65, + width: 65, + child: Image.file( + selectedImages[i], + fit: BoxFit.fill, + ), + ), + ], + ), + + SizedBox( + height: 60, + width: 70, + child: Align( + alignment: Alignment.topRight, + child: GestureDetector( + onTap: () { + setState(() { + selectedImages.removeAt(i); + }); + }, + child: Container( + height: 20, + width: 20, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onPrimary, + borderRadius: BorderRadius.circular(30), + ), + child: Icon( + Icons.cancel, + color: Theme.of(context).primaryColor, + size: 20 + ), + ), + ), + ), + ), + + ], + ); + }, + ) + ); + } + + Widget newMessageContent() { + return Align( + alignment: Alignment.bottomLeft, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: selectedImages.isEmpty ? + 200.0 : + 270.0, + ), + child: Container( + padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), + width: double.infinity, + color: Theme.of(context).backgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + + showSelectedImages(), + + Row( + children: [ + + GestureDetector( + onTap: (){ + setState(() { + showFilePicker = !showFilePicker; + }); + }, + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(30), + ), + child: Icon( + Icons.add, + color: Theme.of(context).colorScheme.onPrimary, + size: 20 + ), + ), + ), + + const SizedBox(width: 15,), + + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: 'Write message...', + hintStyle: TextStyle( + color: Theme.of(context).hintColor, + ), + border: InputBorder.none, + ), + maxLines: null, + controller: msgController, + ), + ), + + const SizedBox(width: 15), + + SizedBox( + width: 45, + height: 45, + child: FittedBox( + child: FloatingActionButton( + onPressed: () async { + if (msgController.text == '' || selectedImages.isEmpty) { + return; + } + await sendMessage( + widget.conversation, + data: msgController.text != '' ? msgController.text : null, + files: selectedImages, + ); + messages = await getMessagesForThread(widget.conversation); + setState(() {}); + msgController.text = ''; + }, + child: Icon( + Icons.send, + color: Theme.of(context).colorScheme.onPrimary, + size: 22 + ), + backgroundColor: Theme.of(context).primaryColor, + ), + ), + ), + const SizedBox(width: 10), + ], + ), + + showFilePicker ? + FilePicker( + cameraHandle: () {}, + galleryHandleMultiple: (List images) async { + for (var img in images) { + selectedImages.add(File(img.path)); + } + setState(() { + showFilePicker = false; + }); + }, + fileHandle: () {}, + ) : + const SizedBox.shrink(), + ], + ), + ), + ), + ); + } } diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index a94e900..1cadb95 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -59,21 +59,20 @@ class _ConversationListItemState extends State { style: const TextStyle(fontSize: 16) ), recentMessage != null ? - const SizedBox(height: 2) : - const SizedBox.shrink() - , + const SizedBox(height: 2) : + const SizedBox.shrink(), recentMessage != null ? - Text( - recentMessage!.data, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold, - ), - ) : - const SizedBox.shrink(), + Text( + recentMessage!.getContent(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold, + ), + ) : + const SizedBox.shrink(), ], ), ), diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index 35939e1..6eda0ba 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -143,9 +143,9 @@ class _ConversationSettingsState extends State { label: const Text( 'Leave Conversation', style: TextStyle(fontSize: 16) -), -icon: const Icon(Icons.exit_to_app), -style: const ButtonStyle( + ), + icon: const Icon(Icons.exit_to_app), + style: const ButtonStyle( alignment: Alignment.centerLeft, ), onPressed: () { diff --git a/mobile/lib/views/main/conversation/settings_user_list_item.dart b/mobile/lib/views/main/conversation/settings_user_list_item.dart index a527e23..d4add1e 100644 --- a/mobile/lib/views/main/conversation/settings_user_list_item.dart +++ b/mobile/lib/views/main/conversation/settings_user_list_item.dart @@ -47,48 +47,48 @@ class _ConversationSettingsUserListItemState extends State( - itemBuilder: (context) => [ - PopupMenuItem( - value: 'admin', - // row with 2 children - child: Row( - children: const [ - Icon(Icons.admin_panel_settings), - SizedBox( - width: 10, - ), - Text('Promote to Admin') - ], - ), - ), - PopupMenuItem( - value: 'remove', - // row with 2 children - child: Row( - children: const [ - Icon(Icons.cancel), - SizedBox( - width: 10, - ), - Text('Remove from chat') - ], - ), - ), - ], - offset: const Offset(0, 0), - elevation: 2, - // on selected we show the dialog box - onSelected: (String value) { - // if value 1 show dialog - if (value == 'admin') { - print('admin'); - return; - // if value 2 show dialog - } - if (value == 'remove') { - print('remove'); - } - }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'admin', + // row with 2 children + child: Row( + children: const [ + Icon(Icons.admin_panel_settings), + SizedBox( + width: 10, + ), + Text('Promote to Admin') + ], + ), + ), + PopupMenuItem( + value: 'remove', + // row with 2 children + child: Row( + children: const [ + Icon(Icons.cancel), + SizedBox( + width: 10, + ), + Text('Remove from chat') + ], + ), + ), + ], + offset: const Offset(0, 0), + elevation: 2, + // on selected we show the dialog box + onSelected: (String value) { + // if value 1 show dialog + if (value == 'admin') { + print('admin'); + return; + // if value 2 show dialog + } + if (value == 'remove') { + print('remove'); + } + }, ); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 128580d..c744b58 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -57,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3+1" crypto: dependency: transitive description: @@ -111,6 +118,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" flutter_test: dependency: "direct dev" description: flutter @@ -142,6 +156,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + image_picker: + dependency: "direct main" + description: + name: image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.5+3" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.5+2" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.8" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.5+6" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" intl: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9a4d284..105e3a3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: qr_flutter: ^4.0.0 qr_code_scanner: ^1.0.1 sliding_up_panel: ^2.0.0+1 + image_picker: ^0.8.5+3 dev_dependencies: flutter_test: