From b1ad911e2d6d2f8273172be6034a154a4886149c Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Mon, 29 Aug 2022 20:27:48 +0930 Subject: [PATCH] WIP - Adding images for conversation & user icons Todo: Save image on server for conversations & profiles --- .../lib/components/custom_circle_avatar.dart | 117 ++++++---- mobile/lib/components/file_picker.dart | 17 +- mobile/lib/components/user_search_result.dart | 1 - mobile/lib/components/view_image.dart | 11 +- mobile/lib/models/conversations.dart | 31 ++- mobile/lib/utils/storage/database.dart | 3 +- mobile/lib/utils/storage/messages.dart | 1 - .../conversation/create_add_users_list.dart | 1 - .../lib/views/main/conversation/detail.dart | 36 ++- .../views/main/conversation/edit_details.dart | 208 +++++++++++------- mobile/lib/views/main/conversation/list.dart | 56 ++--- .../views/main/conversation/list_item.dart | 2 +- .../lib/views/main/conversation/message.dart | 153 ++++++++++--- .../lib/views/main/conversation/settings.dart | 25 ++- .../conversation/settings_user_list_item.dart | 1 - mobile/lib/views/main/friend/list_item.dart | 1 - .../views/main/friend/request_list_item.dart | 1 - mobile/lib/views/main/profile/profile.dart | 5 +- 18 files changed, 443 insertions(+), 227 deletions(-) diff --git a/mobile/lib/components/custom_circle_avatar.dart b/mobile/lib/components/custom_circle_avatar.dart index bf7d1b8..1680fec 100644 --- a/mobile/lib/components/custom_circle_avatar.dart +++ b/mobile/lib/components/custom_circle_avatar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; enum AvatarTypes { @@ -6,74 +8,107 @@ enum AvatarTypes { image, } -class CustomCircleAvatar extends StatefulWidget { +class CustomCircleAvatar extends StatelessWidget { final String? initials; final Icon? icon; - final String? imagePath; + final File? image; + final Function ()? editImageCallback; final double radius; const CustomCircleAvatar({ - Key? key, - this.initials, - this.icon, - this.imagePath, - this.radius = 20, + Key? key, + this.initials, + this.icon, + this.image, + this.editImageCallback, + this.radius = 20, }) : super(key: key); - @override - _CustomCircleAvatarState createState() => _CustomCircleAvatarState(); -} - -class _CustomCircleAvatarState extends State{ - AvatarTypes type = AvatarTypes.image; - - @override - void initState() { - super.initState(); + Widget avatar() { + AvatarTypes? type; - if (widget.imagePath != null) { - type = AvatarTypes.image; - return; - } + if (icon != null) { + type = AvatarTypes.icon; + } - if (widget.icon != null) { - type = AvatarTypes.icon; - return; - } + if (initials != null) { + type = AvatarTypes.initials; + } - if (widget.initials != null) { - type = AvatarTypes.initials; - return; - } + if (image != null) { + type = AvatarTypes.image; + } + if (type == null) { throw ArgumentError('Invalid arguments passed to CustomCircleAvatar'); - } + } - Widget avatar() { if (type == AvatarTypes.initials) { return CircleAvatar( - backgroundColor: Colors.grey[300], - child: Text(widget.initials!), - radius: widget.radius, + backgroundColor: Colors.grey[300], + child: Text(initials!), + radius: radius, ); } if (type == AvatarTypes.icon) { return CircleAvatar( - backgroundColor: Colors.grey[300], - child: widget.icon, - radius: widget.radius, + backgroundColor: Colors.grey[300], + child: icon, + radius: radius, ); } - return CircleAvatar( - backgroundImage: AssetImage(widget.imagePath!), - radius: widget.radius, + return Container( + width: radius * 2, + height: radius * 2, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: Image.file(image!).image, + fit: BoxFit.fill + ), + ), + ); + } + + Widget editIcon(BuildContext context) { + if (editImageCallback == null) { + return const SizedBox.shrink(); + } + + return SizedBox( + height: (radius * 2), + width: (radius * 2), + child: Align( + alignment: Alignment.bottomRight, + child: GestureDetector( + onTap: editImageCallback, + child: Container( + height: (radius / 2) + (radius / 7), + width: (radius / 2) + (radius / 7), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(30), + ), + child: Icon( + Icons.add, + color: Theme.of(context).primaryColor, + size: radius / 2 + ), + ), + ), + ), ); } @override Widget build(BuildContext context) { - return avatar(); + return Stack( + children: [ + avatar(), + editIcon(context), + ] + ); } } diff --git a/mobile/lib/components/file_picker.dart b/mobile/lib/components/file_picker.dart index 6c56310..7160e0d 100644 --- a/mobile/lib/components/file_picker.dart +++ b/mobile/lib/components/file_picker.dart @@ -10,9 +10,10 @@ class FilePicker extends StatelessWidget { this.fileHandle, }) : super(key: key); - final Function()? cameraHandle; - final Function()? galleryHandleSingle; + final Function(XFile image)? cameraHandle; + final Function(XFile image)? galleryHandleSingle; final Function(List images)? galleryHandleMultiple; + // TODO: Implement. Perhaps after first release? final Function()? fileHandle; final ImagePicker _picker = ImagePicker(); @@ -27,7 +28,12 @@ class FilePicker extends StatelessWidget { _filePickerSelection( hasHandle: cameraHandle != null, icon: Icons.camera_alt, - onTap: () { + onTap: () async { + final XFile? image = await _picker.pickImage(source: ImageSource.camera); + if (image == null) { + return; + } + cameraHandle!(image); }, context: context, ), @@ -36,7 +42,10 @@ class FilePicker extends StatelessWidget { icon: Icons.image, onTap: () async { final XFile? image = await _picker.pickImage(source: ImageSource.gallery); - print(image); + if (image == null) { + return; + } + galleryHandleSingle!(image); }, context: context, ), diff --git a/mobile/lib/components/user_search_result.dart b/mobile/lib/components/user_search_result.dart index c8c7b95..2885e7e 100644 --- a/mobile/lib/components/user_search_result.dart +++ b/mobile/lib/components/user_search_result.dart @@ -41,7 +41,6 @@ class _UserSearchResultState extends State{ CustomCircleAvatar( initials: widget.user.username[0].toUpperCase(), icon: const Icon(Icons.person, size: 80), - imagePath: null, radius: 50, ), const SizedBox(height: 10), diff --git a/mobile/lib/components/view_image.dart b/mobile/lib/components/view_image.dart index 648fb00..2fe3fcf 100644 --- a/mobile/lib/components/view_image.dart +++ b/mobile/lib/components/view_image.dart @@ -20,9 +20,14 @@ class ViewImage extends StatelessWidget { backgroundColor: Colors.black, ), body: Center( - child: Image.file( - message.file, - ), + child: InteractiveViewer( + panEnabled: false, + minScale: 1, + maxScale: 4, + child: Image.file( + message.file, + ), + ) ), ); } diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 552955d..f5c7134 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -1,12 +1,15 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:Envelope/models/messages.dart'; import 'package:Envelope/models/text_messages.dart'; +import 'package:mime/mime.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; +import '../utils/storage/write_file.dart'; import '/models/conversation_users.dart'; import '/models/friends.dart'; import '/models/my_profile.dart'; @@ -143,6 +146,11 @@ Future getConversationById(String id) async { throw ArgumentError('Invalid user id'); } + File? file; + if (maps[0]['file'] != null && maps[0]['file'] != '') { + file = File(maps[0]['file']); + } + return Conversation( id: maps[0]['id'], userId: maps[0]['user_id'], @@ -152,6 +160,7 @@ Future getConversationById(String id) async { twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, + icon: file, ); } @@ -165,6 +174,12 @@ Future> getConversations() async { ); return List.generate(maps.length, (i) { + + File? file; + if (maps[i]['file'] != null && maps[i]['file'] != '') { + file = File(maps[i]['file']); + } + return Conversation( id: maps[i]['id'], userId: maps[i]['user_id'], @@ -174,6 +189,7 @@ Future> getConversations() async { twoUser: maps[i]['two_user'] == 1, status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, + icon: file, ); }); } @@ -220,6 +236,7 @@ class Conversation { bool twoUser; ConversationStatus status; bool isRead; + File? icon; Conversation({ required this.id, @@ -230,6 +247,7 @@ class Conversation { required this.twoUser, required this.status, required this.isRead, + this.icon, }); @@ -298,13 +316,23 @@ class Conversation { }); } - return { + Map returnData = { 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': await getEncryptedConversationUsers(this, symKey), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), 'user_conversations': userConversations, }; + + if (icon != null) { + returnData['attachment'] = { + 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())), + 'mimetype': lookupMimeType(icon!.path), + 'extension': getExtension(icon!.path), + }; + } + + return returnData; } Map toMap() { @@ -317,6 +345,7 @@ class Conversation { 'two_user': twoUser ? 1 : 0, 'status': status.index, 'is_read': isRead ? 1 : 0, + 'file': icon != null ? icon!.path : null, }; } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 2dbf2c4..ea22067 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -39,7 +39,8 @@ Future getDatabaseConnection() async { name TEXT, two_user INTEGER, status INTEGER, - is_read INTEGER + is_read INTEGER, + file TEXT ); '''); diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index e9747ea..fd7ced7 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -54,7 +54,6 @@ Future sendMessage(Conversation conversation, { message.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); - } for (File file in files) { diff --git a/mobile/lib/views/main/conversation/create_add_users_list.dart b/mobile/lib/views/main/conversation/create_add_users_list.dart index 6c53ac4..fec37f6 100644 --- a/mobile/lib/views/main/conversation/create_add_users_list.dart +++ b/mobile/lib/views/main/conversation/create_add_users_list.dart @@ -40,7 +40,6 @@ class _ConversationAddFriendItemState extends State { children: [ CustomCircleAvatar( initials: widget.friend.username[0].toUpperCase(), - imagePath: null, ), const SizedBox(width: 16), Expanded( diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index d779392..5eabbec 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:Envelope/models/image_message.dart'; -import 'package:Envelope/models/text_messages.dart'; import 'package:Envelope/views/main/conversation/message.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -12,7 +10,6 @@ import '/models/conversations.dart'; import '/models/messages.dart'; import '/models/my_profile.dart'; import '/utils/storage/messages.dart'; -import '/utils/time.dart'; import '/views/main/conversation/settings.dart'; class ConversationDetail extends StatefulWidget{ @@ -24,7 +21,6 @@ class ConversationDetail extends StatefulWidget{ @override _ConversationDetailState createState() => _ConversationDetailState(); - } class _ConversationDetailState extends State { @@ -54,18 +50,18 @@ class _ConversationDetailState extends State { ), showBack: true, rightHandButton: IconButton( - onPressed: (){ - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationSettings( - conversation: widget.conversation - )), - ); - }, - icon: Icon( - Icons.settings, - color: Theme.of(context).appBarTheme.iconTheme?.color, - ), - ), + onPressed: (){ + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationSettings( + conversation: widget.conversation + )), + ); + }, + icon: Icon( + Icons.settings, + color: Theme.of(context).appBarTheme.iconTheme?.color, + ), + ), ), body: Stack( @@ -257,8 +253,10 @@ class _ConversationDetailState extends State { files: selectedImages, ); messages = await getMessagesForThread(widget.conversation); - setState(() {}); - msgController.text = ''; + setState(() { + msgController.text = ''; + selectedImages = []; + }); }, child: Icon( Icons.send, @@ -275,7 +273,7 @@ class _ConversationDetailState extends State { showFilePicker ? FilePicker( - cameraHandle: () {}, + cameraHandle: (XFile image) {}, galleryHandleMultiple: (List images) async { for (var img in images) { selectedImages.add(File(img.path)); diff --git a/mobile/lib/views/main/conversation/edit_details.dart b/mobile/lib/views/main/conversation/edit_details.dart index a0441b9..4d4b281 100644 --- a/mobile/lib/views/main/conversation/edit_details.dart +++ b/mobile/lib/views/main/conversation/edit_details.dart @@ -1,10 +1,14 @@ +import 'dart:io'; + +import 'package:Envelope/components/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import '/components/custom_circle_avatar.dart'; import '/models/conversations.dart'; class ConversationEditDetails extends StatefulWidget { - final Function(String conversationName) saveCallback; + final Function(String conversationName, File? conversationIcon) saveCallback; final Conversation? conversation; const ConversationEditDetails({ Key? key, @@ -22,11 +26,15 @@ class _ConversationEditDetails extends State { List conversations = []; TextEditingController conversationNameController = TextEditingController(); + File? conversationIcon; + + bool showFileSelector = false; @override void initState() { if (widget.conversation != null) { conversationNameController.text = widget.conversation!.name; + conversationIcon = widget.conversation!.icon; } super.initState(); } @@ -54,94 +62,128 @@ class _ConversationEditDetails extends State { ); return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: SafeArea( - child: Container( - padding: const EdgeInsets.only(right: 16), - child: Row( - children: [ - IconButton( - onPressed: (){ - Navigator.pop(context); - }, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 2,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.conversation != null ? - widget.conversation!.name + " Settings" : - 'Add Conversation', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600 - ), - ), - ], - ), - ), - ], - ), + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back), + ), + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.conversation != null ? + widget.conversation!.name + ' Settings' : + 'Add Conversation', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600 + ), + ), + ], + ), ), + ], + ), ), ), - body: Center( - child: Padding( - padding: const EdgeInsets.only( - top: 50, - left: 25, - right: 25, + ), + + body: Center( + child: Padding( + padding: const EdgeInsets.only( + top: 50, + left: 25, + right: 25, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + + CustomCircleAvatar( + icon: const Icon(Icons.people, size: 60), + image: conversationIcon, + radius: 50, + editImageCallback: () { + setState(() { + showFileSelector = true; + }); + }, ), - child: Form( - key: _formKey, - child: Column( - children: [ - const CustomCircleAvatar( - icon: const Icon(Icons.people, size: 60), - imagePath: null, - radius: 50, - ), - const SizedBox(height: 30), - TextFormField( - controller: conversationNameController, - textAlign: TextAlign.center, - decoration: InputDecoration( - hintText: 'Title', - enabledBorder: inputBorderStyle, - focusedBorder: inputBorderStyle, - ), - style: inputTextStyle, - // The validator receives the text that the user has entered. - validator: (value) { - if (value == null || value.isEmpty) { - return 'Add a title'; - } - return null; - }, - ), - const SizedBox(height: 30), - ElevatedButton( - style: buttonStyle, - onPressed: () { - if (!_formKey.currentState!.validate()) { - // TODO: Show error here - return; - } - - widget.saveCallback(conversationNameController.text); - }, - child: const Text('Save'), - ), - ], + + const SizedBox(height: 20), + + showFileSelector ? + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: FilePicker( + cameraHandle: (XFile image) { + setState(() { + conversationIcon = File(image.path); + showFileSelector = false; + }); + }, + galleryHandleSingle: (XFile image) async { + setState(() { + conversationIcon = File(image.path); + showFileSelector = false; + }); + }, + ), + ) : + const SizedBox(height: 10), + + TextFormField( + controller: conversationNameController, + textAlign: TextAlign.center, + decoration: InputDecoration( + hintText: 'Title', + enabledBorder: inputBorderStyle, + focusedBorder: inputBorderStyle, ), - ), + style: inputTextStyle, + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Add a title'; + } + return null; + }, + ), + + const SizedBox(height: 30), + + ElevatedButton( + style: buttonStyle, + onPressed: () { + if (!_formKey.currentState!.validate()) { + return; + } + + widget.saveCallback( + conversationNameController.text, + conversationIcon, + ); + }, + child: const Text('Save'), + ), + + ], + ), ), + ), ), ); } diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index 62be875..be4b494 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/models/friends.dart'; import 'package:Envelope/utils/storage/conversations.dart'; @@ -61,35 +63,35 @@ class _ConversationListState extends State { ), ), floatingActionButton: Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), - child: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationEditDetails( - saveCallback: (String conversationName) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationAddFriendsList( - friends: friends, - saveCallback: (List friendsSelected) async { - Conversation conversation = await createConversation( - conversationName, - friendsSelected, - false, - ); + padding: const EdgeInsets.only(right: 10, bottom: 10), + child: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationEditDetails( + saveCallback: (String conversationName, File? file) { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationAddFriendsList( + friends: friends, + saveCallback: (List friendsSelected) async { + Conversation conversation = await createConversation( + conversationName, + friendsSelected, + false, + ); - uploadConversation(conversation, context); + uploadConversation(conversation, context); - Navigator.of(context).popUntil((route) => route.isFirst); - Navigator.push(context, MaterialPageRoute(builder: (context){ - return ConversationDetail( - conversation: conversation, - ); - })); - }, - )) - ); - }, - )), + Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail( + conversation: conversation, + ); + })); + }, + )) + ); + }, + )), ).then(onGoBack); }, backgroundColor: Theme.of(context).colorScheme.primary, diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index 1cadb95..670919e 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -43,7 +43,7 @@ class _ConversationListItemState extends State { children: [ CustomCircleAvatar( initials: widget.conversation.name[0].toUpperCase(), - imagePath: null, + image: widget.conversation.icon, ), const SizedBox(width: 16), Expanded( diff --git a/mobile/lib/views/main/conversation/message.dart b/mobile/lib/views/main/conversation/message.dart index 5b4f42d..ee98aa5 100644 --- a/mobile/lib/views/main/conversation/message.dart +++ b/mobile/lib/views/main/conversation/message.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import '/models/messages.dart'; @immutable -class ConversationMessage extends StatelessWidget { +class ConversationMessage extends StatefulWidget { const ConversationMessage({ Key? key, required this.message, @@ -19,18 +19,71 @@ class ConversationMessage extends StatelessWidget { final MyProfile profile; final int index; + @override + _ConversationMessageState createState() => _ConversationMessageState(); +} + +class _ConversationMessageState extends State { + + List> menuItems = []; + + Offset? _tapPosition; + + bool showDownloadButton = false; + bool showDeleteButton = false; + + @override + void initState() { + super.initState(); + + showDownloadButton = widget.message.runtimeType == ImageMessage; + showDeleteButton = widget.message.senderId == widget.profile.id; + + if (showDownloadButton) { + menuItems.add(PopupMenuItem( + value: 'download', + child: Row( + children: const [ + Icon(Icons.download), + SizedBox( + width: 10, + ), + Text('Download') + ], + ), + )); + } + + if (showDeleteButton) { + menuItems.add(PopupMenuItem( + value: 'delete', + child: Row( + children: const [ + Icon(Icons.delete), + SizedBox( + width: 10, + ), + Text('Delete') + ], + ), + )); + } + + setState(() {}); + } + @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), child: Align( alignment: ( - message.senderUsername == profile.username ? + widget.message.senderId == widget.profile.id ? Alignment.topRight : Alignment.topLeft ), child: Column( - crossAxisAlignment: message.senderUsername == profile.username ? + crossAxisAlignment: widget.message.senderId == widget.profile.id ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ @@ -40,26 +93,26 @@ class ConversationMessage extends StatelessWidget { const SizedBox(height: 1.5), Row( - mainAxisAlignment: message.senderUsername == profile.username ? + mainAxisAlignment: widget.message.senderId == widget.profile.id ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ const SizedBox(width: 10), - usernameOrFailedToSend(index), + usernameOrFailedToSend(), ], ), const SizedBox(height: 1.5), Row( - mainAxisAlignment: message.senderUsername == profile.username ? + mainAxisAlignment: widget.message.senderId == widget.profile.id ? MainAxisAlignment.end : MainAxisAlignment.start, children: [ const SizedBox(width: 10), Text( - convertToAgo(message.createdAt), - textAlign: message.senderUsername == profile.username ? + convertToAgo(widget.message.createdAt), + textAlign: widget.message.senderId == widget.profile.id ? TextAlign.left : TextAlign.right, style: TextStyle( @@ -70,7 +123,7 @@ class ConversationMessage extends StatelessWidget { ], ), - index != 0 ? + widget.index != 0 ? const SizedBox(height: 20) : const SizedBox.shrink(), ], @@ -79,22 +132,50 @@ class ConversationMessage extends StatelessWidget { ); } + void _showCustomMenu() { + final Size overlay = MediaQuery.of(context).size; + + int addVerticalOffset = 75 * menuItems.length; + + // TODO: Implement download & delete methods + showMenu( + context: context, + items: menuItems, + position: RelativeRect.fromRect( + Offset(_tapPosition!.dx, (_tapPosition!.dy - addVerticalOffset)) & const Size(40, 40), + Offset.zero & overlay + ) + ) + .then((String? delta) async { + if (delta == null) { + return; + } + + print(delta); + }); + } + + void _storePosition(TapDownDetails details) { + _tapPosition = details.globalPosition; + } + Widget messageContent(BuildContext context) { - if (message.runtimeType == ImageMessage) { + if (widget.message.runtimeType == ImageMessage) { return GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return ViewImage( - message: (message as ImageMessage) + message: (widget.message as ImageMessage) ); })); }, + onLongPress: _showCustomMenu, + onTapDown: _storePosition, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 350, maxWidth: 250), child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Image.file( - (message as ImageMessage).file, + borderRadius: BorderRadius.circular(20), child: Image.file( + (widget.message as ImageMessage).file, fit: BoxFit.fill, ), ), @@ -102,32 +183,36 @@ class ConversationMessage extends StatelessWidget { ); } - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ( - message.senderUsername == profile.username ? - Theme.of(context).colorScheme.primary : - Theme.of(context).colorScheme.tertiary + return GestureDetector( + onLongPress: _showCustomMenu, + onTapDown: _storePosition, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: ( + widget.message.senderId == widget.profile.id ? + Theme.of(context).colorScheme.primary : + Theme.of(context).colorScheme.tertiary + ), ), - ), - padding: const EdgeInsets.all(12), - child: Text( - message.getContent(), - style: TextStyle( - fontSize: 15, - color: message.senderUsername == profile.username ? - Theme.of(context).colorScheme.onPrimary : - Theme.of(context).colorScheme.onTertiary, + padding: const EdgeInsets.all(12), + child: Text( + widget.message.getContent(), + style: TextStyle( + fontSize: 15, + color: widget.message.senderId == widget.profile.id ? + Theme.of(context).colorScheme.onPrimary : + Theme.of(context).colorScheme.onTertiary, + ), ), ), ); } - Widget usernameOrFailedToSend(int index) { - if (message.senderUsername != profile.username) { + Widget usernameOrFailedToSend() { + if (widget.message.senderId != widget.profile.id) { return Text( - message.senderUsername, + widget.message.senderUsername, style: TextStyle( fontSize: 12, color: Colors.grey[300], @@ -135,7 +220,7 @@ class ConversationMessage extends StatelessWidget { ); } - if (message.failedToSend) { + if (widget.message.failedToSend) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: const [ diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index 6eda0ba..4fd36c5 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,6 +1,9 @@ +import 'dart:io'; + import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/models/friends.dart'; import 'package:Envelope/utils/encryption/crypto_utils.dart'; +import 'package:Envelope/utils/storage/write_file.dart'; import 'package:Envelope/views/main/conversation/create_add_users.dart'; import 'package:flutter/material.dart'; @@ -75,12 +78,15 @@ class _ConversationSettingsState extends State { Widget conversationName() { return Row( children: [ - const CustomCircleAvatar( - icon: Icon(Icons.people, size: 40), - imagePath: null, // TODO: Add image here + + CustomCircleAvatar( + icon: const Icon(Icons.people, size: 40), radius: 30, + image: widget.conversation.icon, ), + const SizedBox(width: 10), + Text( widget.conversation.name, style: const TextStyle( @@ -88,6 +94,7 @@ class _ConversationSettingsState extends State { fontWeight: FontWeight.w500, ), ), + widget.conversation.admin && !widget.conversation.twoUser ? IconButton( iconSize: 20, icon: const Icon(Icons.edit), @@ -96,8 +103,18 @@ class _ConversationSettingsState extends State { onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => ConversationEditDetails( - saveCallback: (String conversationName) async { + saveCallback: (String conversationName, File? file) async { + + File? writtenFile; + if (file != null) { + writtenFile = await writeImage( + widget.conversation.id, + file.readAsBytesSync(), + ); + } + widget.conversation.name = conversationName; + widget.conversation.icon = writtenFile; final db = await getDatabaseConnection(); db.update( 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 d4add1e..446702b 100644 --- a/mobile/lib/views/main/conversation/settings_user_list_item.dart +++ b/mobile/lib/views/main/conversation/settings_user_list_item.dart @@ -104,7 +104,6 @@ class _ConversationSettingsUserListItemState extends State[ CustomCircleAvatar( initials: widget.user.username[0].toUpperCase(), - imagePath: null, radius: 15, ), const SizedBox(width: 16), diff --git a/mobile/lib/views/main/friend/list_item.dart b/mobile/lib/views/main/friend/list_item.dart index 582f296..dc411ba 100644 --- a/mobile/lib/views/main/friend/list_item.dart +++ b/mobile/lib/views/main/friend/list_item.dart @@ -33,7 +33,6 @@ class _FriendListItemState extends State { children: [ CustomCircleAvatar( initials: widget.friend.username[0].toUpperCase(), - imagePath: null, ), const SizedBox(width: 16), Expanded( diff --git a/mobile/lib/views/main/friend/request_list_item.dart b/mobile/lib/views/main/friend/request_list_item.dart index 0f2c278..4eccf46 100644 --- a/mobile/lib/views/main/friend/request_list_item.dart +++ b/mobile/lib/views/main/friend/request_list_item.dart @@ -46,7 +46,6 @@ class _FriendRequestListItemState extends State { children: [ CustomCircleAvatar( initials: widget.friend.username[0].toUpperCase(), - imagePath: null, ), const SizedBox(width: 16), Expanded( diff --git a/mobile/lib/views/main/profile/profile.dart b/mobile/lib/views/main/profile/profile.dart index ae7df99..45cfa68 100644 --- a/mobile/lib/views/main/profile/profile.dart +++ b/mobile/lib/views/main/profile/profile.dart @@ -79,7 +79,6 @@ class _ProfileState extends State { children: [ const CustomCircleAvatar( icon: Icon(Icons.person, size: 40), - imagePath: null, // TODO: Add image here radius: 30, ), const SizedBox(width: 20), @@ -259,8 +258,8 @@ class _ProfileState extends State { }); return Column( - children: [ - Padding( + children: [ + Padding( padding: const EdgeInsets.all(20), child: QrImage( backgroundColor: Theme.of(context).colorScheme.primary,