Browse Source

WIP - Working on adding attachments to messages

pull/3/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
c9581363dc
15 changed files with 725 additions and 241 deletions
  1. +1
    -0
      Backend/Database/Init.go
  2. +8
    -0
      Backend/Models/Attachments.go
  3. +6
    -0
      mobile/ios/Runner/Info.plist
  4. +97
    -0
      mobile/lib/components/file_picker.dart
  5. +3
    -2
      mobile/lib/models/conversations.dart
  6. +131
    -0
      mobile/lib/models/image_message.dart
  7. +19
    -92
      mobile/lib/models/messages.dart
  8. +131
    -0
      mobile/lib/models/text_messages.dart
  9. +18
    -4
      mobile/lib/utils/storage/messages.dart
  10. +203
    -84
      mobile/lib/views/main/conversation/detail.dart
  11. +13
    -14
      mobile/lib/views/main/conversation/list_item.dart
  12. +3
    -3
      mobile/lib/views/main/conversation/settings.dart
  13. +42
    -42
      mobile/lib/views/main/conversation/settings_user_list_item.dart
  14. +49
    -0
      mobile/pubspec.lock
  15. +1
    -0
      mobile/pubspec.yaml

+ 1
- 0
Backend/Database/Init.go View File

@ -20,6 +20,7 @@ var DB *gorm.DB
func getModels() []interface{} { func getModels() []interface{} {
return []interface{}{ return []interface{}{
&Models.Session{}, &Models.Session{},
&Models.Attachment{},
&Models.User{}, &Models.User{},
&Models.FriendRequest{}, &Models.FriendRequest{},
&Models.MessageData{}, &Models.MessageData{},


+ 8
- 0
Backend/Models/Attachments.go View File

@ -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"`
}

+ 6
- 0
mobile/ios/Runner/Info.plist View File

@ -47,5 +47,11 @@
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string> <string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Upload images for screen background</string>
<key>NSCameraUsageDescription</key>
<string>Upload image from camera for screen background</string>
<key>NSMicrophoneUsageDescription</key>
<string>Post videos to profile</string>
</dict> </dict>
</plist> </plist>

+ 97
- 0
mobile/lib/components/file_picker.dart View File

@ -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<XFile> 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<XFile>? 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,
),
),
),
);
}
}

+ 3
- 2
mobile/lib/models/conversations.dart View File

@ -2,6 +2,7 @@ import 'dart:convert';
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: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';
@ -348,11 +349,11 @@ class Conversation {
return null; return null;
} }
return Message(
return TextMessage(
id: maps[0]['id'], id: maps[0]['id'],
symmetricKey: maps[0]['symmetric_key'], symmetricKey: maps[0]['symmetric_key'],
userSymmetricKey: maps[0]['user_symmetric_key'], userSymmetricKey: maps[0]['user_symmetric_key'],
data: maps[0]['data'],
text: maps[0]['data'],
senderId: maps[0]['sender_id'], senderId: maps[0]['sender_id'],
senderUsername: maps[0]['sender_username'], senderUsername: maps[0]['sender_username'],
associationKey: maps[0]['association_key'], associationKey: maps[0]['association_key'],


+ 131
- 0
mobile/lib/models/image_message.dart View File

@ -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<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> payloadJson(Conversation conversation, String messageId) async {
final String messageDataId = (const Uuid()).v4();
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = await super.payloadJsonBase(
symmetricKey,
conversation,
messageId,
messageDataId,
);
Map<String, String> 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 <String, dynamic>{
'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
''';
}
}

+ 19
- 92
mobile/lib/models/messages.dart View File

@ -1,17 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; 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/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/storage/database.dart';
import '/utils/strings.dart';
const messageTypeReceiver = 'receiver'; const messageTypeReceiver = 'receiver';
const messageTypeSender = 'sender'; const messageTypeSender = 'sender';
@ -30,11 +29,11 @@ Future<List<Message>> getMessagesForThread(Conversation conversation) async {
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
return Message(
return TextMessage(
id: maps[i]['id'], id: maps[i]['id'],
symmetricKey: maps[i]['symmetric_key'], symmetricKey: maps[i]['symmetric_key'],
userSymmetricKey: maps[i]['user_symmetric_key'], userSymmetricKey: maps[i]['user_symmetric_key'],
data: maps[i]['data'],
text: maps[i]['data'],
senderId: maps[i]['sender_id'], senderId: maps[i]['sender_id'],
senderUsername: maps[i]['sender_username'], senderUsername: maps[i]['sender_username'],
associationKey: maps[i]['association_key'], associationKey: maps[i]['association_key'],
@ -42,24 +41,22 @@ Future<List<Message>> getMessagesForThread(Conversation conversation) async {
failedToSend: maps[i]['failed_to_send'] == 1, failedToSend: maps[i]['failed_to_send'] == 1,
); );
}); });
} }
class Message { class Message {
String id; String id;
String symmetricKey; String symmetricKey;
String userSymmetricKey; String userSymmetricKey;
String data;
String senderId; String senderId;
String senderUsername; String senderUsername;
String associationKey; String associationKey;
String createdAt; String createdAt;
bool failedToSend; bool failedToSend;
Message({ Message({
required this.id, required this.id,
required this.symmetricKey, required this.symmetricKey,
required this.userSymmetricKey, required this.userSymmetricKey,
required this.data,
required this.senderId, required this.senderId,
required this.senderUsername, required this.senderUsername,
required this.associationKey, required this.associationKey,
@ -67,42 +64,13 @@ class Message {
required this.failedToSend, required this.failedToSend,
}); });
Future<List<Map<String, String>>> payloadJsonBase(
Uint8List symmetricKey,
Conversation conversation,
String messageId,
String messageDataId,
) async {
factory Message.fromJson(Map<String, dynamic> 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<String> payloadJson(Conversation conversation, String messageId) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
if (profile.publicKey == null) { if (profile.publicKey == null) {
throw Exception('Could not get profile.publicKey'); throw Exception('Could not get profile.publicKey');
@ -110,10 +78,7 @@ class Message {
RSAPublicKey publicKey = profile.publicKey!; RSAPublicKey publicKey = profile.publicKey!;
final String messageDataId = (const Uuid()).v4();
final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32));
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = []; List<Map<String, String>> messages = [];
List<ConversationUser> conversationUsers = await getConversationUsers(conversation); List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
@ -150,48 +115,10 @@ class Message {
}); });
} }
Map<String, String> 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(<String, dynamic>{
'message_data': messageData,
'message': messages,
});
return messages;
} }
Map<String, dynamic> 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
''';
}
} }

+ 131
- 0
mobile/lib/models/text_messages.dart View File

@ -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<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> payloadJson(Conversation conversation, String messageId) async {
final String messageDataId = (const Uuid()).v4();
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = await super.payloadJsonBase(
symmetricKey,
conversation,
messageId,
messageDataId,
);
Map<String, String> 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 <String, dynamic>{
'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
''';
}
}

+ 18
- 4
mobile/lib/utils/storage/messages.dart View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:Envelope/models/text_messages.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -11,7 +13,7 @@ import '/models/my_profile.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
Future<void> sendMessage(Conversation conversation, String data) async {
Future<void> sendMessage(Conversation conversation, { String? data, List<File>? files }) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
var uuid = const Uuid(); var uuid = const Uuid();
@ -19,13 +21,25 @@ Future<void> sendMessage(Conversation conversation, String data) async {
ConversationUser currentUser = await getConversationUser(conversation, profile.id); ConversationUser currentUser = await getConversationUser(conversation, profile.id);
Message message = Message(
List<Map<String, dynamic>> 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, id: messageId,
symmetricKey: '', symmetricKey: '',
userSymmetricKey: '', userSymmetricKey: '',
senderId: currentUser.userId, senderId: currentUser.userId,
senderUsername: profile.username, senderUsername: profile.username,
data: data,
text: data!,
associationKey: currentUser.associationKey, associationKey: currentUser.associationKey,
createdAt: DateTime.now().toIso8601String(), createdAt: DateTime.now().toIso8601String(),
failedToSend: false, failedToSend: false,
@ -89,7 +103,7 @@ Future<void> updateMessageThread(Conversation conversation, {MyProfile? profile}
final db = await getDatabaseConnection(); final db = await getDatabaseConnection();
for (var i = 0; i < messageThreadJson.length; i++) { for (var i = 0; i < messageThreadJson.length; i++) {
Message message = Message.fromJson(
var message = TextMessage.fromJson(
messageThreadJson[i] as Map<String, dynamic>, messageThreadJson[i] as Map<String, dynamic>,
profile.privateKey!, profile.privateKey!,
); );


+ 203
- 84
mobile/lib/views/main/conversation/detail.dart View File

@ -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: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/conversations.dart';
import '/models/messages.dart'; import '/models/messages.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
@ -30,6 +35,9 @@ class _ConversationDetailState extends State<ConversationDetail> {
TextEditingController msgController = TextEditingController(); TextEditingController msgController = TextEditingController();
bool showFilePicker = false;
List<File> selectedImages = [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -61,80 +69,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
body: Stack( body: Stack(
children: <Widget>[ children: <Widget>[
messagesView(), 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: <Widget>[
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<ConversationDetail> {
), ),
), ),
padding: const EdgeInsets.all(12), 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), const SizedBox(height: 1.5),
Row( Row(
@ -269,4 +196,196 @@ class _ConversationDetailState extends State<ConversationDetail> {
}, },
); );
} }
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: <Widget>[
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<XFile> images) async {
for (var img in images) {
selectedImages.add(File(img.path));
}
setState(() {
showFilePicker = false;
});
},
fileHandle: () {},
) :
const SizedBox.shrink(),
],
),
),
),
);
}
} }

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

@ -59,21 +59,20 @@ class _ConversationListItemState extends State<ConversationListItem> {
style: const TextStyle(fontSize: 16) style: const TextStyle(fontSize: 16)
), ),
recentMessage != null ? recentMessage != null ?
const SizedBox(height: 2) :
const SizedBox.shrink()
,
const SizedBox(height: 2) :
const SizedBox.shrink(),
recentMessage != null ? 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(),
], ],
), ),
), ),


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

@ -143,9 +143,9 @@ class _ConversationSettingsState extends State<ConversationSettings> {
label: const Text( label: const Text(
'Leave Conversation', 'Leave Conversation',
style: TextStyle(fontSize: 16) 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, alignment: Alignment.centerLeft,
), ),
onPressed: () { onPressed: () {


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

@ -47,48 +47,48 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
} }
return PopupMenuButton<String>( return PopupMenuButton<String>(
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');
}
},
); );
} }


+ 49
- 0
mobile/pubspec.lock View File

@ -57,6 +57,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -111,6 +118,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -142,6 +156,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" 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: intl:
dependency: "direct main" dependency: "direct main"
description: description:


+ 1
- 0
mobile/pubspec.yaml View File

@ -25,6 +25,7 @@ dependencies:
qr_flutter: ^4.0.0 qr_flutter: ^4.0.0
qr_code_scanner: ^1.0.1 qr_code_scanner: ^1.0.1
sliding_up_panel: ^2.0.0+1 sliding_up_panel: ^2.0.0+1
image_picker: ^0.8.5+3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:


Loading…
Cancel
Save