@ -1,101 +1,163 @@ | |||||
import 'dart:convert'; | import 'dart:convert'; | ||||
import 'dart:typed_data'; | |||||
import 'package:Envelope/models/conversation_users.dart'; | |||||
import 'package:Envelope/models/friends.dart'; | |||||
import 'package:Envelope/models/my_profile.dart'; | |||||
import 'package:pointycastle/export.dart'; | import 'package:pointycastle/export.dart'; | ||||
import 'package:sqflite/sqflite.dart'; | |||||
import 'package:uuid/uuid.dart'; | |||||
import '/utils/encryption/crypto_utils.dart'; | import '/utils/encryption/crypto_utils.dart'; | ||||
import '/utils/encryption/aes_helper.dart'; | import '/utils/encryption/aes_helper.dart'; | ||||
import '/utils/storage/database.dart'; | import '/utils/storage/database.dart'; | ||||
import '/utils/strings.dart'; | |||||
Conversation findConversationByDetailId(List<Conversation> conversations, String id) { | Conversation findConversationByDetailId(List<Conversation> conversations, String id) { | ||||
for (var conversation in conversations) { | |||||
if (conversation.conversationDetailId == id) { | |||||
return conversation; | |||||
} | |||||
for (var conversation in conversations) { | |||||
if (conversation.conversationDetailId == id) { | |||||
return conversation; | |||||
} | } | ||||
} | |||||
// Or return `null`. | // Or return `null`. | ||||
throw ArgumentError.value(id, "id", "No element with that id"); | throw ArgumentError.value(id, "id", "No element with that id"); | ||||
} | } | ||||
class Conversation { | class Conversation { | ||||
String id; | |||||
String userId; | |||||
String conversationDetailId; | |||||
String symmetricKey; | |||||
bool admin; | |||||
String name; | |||||
Conversation({ | |||||
required this.id, | |||||
required this.userId, | |||||
required this.conversationDetailId, | |||||
required this.symmetricKey, | |||||
required this.admin, | |||||
required this.name, | |||||
}); | |||||
factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { | |||||
var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( | |||||
base64.decode(json['symmetric_key']), | |||||
privKey, | |||||
); | |||||
var detailId = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['conversation_detail_id']), | |||||
); | |||||
var admin = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['admin']), | |||||
); | |||||
return Conversation( | |||||
id: json['id'], | |||||
userId: json['user_id'], | |||||
conversationDetailId: detailId, | |||||
symmetricKey: base64.encode(symmetricKeyDecrypted), | |||||
admin: admin == 'true', | |||||
name: 'Unknown', | |||||
); | |||||
} | |||||
@override | |||||
String toString() { | |||||
return ''' | |||||
id: $id | |||||
userId: $userId | |||||
name: $name | |||||
admin: $admin'''; | |||||
} | |||||
Map<String, dynamic> toMap() { | |||||
return { | |||||
'id': id, | |||||
'user_id': userId, | |||||
'conversation_detail_id': conversationDetailId, | |||||
'symmetric_key': symmetricKey, | |||||
'admin': admin ? 1 : 0, | |||||
'name': name, | |||||
}; | |||||
} | |||||
String id; | |||||
String userId; | |||||
String conversationDetailId; | |||||
String symmetricKey; | |||||
bool admin; | |||||
String name; | |||||
Conversation({ | |||||
required this.id, | |||||
required this.userId, | |||||
required this.conversationDetailId, | |||||
required this.symmetricKey, | |||||
required this.admin, | |||||
required this.name, | |||||
}); | |||||
factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { | |||||
var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( | |||||
base64.decode(json['symmetric_key']), | |||||
privKey, | |||||
); | |||||
var detailId = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['conversation_detail_id']), | |||||
); | |||||
var admin = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['admin']), | |||||
); | |||||
return Conversation( | |||||
id: json['id'], | |||||
userId: json['user_id'], | |||||
conversationDetailId: detailId, | |||||
symmetricKey: base64.encode(symmetricKeyDecrypted), | |||||
admin: admin == 'true', | |||||
name: 'Unknown', | |||||
); | |||||
} | |||||
@override | |||||
String toString() { | |||||
return ''' | |||||
id: $id | |||||
userId: $userId | |||||
name: $name | |||||
admin: $admin'''; | |||||
} | |||||
Map<String, dynamic> toMap() { | |||||
return { | |||||
'id': id, | |||||
'user_id': userId, | |||||
'conversation_detail_id': conversationDetailId, | |||||
'symmetric_key': symmetricKey, | |||||
'admin': admin ? 1 : 0, | |||||
'name': name, | |||||
}; | |||||
} | |||||
} | } | ||||
Future<Conversation> createConversation(String title, List<Friend> friends) async { | |||||
final db = await getDatabaseConnection(); | |||||
MyProfile profile = await MyProfile.getProfile(); | |||||
var uuid = const Uuid(); | |||||
final String conversationId = uuid.v4(); | |||||
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); | |||||
String associationKey = generateRandomString(32); | |||||
Conversation conversation = Conversation( | |||||
id: conversationId, | |||||
userId: profile.id, | |||||
conversationDetailId: '', | |||||
symmetricKey: base64.encode(symmetricKey), | |||||
admin: true, | |||||
name: title, | |||||
); | |||||
await db.insert( | |||||
'conversations', | |||||
conversation.toMap(), | |||||
conflictAlgorithm: ConflictAlgorithm.replace, | |||||
); | |||||
await db.insert( | |||||
'conversation_users', | |||||
ConversationUser( | |||||
id: uuid.v4(), | |||||
conversationId: conversationId, | |||||
username: profile.username, | |||||
associationKey: associationKey, | |||||
admin: false, | |||||
).toMap(), | |||||
conflictAlgorithm: ConflictAlgorithm.replace, | |||||
); | |||||
for (Friend friend in friends) { | |||||
await db.insert( | |||||
'conversation_users', | |||||
ConversationUser( | |||||
id: uuid.v4(), | |||||
conversationId: conversationId, | |||||
username: friend.username, | |||||
associationKey: associationKey, | |||||
admin: false, | |||||
).toMap(), | |||||
conflictAlgorithm: ConflictAlgorithm.replace, | |||||
); | |||||
} | |||||
return conversation; | |||||
} | |||||
// A method that retrieves all the dogs from the dogs table. | // A method that retrieves all the dogs from the dogs table. | ||||
Future<List<Conversation>> getConversations() async { | Future<List<Conversation>> getConversations() async { | ||||
final db = await getDatabaseConnection(); | |||||
final List<Map<String, dynamic>> maps = await db.query('conversations'); | |||||
return List.generate(maps.length, (i) { | |||||
return Conversation( | |||||
id: maps[i]['id'], | |||||
userId: maps[i]['user_id'], | |||||
conversationDetailId: maps[i]['conversation_detail_id'], | |||||
symmetricKey: maps[i]['symmetric_key'], | |||||
admin: maps[i]['admin'] == 1, | |||||
name: maps[i]['name'], | |||||
); | |||||
}); | |||||
final db = await getDatabaseConnection(); | |||||
final List<Map<String, dynamic>> maps = await db.query('conversations'); | |||||
return List.generate(maps.length, (i) { | |||||
return Conversation( | |||||
id: maps[i]['id'], | |||||
userId: maps[i]['user_id'], | |||||
conversationDetailId: maps[i]['conversation_detail_id'], | |||||
symmetricKey: maps[i]['symmetric_key'], | |||||
admin: maps[i]['admin'] == 1, | |||||
name: maps[i]['name'], | |||||
); | |||||
}); | |||||
} | } |
@ -0,0 +1,170 @@ | |||||
import 'package:Envelope/models/conversations.dart'; | |||||
import 'package:Envelope/views/main/conversation_create_add_users_list.dart'; | |||||
import 'package:Envelope/views/main/conversation_detail.dart'; | |||||
import 'package:flutter/material.dart'; | |||||
import '/models/friends.dart'; | |||||
import '/views/main/friend_list_item.dart'; | |||||
class ConversationAddFriendsList extends StatefulWidget { | |||||
final List<Friend> friends; | |||||
final String title; | |||||
const ConversationAddFriendsList({ | |||||
Key? key, | |||||
required this.friends, | |||||
required this.title, | |||||
}) : super(key: key); | |||||
@override | |||||
State<ConversationAddFriendsList> createState() => _ConversationAddFriendsListState(); | |||||
} | |||||
class _ConversationAddFriendsListState extends State<ConversationAddFriendsList> { | |||||
List<Friend> friends = []; | |||||
List<Friend> friendsSelected = []; | |||||
@override | |||||
void initState() { | |||||
super.initState(); | |||||
friends.addAll(widget.friends); | |||||
setState(() {}); | |||||
} | |||||
void filterSearchResults(String query) { | |||||
List<Friend> dummySearchList = []; | |||||
dummySearchList.addAll(widget.friends); | |||||
if(query.isNotEmpty) { | |||||
List<Friend> dummyListData = []; | |||||
for (Friend friend in dummySearchList) { | |||||
if (friend.username.toLowerCase().contains(query)) { | |||||
dummyListData.add(friend); | |||||
} | |||||
} | |||||
setState(() { | |||||
friends.clear(); | |||||
friends.addAll(dummyListData); | |||||
}); | |||||
return; | |||||
} | |||||
setState(() { | |||||
friends.clear(); | |||||
friends.addAll(widget.friends); | |||||
}); | |||||
} | |||||
Widget list() { | |||||
if (friends.isEmpty) { | |||||
return const Center( | |||||
child: Text('No Friends'), | |||||
); | |||||
} | |||||
return ListView.builder( | |||||
itemCount: friends.length, | |||||
shrinkWrap: true, | |||||
padding: const EdgeInsets.only(top: 16), | |||||
physics: const BouncingScrollPhysics(), | |||||
itemBuilder: (context, i) { | |||||
return ConversationAddFriendItem( | |||||
friend: friends[i], | |||||
isSelected: (bool value) { | |||||
setState(() { | |||||
widget.friends[i].selected = value; | |||||
if (value) { | |||||
friendsSelected.add(friends[i]); | |||||
return; | |||||
} | |||||
friendsSelected.remove(friends[i]); | |||||
}); | |||||
} | |||||
); | |||||
}, | |||||
); | |||||
} | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
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( | |||||
friendsSelected.isEmpty ? | |||||
'Select Friends' : | |||||
'${friendsSelected.length} Friends Selected', | |||||
style: const TextStyle( | |||||
fontSize: 16, | |||||
fontWeight: FontWeight.w600 | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
body: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Padding( | |||||
padding: const EdgeInsets.only(top: 20,left: 16,right: 16), | |||||
child: TextField( | |||||
decoration: const InputDecoration( | |||||
hintText: "Search...", | |||||
prefixIcon: Icon( | |||||
Icons.search, | |||||
size: 20 | |||||
), | |||||
), | |||||
onChanged: (value) => filterSearchResults(value.toLowerCase()) | |||||
), | |||||
), | |||||
Padding( | |||||
padding: const EdgeInsets.only(top: 0,left: 16,right: 16), | |||||
child: list(), | |||||
), | |||||
], | |||||
), | |||||
floatingActionButton: Padding( | |||||
padding: const EdgeInsets.only(right: 10, bottom: 10), | |||||
child: FloatingActionButton( | |||||
onPressed: () async { | |||||
Conversation conversation = await createConversation(widget.title, friendsSelected); | |||||
friendsSelected = []; | |||||
Navigator.of(context).popUntil((route) => route.isFirst); | |||||
Navigator.push(context, MaterialPageRoute(builder: (context){ | |||||
return ConversationDetail( | |||||
conversation: conversation, | |||||
); | |||||
})); | |||||
}, | |||||
backgroundColor: Theme.of(context).colorScheme.primary, | |||||
child: friendsSelected.isEmpty ? | |||||
const Text('Skip') : | |||||
const Icon(Icons.add, size: 30), | |||||
), | |||||
), | |||||
); | |||||
} | |||||
} |
@ -0,0 +1,96 @@ | |||||
import 'package:Envelope/components/custom_circle_avatar.dart'; | |||||
import 'package:Envelope/models/friends.dart'; | |||||
import 'package:flutter/material.dart'; | |||||
class ConversationAddFriendItem extends StatefulWidget{ | |||||
final Friend friend; | |||||
final ValueChanged<bool> isSelected; | |||||
const ConversationAddFriendItem({ | |||||
Key? key, | |||||
required this.friend, | |||||
required this.isSelected, | |||||
}) : super(key: key); | |||||
@override | |||||
_ConversationAddFriendItemState createState() => _ConversationAddFriendItemState(); | |||||
} | |||||
class _ConversationAddFriendItemState extends State<ConversationAddFriendItem> { | |||||
@override | |||||
void initState() { | |||||
super.initState(); | |||||
} | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
return GestureDetector( | |||||
behavior: HitTestBehavior.opaque, | |||||
onTap: () async { | |||||
setState(() { | |||||
widget.friend.selected = !(widget.friend.selected ?? false); | |||||
widget.isSelected(widget.friend.selected ?? false); | |||||
}); | |||||
}, | |||||
child: Container( | |||||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
child: Row( | |||||
children: <Widget>[ | |||||
Expanded( | |||||
child: Row( | |||||
children: <Widget>[ | |||||
CustomCircleAvatar( | |||||
initials: widget.friend.username[0].toUpperCase(), | |||||
imagePath: null, | |||||
), | |||||
const SizedBox(width: 16), | |||||
Expanded( | |||||
child: Align( | |||||
alignment: Alignment.centerLeft, | |||||
child: Container( | |||||
color: Colors.transparent, | |||||
child: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Text( | |||||
widget.friend.username, | |||||
style: const TextStyle( | |||||
fontSize: 16 | |||||
) | |||||
), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
(widget.friend.selected ?? false) | |||||
? Align( | |||||
alignment: Alignment.centerRight, | |||||
child: Icon( | |||||
Icons.check_circle, | |||||
color: Theme.of(context).colorScheme.primary, | |||||
size: 36, | |||||
), | |||||
) | |||||
: Padding( | |||||
padding: const EdgeInsets.only(right: 3), | |||||
child: Container( | |||||
width: 30, | |||||
height: 30, | |||||
decoration: BoxDecoration( | |||||
border: Border.all( | |||||
color: Theme.of(context).colorScheme.primary, | |||||
width: 2, | |||||
), | |||||
borderRadius: const BorderRadius.all(Radius.circular(100)) | |||||
), | |||||
) | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
); | |||||
} | |||||
} |
@ -0,0 +1,146 @@ | |||||
import 'package:flutter/material.dart'; | |||||
import '/components/custom_circle_avatar.dart'; | |||||
import '/views/main/conversation_create_add_users.dart'; | |||||
import '/models/friends.dart'; | |||||
import '/models/conversations.dart'; | |||||
class ConversationEditDetails extends StatefulWidget { | |||||
final Conversation? conversation; | |||||
final List<Friend>? friends; | |||||
const ConversationEditDetails({ | |||||
Key? key, | |||||
this.conversation, | |||||
this.friends, | |||||
}) : super(key: key); | |||||
@override | |||||
State<ConversationEditDetails> createState() => _ConversationEditDetails(); | |||||
} | |||||
class _ConversationEditDetails extends State<ConversationEditDetails> { | |||||
final _formKey = GlobalKey<FormState>(); | |||||
List<Conversation> conversations = []; | |||||
TextEditingController conversationNameController = TextEditingController(); | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
const TextStyle inputTextStyle = TextStyle( | |||||
fontSize: 25, | |||||
); | |||||
final OutlineInputBorder inputBorderStyle = OutlineInputBorder( | |||||
borderRadius: BorderRadius.circular(5), | |||||
borderSide: const BorderSide( | |||||
color: Colors.transparent, | |||||
) | |||||
); | |||||
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( | |||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), | |||||
textStyle: TextStyle( | |||||
fontSize: 15, | |||||
fontWeight: FontWeight.bold, | |||||
color: Theme.of(context).colorScheme.error, | |||||
), | |||||
); | |||||
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 | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
body: Center( | |||||
child: Padding( | |||||
padding: const EdgeInsets.only( | |||||
top: 50, | |||||
left: 25, | |||||
right: 25, | |||||
), | |||||
child: Form( | |||||
key: _formKey, | |||||
child: Column( | |||||
children: [ | |||||
CustomCircleAvatar( | |||||
icon: widget.conversation != null ? | |||||
null : // TODO: Add icon here | |||||
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()) { | |||||
Navigator.of(context).push( | |||||
MaterialPageRoute(builder: (context) => ConversationAddFriendsList( | |||||
friends: widget.friends!, | |||||
title: conversationNameController.text, | |||||
) | |||||
) | |||||
); | |||||
} | |||||
}, | |||||
child: const Text('Save'), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
); | |||||
} | |||||
} |