@ -1,101 +1,163 @@ | |||
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:sqflite/sqflite.dart'; | |||
import 'package:uuid/uuid.dart'; | |||
import '/utils/encryption/crypto_utils.dart'; | |||
import '/utils/encryption/aes_helper.dart'; | |||
import '/utils/storage/database.dart'; | |||
import '/utils/strings.dart'; | |||
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`. | |||
throw ArgumentError.value(id, "id", "No element with that id"); | |||
} | |||
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. | |||
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'), | |||
), | |||
], | |||
), | |||
), | |||
), | |||
), | |||
); | |||
} | |||
} |