Browse Source

WIP - Adding images for conversation & user icons

Todo: Save image on server for conversations & profiles
pull/3/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
b1ad911e2d
18 changed files with 443 additions and 227 deletions
  1. +76
    -41
      mobile/lib/components/custom_circle_avatar.dart
  2. +13
    -4
      mobile/lib/components/file_picker.dart
  3. +0
    -1
      mobile/lib/components/user_search_result.dart
  4. +8
    -3
      mobile/lib/components/view_image.dart
  5. +30
    -1
      mobile/lib/models/conversations.dart
  6. +2
    -1
      mobile/lib/utils/storage/database.dart
  7. +0
    -1
      mobile/lib/utils/storage/messages.dart
  8. +0
    -1
      mobile/lib/views/main/conversation/create_add_users_list.dart
  9. +17
    -19
      mobile/lib/views/main/conversation/detail.dart
  10. +125
    -83
      mobile/lib/views/main/conversation/edit_details.dart
  11. +29
    -27
      mobile/lib/views/main/conversation/list.dart
  12. +1
    -1
      mobile/lib/views/main/conversation/list_item.dart
  13. +119
    -34
      mobile/lib/views/main/conversation/message.dart
  14. +21
    -4
      mobile/lib/views/main/conversation/settings.dart
  15. +0
    -1
      mobile/lib/views/main/conversation/settings_user_list_item.dart
  16. +0
    -1
      mobile/lib/views/main/friend/list_item.dart
  17. +0
    -1
      mobile/lib/views/main/friend/request_list_item.dart
  18. +2
    -3
      mobile/lib/views/main/profile/profile.dart

+ 76
- 41
mobile/lib/components/custom_circle_avatar.dart View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum AvatarTypes { enum AvatarTypes {
@ -6,74 +8,107 @@ enum AvatarTypes {
image, image,
} }
class CustomCircleAvatar extends StatefulWidget {
class CustomCircleAvatar extends StatelessWidget {
final String? initials; final String? initials;
final Icon? icon; final Icon? icon;
final String? imagePath;
final File? image;
final Function ()? editImageCallback;
final double radius; final double radius;
const CustomCircleAvatar({ 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); }) : super(key: key);
@override
_CustomCircleAvatarState createState() => _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
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'); throw ArgumentError('Invalid arguments passed to CustomCircleAvatar');
}
}
Widget avatar() {
if (type == AvatarTypes.initials) { if (type == AvatarTypes.initials) {
return CircleAvatar( 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) { if (type == AvatarTypes.icon) {
return CircleAvatar( 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return avatar();
return Stack(
children: [
avatar(),
editIcon(context),
]
);
} }
} }

+ 13
- 4
mobile/lib/components/file_picker.dart View File

@ -10,9 +10,10 @@ class FilePicker extends StatelessWidget {
this.fileHandle, this.fileHandle,
}) : super(key: key); }) : super(key: key);
final Function()? cameraHandle;
final Function()? galleryHandleSingle;
final Function(XFile image)? cameraHandle;
final Function(XFile image)? galleryHandleSingle;
final Function(List<XFile> images)? galleryHandleMultiple; final Function(List<XFile> images)? galleryHandleMultiple;
// TODO: Implement. Perhaps after first release?
final Function()? fileHandle; final Function()? fileHandle;
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
@ -27,7 +28,12 @@ class FilePicker extends StatelessWidget {
_filePickerSelection( _filePickerSelection(
hasHandle: cameraHandle != null, hasHandle: cameraHandle != null,
icon: Icons.camera_alt, icon: Icons.camera_alt,
onTap: () {
onTap: () async {
final XFile? image = await _picker.pickImage(source: ImageSource.camera);
if (image == null) {
return;
}
cameraHandle!(image);
}, },
context: context, context: context,
), ),
@ -36,7 +42,10 @@ class FilePicker extends StatelessWidget {
icon: Icons.image, icon: Icons.image,
onTap: () async { onTap: () async {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery); final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
print(image);
if (image == null) {
return;
}
galleryHandleSingle!(image);
}, },
context: context, context: context,
), ),


+ 0
- 1
mobile/lib/components/user_search_result.dart View File

@ -41,7 +41,6 @@ class _UserSearchResultState extends State<UserSearchResult>{
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.user.username[0].toUpperCase(), initials: widget.user.username[0].toUpperCase(),
icon: const Icon(Icons.person, size: 80), icon: const Icon(Icons.person, size: 80),
imagePath: null,
radius: 50, radius: 50,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),


+ 8
- 3
mobile/lib/components/view_image.dart View File

@ -20,9 +20,14 @@ class ViewImage extends StatelessWidget {
backgroundColor: Colors.black, backgroundColor: Colors.black,
), ),
body: Center( body: Center(
child: Image.file(
message.file,
),
child: InteractiveViewer(
panEnabled: false,
minScale: 1,
maxScale: 4,
child: Image.file(
message.file,
),
)
), ),
); );
} }


+ 30
- 1
mobile/lib/models/conversations.dart View File

@ -1,12 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/models/messages.dart'; import 'package:Envelope/models/messages.dart';
import 'package:Envelope/models/text_messages.dart'; import 'package:Envelope/models/text_messages.dart';
import 'package:mime/mime.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../utils/storage/write_file.dart';
import '/models/conversation_users.dart'; import '/models/conversation_users.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
@ -143,6 +146,11 @@ Future<Conversation> getConversationById(String id) async {
throw ArgumentError('Invalid user id'); throw ArgumentError('Invalid user id');
} }
File? file;
if (maps[0]['file'] != null && maps[0]['file'] != '') {
file = File(maps[0]['file']);
}
return Conversation( return Conversation(
id: maps[0]['id'], id: maps[0]['id'],
userId: maps[0]['user_id'], userId: maps[0]['user_id'],
@ -152,6 +160,7 @@ Future<Conversation> getConversationById(String id) async {
twoUser: maps[0]['two_user'] == 1, twoUser: maps[0]['two_user'] == 1,
status: ConversationStatus.values[maps[0]['status']], status: ConversationStatus.values[maps[0]['status']],
isRead: maps[0]['is_read'] == 1, isRead: maps[0]['is_read'] == 1,
icon: file,
); );
} }
@ -165,6 +174,12 @@ Future<List<Conversation>> getConversations() async {
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
File? file;
if (maps[i]['file'] != null && maps[i]['file'] != '') {
file = File(maps[i]['file']);
}
return Conversation( return Conversation(
id: maps[i]['id'], id: maps[i]['id'],
userId: maps[i]['user_id'], userId: maps[i]['user_id'],
@ -174,6 +189,7 @@ Future<List<Conversation>> getConversations() async {
twoUser: maps[i]['two_user'] == 1, twoUser: maps[i]['two_user'] == 1,
status: ConversationStatus.values[maps[i]['status']], status: ConversationStatus.values[maps[i]['status']],
isRead: maps[i]['is_read'] == 1, isRead: maps[i]['is_read'] == 1,
icon: file,
); );
}); });
} }
@ -220,6 +236,7 @@ class Conversation {
bool twoUser; bool twoUser;
ConversationStatus status; ConversationStatus status;
bool isRead; bool isRead;
File? icon;
Conversation({ Conversation({
required this.id, required this.id,
@ -230,6 +247,7 @@ class Conversation {
required this.twoUser, required this.twoUser,
required this.status, required this.status,
required this.isRead, required this.isRead,
this.icon,
}); });
@ -298,13 +316,23 @@ class Conversation {
}); });
} }
return {
Map<String, dynamic> returnData = {
'id': id, 'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': await getEncryptedConversationUsers(this, symKey), 'users': await getEncryptedConversationUsers(this, symKey),
'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)),
'user_conversations': userConversations, '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<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -317,6 +345,7 @@ class Conversation {
'two_user': twoUser ? 1 : 0, 'two_user': twoUser ? 1 : 0,
'status': status.index, 'status': status.index,
'is_read': isRead ? 1 : 0, 'is_read': isRead ? 1 : 0,
'file': icon != null ? icon!.path : null,
}; };
} }


+ 2
- 1
mobile/lib/utils/storage/database.dart View File

@ -39,7 +39,8 @@ Future<Database> getDatabaseConnection() async {
name TEXT, name TEXT,
two_user INTEGER, two_user INTEGER,
status INTEGER, status INTEGER,
is_read INTEGER
is_read INTEGER,
file TEXT
); );
'''); ''');


+ 0
- 1
mobile/lib/utils/storage/messages.dart View File

@ -54,7 +54,6 @@ Future<void> sendMessage(Conversation conversation, {
message.toMap(), message.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }
for (File file in files) { for (File file in files) {


+ 0
- 1
mobile/lib/views/main/conversation/create_add_users_list.dart View File

@ -40,7 +40,6 @@ class _ConversationAddFriendItemState extends State<ConversationAddFriendItem> {
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.friend.username[0].toUpperCase(), initials: widget.friend.username[0].toUpperCase(),
imagePath: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(


+ 17
- 19
mobile/lib/views/main/conversation/detail.dart View File

@ -1,7 +1,5 @@
import 'dart:io'; 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:Envelope/views/main/conversation/message.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -12,7 +10,6 @@ import '/models/conversations.dart';
import '/models/messages.dart'; import '/models/messages.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
import '/utils/storage/messages.dart'; import '/utils/storage/messages.dart';
import '/utils/time.dart';
import '/views/main/conversation/settings.dart'; import '/views/main/conversation/settings.dart';
class ConversationDetail extends StatefulWidget{ class ConversationDetail extends StatefulWidget{
@ -24,7 +21,6 @@ class ConversationDetail extends StatefulWidget{
@override @override
_ConversationDetailState createState() => _ConversationDetailState(); _ConversationDetailState createState() => _ConversationDetailState();
} }
class _ConversationDetailState extends State<ConversationDetail> { class _ConversationDetailState extends State<ConversationDetail> {
@ -54,18 +50,18 @@ class _ConversationDetailState extends State<ConversationDetail> {
), ),
showBack: true, showBack: true,
rightHandButton: IconButton( 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( body: Stack(
@ -257,8 +253,10 @@ class _ConversationDetailState extends State<ConversationDetail> {
files: selectedImages, files: selectedImages,
); );
messages = await getMessagesForThread(widget.conversation); messages = await getMessagesForThread(widget.conversation);
setState(() {});
msgController.text = '';
setState(() {
msgController.text = '';
selectedImages = [];
});
}, },
child: Icon( child: Icon(
Icons.send, Icons.send,
@ -275,7 +273,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
showFilePicker ? showFilePicker ?
FilePicker( FilePicker(
cameraHandle: () {},
cameraHandle: (XFile image) {},
galleryHandleMultiple: (List<XFile> images) async { galleryHandleMultiple: (List<XFile> images) async {
for (var img in images) { for (var img in images) {
selectedImages.add(File(img.path)); selectedImages.add(File(img.path));


+ 125
- 83
mobile/lib/views/main/conversation/edit_details.dart View File

@ -1,10 +1,14 @@
import 'dart:io';
import 'package:Envelope/components/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '/components/custom_circle_avatar.dart'; import '/components/custom_circle_avatar.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
class ConversationEditDetails extends StatefulWidget { class ConversationEditDetails extends StatefulWidget {
final Function(String conversationName) saveCallback;
final Function(String conversationName, File? conversationIcon) saveCallback;
final Conversation? conversation; final Conversation? conversation;
const ConversationEditDetails({ const ConversationEditDetails({
Key? key, Key? key,
@ -22,11 +26,15 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
List<Conversation> conversations = []; List<Conversation> conversations = [];
TextEditingController conversationNameController = TextEditingController(); TextEditingController conversationNameController = TextEditingController();
File? conversationIcon;
bool showFileSelector = false;
@override @override
void initState() { void initState() {
if (widget.conversation != null) { if (widget.conversation != null) {
conversationNameController.text = widget.conversation!.name; conversationNameController.text = widget.conversation!.name;
conversationIcon = widget.conversation!.icon;
} }
super.initState(); super.initState();
} }
@ -54,94 +62,128 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
); );
return Scaffold( return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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'),
),
],
),
), ),
),
), ),
); );
} }


+ 29
- 27
mobile/lib/views/main/conversation/list.dart View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/components/custom_title_bar.dart';
import 'package:Envelope/models/friends.dart'; import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/storage/conversations.dart'; import 'package:Envelope/utils/storage/conversations.dart';
@ -61,35 +63,35 @@ class _ConversationListState extends State<ConversationList> {
), ),
), ),
floatingActionButton: Padding( 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<Friend> 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<Friend> 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); ).then(onGoBack);
}, },
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,


+ 1
- 1
mobile/lib/views/main/conversation/list_item.dart View File

@ -43,7 +43,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.conversation.name[0].toUpperCase(), initials: widget.conversation.name[0].toUpperCase(),
imagePath: null,
image: widget.conversation.icon,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(


+ 119
- 34
mobile/lib/views/main/conversation/message.dart View File

@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
import '/models/messages.dart'; import '/models/messages.dart';
@immutable @immutable
class ConversationMessage extends StatelessWidget {
class ConversationMessage extends StatefulWidget {
const ConversationMessage({ const ConversationMessage({
Key? key, Key? key,
required this.message, required this.message,
@ -19,18 +19,71 @@ class ConversationMessage extends StatelessWidget {
final MyProfile profile; final MyProfile profile;
final int index; final int index;
@override
_ConversationMessageState createState() => _ConversationMessageState();
}
class _ConversationMessageState extends State<ConversationMessage> {
List<PopupMenuEntry<String>> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0),
child: Align( child: Align(
alignment: ( alignment: (
message.senderUsername == profile.username ?
widget.message.senderId == widget.profile.id ?
Alignment.topRight : Alignment.topRight :
Alignment.topLeft Alignment.topLeft
), ),
child: Column( child: Column(
crossAxisAlignment: message.senderUsername == profile.username ?
crossAxisAlignment: widget.message.senderId == widget.profile.id ?
CrossAxisAlignment.end : CrossAxisAlignment.end :
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -40,26 +93,26 @@ class ConversationMessage extends StatelessWidget {
const SizedBox(height: 1.5), const SizedBox(height: 1.5),
Row( Row(
mainAxisAlignment: message.senderUsername == profile.username ?
mainAxisAlignment: widget.message.senderId == widget.profile.id ?
MainAxisAlignment.end : MainAxisAlignment.end :
MainAxisAlignment.start, MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const SizedBox(width: 10), const SizedBox(width: 10),
usernameOrFailedToSend(index),
usernameOrFailedToSend(),
], ],
), ),
const SizedBox(height: 1.5), const SizedBox(height: 1.5),
Row( Row(
mainAxisAlignment: message.senderUsername == profile.username ?
mainAxisAlignment: widget.message.senderId == widget.profile.id ?
MainAxisAlignment.end : MainAxisAlignment.end :
MainAxisAlignment.start, MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
convertToAgo(message.createdAt),
textAlign: message.senderUsername == profile.username ?
convertToAgo(widget.message.createdAt),
textAlign: widget.message.senderId == widget.profile.id ?
TextAlign.left : TextAlign.left :
TextAlign.right, TextAlign.right,
style: TextStyle( style: TextStyle(
@ -70,7 +123,7 @@ class ConversationMessage extends StatelessWidget {
], ],
), ),
index != 0 ?
widget.index != 0 ?
const SizedBox(height: 20) : const SizedBox(height: 20) :
const SizedBox.shrink(), 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<void>((String? delta) async {
if (delta == null) {
return;
}
print(delta);
});
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
Widget messageContent(BuildContext context) { Widget messageContent(BuildContext context) {
if (message.runtimeType == ImageMessage) {
if (widget.message.runtimeType == ImageMessage) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) { Navigator.push(context, MaterialPageRoute(builder: (context) {
return ViewImage( return ViewImage(
message: (message as ImageMessage)
message: (widget.message as ImageMessage)
); );
})); }));
}, },
onLongPress: _showCustomMenu,
onTapDown: _storePosition,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350, maxWidth: 250), constraints: const BoxConstraints(maxHeight: 350, maxWidth: 250),
child: ClipRRect( 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, 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( return Text(
message.senderUsername,
widget.message.senderUsername,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[300], color: Colors.grey[300],
@ -135,7 +220,7 @@ class ConversationMessage extends StatelessWidget {
); );
} }
if (message.failedToSend) {
if (widget.message.failedToSend) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: const <Widget>[ children: const <Widget>[


+ 21
- 4
mobile/lib/views/main/conversation/settings.dart View File

@ -1,6 +1,9 @@
import 'dart:io';
import 'package:Envelope/components/custom_title_bar.dart'; import 'package:Envelope/components/custom_title_bar.dart';
import 'package:Envelope/models/friends.dart'; import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/encryption/crypto_utils.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:Envelope/views/main/conversation/create_add_users.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -75,12 +78,15 @@ class _ConversationSettingsState extends State<ConversationSettings> {
Widget conversationName() { Widget conversationName() {
return Row( return Row(
children: <Widget> [ children: <Widget> [
const CustomCircleAvatar(
icon: Icon(Icons.people, size: 40),
imagePath: null, // TODO: Add image here
CustomCircleAvatar(
icon: const Icon(Icons.people, size: 40),
radius: 30, radius: 30,
image: widget.conversation.icon,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
widget.conversation.name, widget.conversation.name,
style: const TextStyle( style: const TextStyle(
@ -88,6 +94,7 @@ class _ConversationSettingsState extends State<ConversationSettings> {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
widget.conversation.admin && !widget.conversation.twoUser ? IconButton( widget.conversation.admin && !widget.conversation.twoUser ? IconButton(
iconSize: 20, iconSize: 20,
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
@ -96,8 +103,18 @@ class _ConversationSettingsState extends State<ConversationSettings> {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails( 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.name = conversationName;
widget.conversation.icon = writtenFile;
final db = await getDatabaseConnection(); final db = await getDatabaseConnection();
db.update( db.update(


+ 0
- 1
mobile/lib/views/main/conversation/settings_user_list_item.dart View File

@ -104,7 +104,6 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.user.username[0].toUpperCase(), initials: widget.user.username[0].toUpperCase(),
imagePath: null,
radius: 15, radius: 15,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),


+ 0
- 1
mobile/lib/views/main/friend/list_item.dart View File

@ -33,7 +33,6 @@ class _FriendListItemState extends State<FriendListItem> {
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.friend.username[0].toUpperCase(), initials: widget.friend.username[0].toUpperCase(),
imagePath: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(


+ 0
- 1
mobile/lib/views/main/friend/request_list_item.dart View File

@ -46,7 +46,6 @@ class _FriendRequestListItemState extends State<FriendRequestListItem> {
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.friend.username[0].toUpperCase(), initials: widget.friend.username[0].toUpperCase(),
imagePath: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(


+ 2
- 3
mobile/lib/views/main/profile/profile.dart View File

@ -79,7 +79,6 @@ class _ProfileState extends State<Profile> {
children: <Widget> [ children: <Widget> [
const CustomCircleAvatar( const CustomCircleAvatar(
icon: Icon(Icons.person, size: 40), icon: Icon(Icons.person, size: 40),
imagePath: null, // TODO: Add image here
radius: 30, radius: 30,
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
@ -259,8 +258,8 @@ class _ProfileState extends State<Profile> {
}); });
return Column( return Column(
children: [
Padding(
children: [
Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: QrImage( child: QrImage(
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,


Loading…
Cancel
Save