| @ -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'), | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| } | |||