diff --git a/packages/simplex_app/lib/model/group.dart b/packages/simplex_app/lib/model/group.dart new file mode 100644 index 0000000000..268f949624 --- /dev/null +++ b/packages/simplex_app/lib/model/group.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +class Group { + final String? groupName; + final String? groupDescription; + final List members; + + Group({this.groupName, this.groupDescription, this.members = const []}); + + factory Group.fromJson(Map json) { + return Group( + groupName: json['gName'], + groupDescription: json['desc'], + members: json['contacts'], + ); + } + + static Map toJson(Group group) { + return { + 'gName': group.groupName, + 'desc': group.groupDescription, + 'contacts': group.members, + }; + } + + static String encode(List groups) => json.encode( + groups + .map>((group) => Group.toJson(group)) + .toList(), + ); + + static List decode(String? groups) => + (json.decode(groups!) as List) + .map((item) => Group.fromJson(item)) + .toList(); +} diff --git a/packages/simplex_app/lib/views/group/add_group_view.dart b/packages/simplex_app/lib/views/group/add_group_view.dart index 750bfa7aad..45a278c139 100644 --- a/packages/simplex_app/lib/views/group/add_group_view.dart +++ b/packages/simplex_app/lib/views/group/add_group_view.dart @@ -2,7 +2,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:simplex_chat/constants.dart'; +import 'package:simplex_chat/model/group.dart'; import 'package:simplex_chat/widgets/custom_text_field.dart'; class AddGroupView extends StatefulWidget { @@ -13,11 +15,15 @@ class AddGroupView extends StatefulWidget { } class _AddGroupViewState extends State { + final _formKey = GlobalKey(); + final _displayNameController = TextEditingController(); + final _descController = TextEditingController(); @override void dispose() { _displayNameController.dispose(); + _descController.dispose(); super.dispose(); } @@ -27,57 +33,79 @@ class _AddGroupViewState extends State { onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( appBar: AppBar( + leading: BackButton( + onPressed: () => Navigator.of(context).pop(false), + ), title: const Text('New Group'), ), - body: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 10.0), - const Center( - child: GroupDP(), - ), - const SizedBox(height: 25.0), - const Text('Group Name', style: kSmallHeadingStyle), - const SizedBox(height: 10.0), - CustomTextField( - textEditingController: _displayNameController, - textInputType: TextInputType.name, - hintText: 'e.g College friends', - validatorFtn: (value) { - if (value!.isEmpty) { - return 'Group name cannot be empty!'; - } - return null; - }, - ), - const SizedBox(height: 10.0), - ListTile( - leading: const Icon(Icons.person_add), - title: const Text('Add a member'), - onTap: () {}, - ), - const Divider(height: 30.0), - const ListTile( - leading: CircleAvatar( - backgroundImage: AssetImage('assets/dp.png'), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 10.0), + const Center( + child: GroupDP(), ), - title: Text('You'), - trailing: Text( - 'Owner', - style: TextStyle(color: Colors.grey, fontSize: 12.0), - )), - ], + const SizedBox(height: 25.0), + const Text('Group Name', style: kSmallHeadingStyle), + const SizedBox(height: 10.0), + CustomTextField( + textEditingController: _displayNameController, + textInputType: TextInputType.name, + hintText: 'e.g College friends', + validatorFtn: (value) { + if (value!.isEmpty) { + return 'Group name cannot be empty!'; + } + return null; + }, + ), + const SizedBox(height: 10.0), + const Text('Group Description', style: kSmallHeadingStyle), + const SizedBox(height: 10.0), + CustomTextField( + textEditingController: _descController, + textInputType: TextInputType.text, + hintText: 'e.g Friends from UK', + ), + const SizedBox(height: 10.0), + ListTile( + leading: const Icon(Icons.person_add), + title: const Text('Add a member'), + onTap: () {}, + ), + const Divider(height: 30.0), + const ListTile( + leading: CircleAvatar( + backgroundImage: AssetImage('assets/dp.png'), + ), + title: Text('You'), + trailing: Text( + 'Owner', + style: TextStyle(color: Colors.grey, fontSize: 12.0), + )), + ], + ), + ), ), ), floatingActionButton: Visibility( visible: MediaQuery.of(context).viewInsets.bottom == 0, child: FloatingActionButton( heroTag: 'setup', - onPressed: () { - FocusScope.of(context).unfocus(); - Navigator.pop(context); + onPressed: () async { + if (_formKey.currentState!.validate()) { + FocusScope.of(context).unfocus(); + _addNewGroup(_displayNameController.text.trim(), + _descController.text.trim()); + _descController.clear(); + _displayNameController.clear(); + Navigator.of(context).pop(true); + } }, child: const Icon(Icons.check), ), @@ -85,6 +113,33 @@ class _AddGroupViewState extends State { ), ); } + + void _addNewGroup(String name, String desc) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + List _localList = []; + _localList = List.from(Group.decode(prefs.getString('groups'))); + + List _groups = [ + Group( + groupName: name, + groupDescription: desc, + members: [], + ), + ]; + _groups = _localList + _groups; + + final String _newGroups = Group.encode(_groups); + + await prefs.setString('groups', _newGroups); + + var snackBar = SnackBar( + backgroundColor: Colors.green, + content: Text('$name added!'), + ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(snackBar); + } } class GroupDP extends StatefulWidget { diff --git a/packages/simplex_app/lib/views/group/group_view.dart b/packages/simplex_app/lib/views/group/group_view.dart new file mode 100644 index 0000000000..aae190e1bb --- /dev/null +++ b/packages/simplex_app/lib/views/group/group_view.dart @@ -0,0 +1,335 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:simplex_chat/animations/bottom_animation.dart'; +import 'package:simplex_chat/app_routes.dart'; +import 'package:simplex_chat/constants.dart'; +import 'package:simplex_chat/model/group.dart'; +import 'package:simplex_chat/views/conversation/conversation_view.dart'; + +class GroupView extends StatefulWidget { + const GroupView({Key? key}) : super(key: key); + + @override + State createState() => _GroupViewState(); +} + +class _GroupViewState extends State { + bool? _eraseMedia = false; + final List _options = [ + 'Add group', + 'Scan invitation', + ]; + + List _groupList = []; + + // delete a group + void _deleteContact(Group group) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + setState(() { + _groupList.remove(group); + }); + await prefs.setString('groups', Group.encode(_groupList)); + var snackBar = SnackBar( + backgroundColor: Colors.red, + content: Text('${group.groupName} deleted!'), + ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(snackBar); + } + + // getting data from local storage FOR NOW!! + void _getGroups() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final String? _groups = prefs.getString('groups'); + setState(() { + _groupList = List.from(Group.decode(_groups)); + }); + } + + @override + void initState() { + _getGroups(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0), + child: Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: _addNewGroups, + child: SvgPicture.asset( + 'assets/logo.svg', + height: 40.0, + ), + ), + ), + const SizedBox(height: 15.0), + Row( + children: const [ + Icon(Icons.group, color: kPrimaryColor), + SizedBox(width: 8.0), + Text( + 'Groups', + style: kHeadingStyle, + ) + ], + ), + const SizedBox(height: 5.0), + _groupList.isEmpty + ? SizedBox( + height: MediaQuery.of(context).size.height * 0.7, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "You don't have any groups yet!", + style: kMediumHeadingStyle, + textAlign: TextAlign.center, + ), + SizedBox(height: 8.0), + Text( + 'Click the icon below to add a contact', + textAlign: TextAlign.center, + ), + ], + ), + ), + ) + : ListView( + shrinkWrap: true, + children: List.generate( + _groupList.length, + (index) => WidgetAnimator( + child: ListTile( + leading: const CircleAvatar( + backgroundImage: AssetImage('assets/dp.png'), + ), + title: Text(_groupList[index].groupName!), + subtitle: Text(_groupList[index].groupDescription!), + trailing: Text( + 'Members: ${_groupList[index].members.length}', + style: const TextStyle( + fontSize: 11, color: Colors.grey), + ), + onLongPress: () => + _conversationOptions(_groupList[index]), + ), + ), + ), + ), + ], + ), + ), + floatingActionButton: PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + offset: const Offset(-10, -120), + onSelected: (value) async { + if (value == _options[0]) { + var value = await Navigator.pushNamed(context, AppRoutes.addGroup); + if (value == true) { + _getGroups(); + } + } else { + await Navigator.pushNamed(context, AppRoutes.scanInvitation); + } + }, + itemBuilder: (context) => _options + .map( + (opt) => PopupMenuItem( + value: opt, + child: Text(opt), + ), + ) + .toList(), + child: const FloatingActionButton( + heroTag: 'group', + onPressed: null, + child: Icon( + Icons.group_add, + ), + ), + ), + ); + } + + void _conversationOptions(Group group) { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + _deleteConversation(group); + }, + child: const Text( + 'Delete Group', + style: TextStyle(color: Colors.red), + )), + TextButton( + onPressed: () { + Navigator.pop(context); + _disconnect(); + }, + child: const Text('Leave Group')), + ], + ), + ), + ); + } + + void _deleteConversation(Group group) { + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: const Text('Are you Sure?'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('All group history will be deleted from your device!'), + const SizedBox(height: 15.0), + Row( + children: [ + Checkbox( + value: _eraseMedia, + onChanged: (value) { + setState(() { + _eraseMedia = value; + }); + }), + const Text('Erase files & Media') + ], + ), + ], + ), + actions: [ + InkWell( + onTap: () { + _deleteContact(group); + Navigator.pop(context); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.check, color: Colors.green), + ), + ), + InkWell( + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.cancel_outlined, color: Colors.red), + ), + ) + ], + ), + ), + ); + } + + void _disconnect() { + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: const Text('Are you Sure?'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Leaving a group will erase all the data from your device and you will no longer be able to contact again!'), + const SizedBox(height: 15.0), + Row( + children: [ + Checkbox( + value: _eraseMedia, + onChanged: (value) { + setState(() { + _eraseMedia = value; + }); + }), + const Text('Erase files & Media') + ], + ), + ], + ), + actions: [ + InkWell( + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.check, color: Colors.green), + ), + ), + InkWell( + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.cancel_outlined, color: Colors.red), + ), + ) + ], + ), + ), + ); + } + + // dummy ftn for loading new contacts + void _addNewGroups() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + List _localList = []; + _localList = List.from(Group.decode(prefs.getString('groups'))); + + List _groups = [ + Group( + groupName: 'Family group', + groupDescription: 'Some description here', + members: [ + 'Hamza', + 'Alice', + 'John', + 'Bob', + ], + ), + Group( + groupName: 'Friends', + groupDescription: 'Miss you all', + members: [ + 'Alice', + 'John', + 'Bob', + ], + ), + ]; + _groups = _localList + _groups; + + // dummy ftn for filling the list + final String _newGroups = Group.encode(_groups); + + await prefs.setString('groups', _newGroups); + + _getGroups(); + + const snackBar = SnackBar( + backgroundColor: Colors.green, + content: Text('New Groups loaded!'), + ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(snackBar); + } +}