diff --git a/assets/svg/ic_pencil.svg b/assets/svg/ic_pencil.svg new file mode 100644 index 00000000..d66ed484 --- /dev/null +++ b/assets/svg/ic_pencil.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/ic_trust_deactivated.svg b/assets/svg/ic_trust_deactivated.svg new file mode 100644 index 00000000..b3cef1bc --- /dev/null +++ b/assets/svg/ic_trust_deactivated.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart index 54194a67..3e275f7c 100644 --- a/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart +++ b/lib/desktop_screens_new/groups_screen/widgets/desktop_cover_image_picker.dart @@ -87,7 +87,7 @@ class DesktopCoverImagePicker extends StatelessWidget { ), ) : Container( - padding: const EdgeInsets.fromLTRB(108, 12, 108, 16), + padding: const EdgeInsets.only(top: 12, bottom: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: ColorConstants.pickerBackgroundColor, @@ -104,7 +104,6 @@ class DesktopCoverImagePicker extends StatelessWidget { AppVectors.icDesktopImage, width: 48, height: 32, - fit: BoxFit.fitWidth, ), ], ), diff --git a/lib/screens/group_contacts_screen/widgets/group_contact_list_tile.dart b/lib/screens/common_widgets/contact_list_tile.dart similarity index 100% rename from lib/screens/group_contacts_screen/widgets/group_contact_list_tile.dart rename to lib/screens/common_widgets/contact_list_tile.dart diff --git a/lib/screens/common_widgets/cover_image_picker.dart b/lib/screens/common_widgets/cover_image_picker.dart new file mode 100644 index 00000000..39ddc5a4 --- /dev/null +++ b/lib/screens/common_widgets/cover_image_picker.dart @@ -0,0 +1,114 @@ +import 'dart:typed_data'; + +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/images.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CoverImagePicker extends StatelessWidget { + final Function() onTap; + final Uint8List? groupImage; + final double height; + final EdgeInsetsGeometry margin; + final bool showOptions; + final Function()? onCancel; + + const CoverImagePicker({ + required this.onTap, + required this.groupImage, + required this.height, + this.margin = const EdgeInsets.symmetric(horizontal: 28), + this.showOptions = false, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: height, + width: double.infinity, + margin: margin, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: ColorConstants.dividerContextMenuColor, + ), + child: Stack( + fit: StackFit.expand, + children: [ + groupImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.memory( + groupImage!, + fit: BoxFit.cover, + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Insert Cover Image", + style: CustomTextStyles.greyW50014, + ), + SizedBox(height: 8), + Image.asset( + ImageConstants.icImage, + width: 48, + height: 32, + ), + ], + ), + if (showOptions) + Positioned( + top: 12, + right: 12, + left: 12, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: onCancel, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icCancel, + width: 16, + height: 16, + color: Colors.black, + fit: BoxFit.cover, + ), + ), + ), + if (groupImage != null) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white54.withOpacity(0.5), + ), + child: SvgPicture.asset( + AppVectors.icEdit, + width: 16, + height: 16, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/contact_new_version/contact_detail_screen.dart b/lib/screens/contact_new_version/contact_detail_screen.dart index 7ea37aba..2abf445b 100644 --- a/lib/screens/contact_new_version/contact_detail_screen.dart +++ b/lib/screens/contact_new_version/contact_detail_screen.dart @@ -156,46 +156,39 @@ class _ContactDetailScreenState extends State { SizedBox(height: 4), Flexible( child: isEditNickname - ? Row( - children: [ - Flexible( - child: TextField( - maxLines: 1, - decoration: InputDecoration( - contentPadding: EdgeInsets.only( - left: 16, - ), - hintText: 'Enter Nickname', - hintStyle: TextStyle( - fontSize: 14.toFont, - fontWeight: FontWeight.w500, - color: ColorConstants.textBlack, - ), - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(5), - borderSide: BorderSide.none, - ), - labelStyle: TextStyle( - fontSize: 14.toFont, - ), - fillColor: Colors.white, - filled: true, - suffixIcon: InkWell( - onTap: () { - nicknameController.clear(); - }, - child: Icon( - Icons.clear, - color: Colors.black, - size: 16, - ), - ), - ), - controller: nicknameController, + ? TextField( + maxLines: 1, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 16, + ), + hintText: 'Enter Nickname', + hintStyle: TextStyle( + fontSize: 14.toFont, + fontWeight: FontWeight.w500, + color: ColorConstants.textBlack, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + labelStyle: TextStyle( + fontSize: 14.toFont, + ), + fillColor: Colors.white, + filled: true, + suffixIcon: InkWell( + onTap: () { + nicknameController.clear(); + }, + child: Icon( + Icons.clear, + color: Colors.black, + size: 16, ), ), - ], + ), + controller: nicknameController, ) : Text( widget.contact.tags?['nickname'] ?? diff --git a/lib/screens/contact_new_version/contact_screen.dart b/lib/screens/contact_new_version/contact_screen.dart index 22fc314b..8c9a33c1 100644 --- a/lib/screens/contact_new_version/contact_screen.dart +++ b/lib/screens/contact_new_version/contact_screen.dart @@ -1,5 +1,4 @@ import 'package:at_common_flutter/services/size_config.dart'; -import 'package:at_contacts_group_flutter/screens/group_view/group_view.dart'; import 'package:at_contacts_group_flutter/services/group_service.dart'; import 'package:atsign_atmosphere_pro/data_models/enums/contact_type.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/app_bar_custom.dart'; @@ -10,6 +9,7 @@ import 'package:atsign_atmosphere_pro/screens/contact_new_version/contact_detail import 'package:atsign_atmosphere_pro/screens/contact_new_version/create_group_screen.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/contact_skeleton_loading_widget.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/list_contact_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/group_contacts_screen.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; import 'package:atsign_atmosphere_pro/view_models/contact_provider.dart'; import 'package:atsign_atmosphere_pro/view_models/create_group_provider.dart'; @@ -274,7 +274,7 @@ class _ContactScreenState extends State await Navigator.push( context, MaterialPageRoute( - builder: (context) => GroupView( + builder: (context) => GroupContactsScreen( group: group, ), ), diff --git a/lib/screens/contact_new_version/create_group_screen.dart b/lib/screens/contact_new_version/create_group_screen.dart index f289a51f..69a8bffc 100644 --- a/lib/screens/contact_new_version/create_group_screen.dart +++ b/lib/screens/contact_new_version/create_group_screen.dart @@ -2,17 +2,16 @@ import 'package:at_common_flutter/at_common_flutter.dart'; import 'package:at_commons/at_commons.dart'; import 'package:at_contact/at_contact.dart'; import 'package:at_contacts_group_flutter/at_contacts_group_flutter.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/cover_image_picker.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_toast.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/input_widget.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/search_widget.dart'; import 'package:atsign_atmosphere_pro/screens/contact_new_version/widget/list_contact_widget.dart'; import 'package:atsign_atmosphere_pro/utils/colors.dart'; -import 'package:atsign_atmosphere_pro/utils/images.dart'; import 'package:atsign_atmosphere_pro/utils/text_strings.dart'; import 'package:atsign_atmosphere_pro/utils/vectors.dart'; import 'package:atsign_atmosphere_pro/view_models/create_group_provider.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; @@ -115,7 +114,14 @@ class _CreateGroupScreenState extends State { }, ), ), - _buildImage(value.selectedImageByteData), + SizedBox(height: 16), + CoverImagePicker( + onTap: () async { + await _provider.selectCoverImage(); + }, + groupImage: value.selectedImageByteData, + height: 88, + ), Padding( padding: const EdgeInsets.only( top: 22, @@ -254,48 +260,4 @@ class _CreateGroupScreenState extends State { ); }); } - - Widget _buildImage(Uint8List? selectedImage) { - return InkWell( - onTap: () async { - await _provider.selectCoverImage(); - }, - child: Container( - height: 89, - width: double.infinity, - margin: const EdgeInsets.fromLTRB(27, 14, 27, 0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Color(0xFFECECEC), - ), - child: selectedImage != null - ? ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.memory( - selectedImage, - fit: BoxFit.cover, - ), - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Insert Cover Image", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: ColorConstants.grey, - ), - ), - SizedBox(height: 8), - Image.asset( - ImageConstants.icImage, - ), - ], - ), - ), - ), - ); - } } diff --git a/lib/screens/group_contacts/group_contacts_screen.dart b/lib/screens/group_contacts/group_contacts_screen.dart new file mode 100644 index 00000000..599023b0 --- /dev/null +++ b/lib/screens/group_contacts/group_contacts_screen.dart @@ -0,0 +1,308 @@ +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/cover_image_picker.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_custom_button.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_edit_options_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_manage_members_widget.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_list_view.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupContactsScreen extends StatefulWidget { + final AtGroup group; + + const GroupContactsScreen({required this.group}); + + @override + State createState() => _GroupContactsScreenState(); +} + +class _GroupContactsScreenState extends State { + bool showEditOptions = false; + bool isEditingName = false; + bool isEditingImage = false; + late TextEditingController groupNameController = + TextEditingController(text: widget.group.groupName); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !isEditingName, + onPopInvoked: (didPop) { + if (isEditingName) { + groupNameController.text = widget.group.groupName ?? ''; + setState(() { + isEditingName = false; + }); + } + }, + child: Scaffold( + backgroundColor: ColorConstants.background, + appBar: GroupsAppBar( + title: isEditingName ? 'Edit' : widget.group.groupName ?? '', + onBack: () { + if (isEditingName) { + groupNameController.text = widget.group.groupName ?? ''; + setState(() { + isEditingName = false; + }); + } else { + Navigator.pop(context); + } + }, + actions: [ + isEditingName || isEditingImage + ? SvgPicture.asset( + AppVectors.icOptions, + width: 16, + fit: BoxFit.cover, + color: Colors.black, + ) + : InkWell( + onTap: () { + setState(() { + showEditOptions = !showEditOptions; + }); + }, + child: SvgPicture.asset( + AppVectors.icPencil, + width: 24, + height: 24, + fit: BoxFit.cover, + color: showEditOptions + ? ColorConstants.orangeColor + : Colors.black, + ), + ), + ], + ), + body: SafeArea( + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20), + isEditingName + ? buildEditGroupNameWidget() + : CoverImagePicker( + showOptions: isEditingImage, + onTap: () {}, + groupImage: widget.group.groupPicture, + height: 120, + ), + SizedBox(height: isEditingName || isEditingImage ? 24 : 12), + isEditingName || isEditingImage + ? GroupsCustomButton( + title: 'Save', + onTap: () {}, + borderRadius: 5, + ) + : GroupsCustomButton( + title: 'Transfer File', + suffix: SvgPicture.asset( + AppVectors.icTransfer, + width: 24, + height: 16, + fit: BoxFit.cover, + ), + onTap: () {}, + borderRadius: 7, + ), + Expanded( + child: GroupsMemberListView( + members: widget.group.members!, + ), + ), + ], + ), + if (showEditOptions) ...[ + Positioned( + top: 0, + bottom: 0, + right: 0, + left: 0, + child: InkWell( + onTap: () { + setState(() { + showEditOptions = false; + }); + }, + ), + ), + Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: GroupsEditOptionsWidget( + onEditName: () { + setState(() { + showEditOptions = false; + isEditingName = true; + }); + }, + onCoverImage: () { + setState(() { + showEditOptions = false; + isEditingImage = true; + }); + }, + onManageMembers: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => GroupsManageMembersWidget(), + ), + ); + }, + onDelete: () async { + await showDeleteConfirmDialog(); + }, + ), + ), + ] + ], + ), + ), + ), + ); + } + + Widget buildEditGroupNameWidget() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Group Name', + style: CustomTextStyles.blackW5008, + ), + SizedBox(height: 4), + TextField( + maxLines: 1, + style: CustomTextStyles.blackW50014, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 28, + top: 16, + bottom: 16, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + fillColor: Colors.white, + filled: true, + isDense: true, + suffix: InkWell( + onTap: () { + groupNameController.clear(); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12), + child: SvgPicture.asset( + AppVectors.icCancel, + color: Colors.black, + height: 8, + width: 8, + fit: BoxFit.cover, + ), + ), + ), + ), + controller: groupNameController, + ) + ], + ), + ); + } + + Future showDeleteConfirmDialog() async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 28, vertical: 24), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Delete', + style: CustomTextStyles.blackBold(size: 14), + ), + SizedBox(height: 8), + Text( + 'Are you sure you want to delete ${widget.group.groupName}?', + style: CustomTextStyles.blackW40013, + textAlign: TextAlign.center, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Note', + style: CustomTextStyles.blackItalicW40013, + ), + TextSpan( + text: ': this action cannot be undone.', + style: CustomTextStyles.blackW40013, + ) + ], + ), + ), + SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.black), + ), + child: Text( + 'Cancel', + style: CustomTextStyles.blackUnderlineW40012, + ), + ), + ), + InkWell( + child: Container( + height: 36, + width: 84, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: ColorConstants.bottomBlack, + ), + child: Text( + 'Move', + style: CustomTextStyles.whiteBold12, + ), + ), + ) + ], + ), + ) + ], + ), + ); + }, + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_app_bar.dart b/lib/screens/group_contacts/widgets/groups_app_bar.dart new file mode 100644 index 00000000..f038c50a --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_app_bar.dart @@ -0,0 +1,57 @@ +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupsAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List actions; + final Function()? onBack; + + const GroupsAppBar({ + required this.title, + this.actions = const [], + this.onBack, + }); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + SizedBox(width: 8), + InkWell( + onTap: onBack ?? + () { + Navigator.pop(context); + }, + child: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + AppVectors.icBack, + height: 20, + width: 8, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ), + ), + SizedBox(width: 32), + Text( + title, + style: CustomTextStyles.desktopPrimaryW50018, + ), + Spacer(), + ...actions + ], + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(28); +} diff --git a/lib/screens/group_contacts/widgets/groups_custom_button.dart b/lib/screens/group_contacts/widgets/groups_custom_button.dart new file mode 100644 index 00000000..a6cb4b8c --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_custom_button.dart @@ -0,0 +1,60 @@ +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:flutter/material.dart'; + +class GroupsCustomButton extends StatelessWidget { + final Function() onTap; + final bool isLoading; + final String title; + final double spaceSize; + final Widget? suffix; + final double borderRadius; + + const GroupsCustomButton({ + required this.onTap, + this.isLoading = false, + required this.title, + this.spaceSize = 28, + this.suffix, + required this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 52, + width: double.infinity, + decoration: BoxDecoration( + color: ColorConstants.raisinBlack, + borderRadius: BorderRadius.circular(borderRadius), + ), + margin: EdgeInsets.symmetric(horizontal: 28), + child: InkWell( + onTap: onTap, + child: Center( + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: CustomTextStyles.whiteBold(size: 14), + ), + if (suffix != null) ...[ + SizedBox(width: spaceSize), + suffix!, + ] + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart b/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart new file mode 100644 index 00000000..43f0fbe5 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_edit_options_widget.dart @@ -0,0 +1,102 @@ +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:flutter/material.dart'; + +class GroupsEditOptionsWidget extends StatelessWidget { + final Function() onEditName; + final Function() onCoverImage; + final Function() onManageMembers; + final Function() onDelete; + + const GroupsEditOptionsWidget({ + required this.onEditName, + required this.onCoverImage, + required this.onManageMembers, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(7), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 18, + offset: Offset(0, 4), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: onEditName, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Edit Name', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onCoverImage, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Edit Cover Image', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onManageMembers, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Manage Members', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + Divider( + height: 0, + thickness: 1, + color: Colors.black, + ), + InkWell( + onTap: onDelete, + child: SizedBox( + height: 56, + child: Center( + child: Text( + 'Delete', + style: CustomTextStyles.blackW40017, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart new file mode 100644 index 00000000..31a43c60 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_manage_members_widget.dart @@ -0,0 +1,259 @@ +import 'package:at_common_flutter/services/size_config.dart'; +import 'package:at_contact/at_contact.dart'; +import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; +import 'package:at_contacts_group_flutter/services/group_service.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_app_bar.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_item_widget.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; + +class GroupsManageMembersWidget extends StatefulWidget { + const GroupsManageMembersWidget(); + + @override + State createState() => + _GroupsManageMembersWidgetState(); +} + +class _GroupsManageMembersWidgetState extends State { + TextEditingController searchController = TextEditingController(); + late TrustedContactProvider trustedContactProvider = + context.read(); + final _groupService = GroupService(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _groupService.fetchGroupsAndContacts(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: ColorConstants.background, + appBar: GroupsAppBar(title: 'Manage Members'), + body: SafeArea( + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 28), + Padding( + padding: EdgeInsets.only(left: 36, right: 20), + child: buildSearchWidget(), + ), + SizedBox(height: 12), + Expanded( + child: StreamBuilder>( + stream: _groupService.allContactsStream, + initialData: _groupService.allContacts, + builder: (context, snapshot) { + final contactList = (snapshot.data ?? []) + .where((e) => e?.contact != null) + .map((e) => e!.contact!) + .toList(); + return ListView.separated( + physics: ClampingScrollPhysics(), + padding: EdgeInsets.only( + left: 32, + right: 32, + bottom: 112, + ), + itemBuilder: (context, index) { + return buildContactList(contactList)[index]; + }, + separatorBuilder: (context, index) { + return SizedBox(height: 12); + }, + itemCount: buildContactList(contactList).length, + ); + }, + ), + ), + ], + ), + Positioned( + bottom: 28, + left: 48, + right: 48, + child: buildSelectContactButton(), + ), + ], + ), + ), + ); + } + + Widget buildSearchWidget() { + return Row( + children: [ + Expanded( + child: TextField( + maxLines: 1, + style: CustomTextStyles.blackW50014, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + left: 20, + top: 20, + bottom: 20, + right: 24, + ), + hintText: 'Search', + hintStyle: CustomTextStyles.greyW50014, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + fillColor: Colors.white, + filled: true, + isDense: true, + suffixIcon: Icon( + Icons.search, + size: 20, + color: ColorConstants.grey, + ), + ), + controller: searchController, + ), + ), + SizedBox(width: 12), + InkWell( + child: Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorConstants.iconButtonColor, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + AppVectors.icTrust, + width: 24, + height: 20, + color: Colors.black, + fit: BoxFit.cover, + ), + ), + ), + ], + ); + } + + List buildContactList(List list) { + final List result = []; + if (list.isNotEmpty) { + final List sortedList = sortGroupAlphabetical(list); + bool isSameCharWithPrev(int i) => + (((sortedList[i].atSign ?? '').isNotEmpty + ? sortedList[i].atSign![1] + : ' ') != + ((sortedList[i - 1].atSign ?? '').isNotEmpty + ? sortedList[i - 1].atSign![1] + : ' ')); + + bool isPrevCharacter(int i) => RegExp(r'^[a-z]+$').hasMatch( + (((sortedList[i - 1].atSign ?? '').isNotEmpty + ? sortedList[i - 1].atSign![1] + : ' '))[0] + .toLowerCase()); + + for (int i = 0; i < sortedList.length; i++) { + if (i == 0 || (isSameCharWithPrev(i) && isPrevCharacter(i))) { + result.add(buildAlphabeticalTitle( + (sortedList[i].atSign ?? '').isNotEmpty + ? sortedList[i].atSign![1] + : '')); + } + result.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: GroupsMemberItemWidget( + member: sortedList[i], + isTrusted: trustedContactProvider.trustedContacts.any( + (element) => element.atSign == sortedList[i].atSign, + ), + ), + ), + ); + } + } + return result; + } + + Widget buildAlphabeticalTitle(String char) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + char.isNotEmpty && RegExp(r'^[a-z]+$').hasMatch(char.toLowerCase()) + ? Text( + char.toUpperCase(), + style: CustomTextStyles.darkSliverBold20, + ) + : Text( + 'Others', + style: CustomTextStyles.darkSliverBold20, + ), + SizedBox(width: 20), + Divider( + height: 1.toHeight, + color: ColorConstants.dividerColor, + ) + ], + ), + ); + } + + List sortGroupAlphabetical(List list) { + final List emptyTitleContact = list + .where((e) => + (e.atSign ?? '').isEmpty || + !RegExp(r'^[a-z]+$').hasMatch( + (e.atSign ?? '')[1].toLowerCase(), + )) + .toList(); + final List nonEmptyTitleContact = list + .where((e) => + (e.atSign ?? '').isNotEmpty && + RegExp(r'^[a-z]+$').hasMatch( + (e.atSign ?? '')[1].toLowerCase(), + )) + .toList(); + nonEmptyTitleContact.sort( + (a, b) => (a.atSign?[1] ?? '').compareTo( + (b.atSign?[1] ?? ''), + ), + ); + return [...nonEmptyTitleContact, ...emptyTitleContact]; + } + + Widget buildSelectContactButton() { + return Container( + padding: EdgeInsets.symmetric(vertical: 16), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(7), + ), + child: RichText( + text: TextSpan( + text: 'Selects Contact ', + style: CustomTextStyles.whiteBold16, + children: [ + TextSpan( + text: '(3)', + style: CustomTextStyles.whiteW40016, + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_member_item_widget.dart b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart new file mode 100644 index 00000000..81fbd08d --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_member_item_widget.dart @@ -0,0 +1,91 @@ +import 'dart:typed_data'; + +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; +import 'package:atsign_atmosphere_pro/utils/colors.dart'; +import 'package:atsign_atmosphere_pro/utils/text_styles.dart'; +import 'package:atsign_atmosphere_pro/utils/vectors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class GroupsMemberItemWidget extends StatelessWidget { + final AtContact member; + final bool isTrusted; + final bool isSelected; + + const GroupsMemberItemWidget({ + required this.member, + required this.isTrusted, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB(16, 16, 24, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? Border.all( + color: ColorConstants.orange, + width: 2, + ) + : null, + ), + child: Row( + children: [ + member.tags?['image'] != null + ? ClipRRect( + borderRadius: BorderRadius.circular(18), + child: Image.memory( + Uint8List.fromList(member.tags?['image'].cast()), + width: 40, + height: 40, + fit: BoxFit.cover, + ), + ) + : ContactInitial( + initials: member.atSign?.substring(1), + borderRadius: 18, + ), + SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + member.atSign ?? '', + style: CustomTextStyles.blackW60013, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (member.tags?['nickname'] != null) + Text( + member.tags?['nickname'], + style: CustomTextStyles.blackW60013, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + )), + SizedBox(width: 16), + SizedBox( + width: 28, + height: 28, + child: SvgPicture.asset( + isTrusted + ? AppVectors.icTrustActivated + : AppVectors.icTrustDeactivated, + width: 24, + height: 20, + fit: BoxFit.cover, + alignment: Alignment.center, + ), + ) + ], + ), + ); + } +} diff --git a/lib/screens/group_contacts/widgets/groups_member_list_view.dart b/lib/screens/group_contacts/widgets/groups_member_list_view.dart new file mode 100644 index 00000000..0aac6fe0 --- /dev/null +++ b/lib/screens/group_contacts/widgets/groups_member_list_view.dart @@ -0,0 +1,43 @@ +import 'package:at_contact/at_contact.dart'; +import 'package:atsign_atmosphere_pro/screens/group_contacts/widgets/groups_member_item_widget.dart'; +import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class GroupsMemberListView extends StatefulWidget { + final Set members; + + const GroupsMemberListView({ + required this.members, + }); + + @override + State createState() => _GroupsMemberListViewState(); +} + +class _GroupsMemberListViewState extends State { + late TrustedContactProvider trustedContactProvider = + context.read(); + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: EdgeInsets.symmetric(horizontal: 36, vertical: 28), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return GroupsMemberItemWidget( + member: widget.members.elementAt(index), + isTrusted: trustedContactProvider.trustedContacts.any( + (element) => + element.atSign == widget.members.elementAt(index).atSign, + ), + ); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 16); + }, + itemCount: widget.members.length, + ); + } +} diff --git a/lib/screens/trusted_contacts/trusted_contacts.dart b/lib/screens/trusted_contacts/trusted_contacts.dart index 5717d159..855cae04 100644 --- a/lib/screens/trusted_contacts/trusted_contacts.dart +++ b/lib/screens/trusted_contacts/trusted_contacts.dart @@ -9,7 +9,7 @@ import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dar import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_button.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_circle_avatar.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/provider_handler.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:atsign_atmosphere_pro/screens/trusted_contacts/widgets/remove_trusted_contact_dialog.dart'; import 'package:atsign_atmosphere_pro/utils/images.dart'; import 'package:atsign_atmosphere_pro/utils/text_strings.dart'; diff --git a/lib/screens/welcome_screen/widgets/overlapping_contacts.dart b/lib/screens/welcome_screen/widgets/overlapping_contacts.dart index 17b6294e..7f87541b 100644 --- a/lib/screens/welcome_screen/widgets/overlapping_contacts.dart +++ b/lib/screens/welcome_screen/widgets/overlapping_contacts.dart @@ -7,7 +7,7 @@ import 'package:at_contact/at_contact.dart'; import 'package:at_contacts_group_flutter/models/group_contacts_model.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/custom_circle_avatar.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:atsign_atmosphere_pro/screens/welcome_screen/widgets/contact_card.dart'; import 'package:atsign_atmosphere_pro/services/common_utility_functions.dart'; import 'package:atsign_atmosphere_pro/view_models/trusted_sender_view_model.dart'; diff --git a/lib/utils/text_styles.dart b/lib/utils/text_styles.dart index 31864d3d..9aac0c8f 100644 --- a/lib/utils/text_styles.dart +++ b/lib/utils/text_styles.dart @@ -373,6 +373,12 @@ class CustomTextStyles { letterSpacing: 0.1, fontWeight: FontWeight.normal); + static TextStyle desktopPrimaryW50018 = TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.w500, + ); + static TextStyle desktopPrimaryRegular16 = TextStyle( color: Colors.black, fontSize: 16, @@ -405,6 +411,12 @@ class CustomTextStyles { fontWeight: FontWeight.w500, ); + static TextStyle whiteW40016 = TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w400, + ); + static TextStyle blackW50020 = const TextStyle( color: Colors.black, fontSize: 20, @@ -412,7 +424,10 @@ class CustomTextStyles { ); static TextStyle orangeW50014 = TextStyle( - color: ColorConstants.orange, fontSize: 14, fontWeight: FontWeight.w500); + color: ColorConstants.orange, + fontSize: 14, + fontWeight: FontWeight.w500, + ); static TextStyle whiteBold12 = const TextStyle( color: Colors.white, @@ -432,6 +447,26 @@ class CustomTextStyles { fontWeight: FontWeight.w400, ); + static TextStyle blackUnderlineW40012 = const TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w400, + decoration: TextDecoration.underline, + ); + + static TextStyle blackW40013 = const TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w400, + ); + + static TextStyle blackItalicW40013 = const TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.w400, + fontStyle: FontStyle.italic, + ); + static TextStyle darkSliverBold20 = TextStyle( color: ColorConstants.darkSliver, fontSize: 20, @@ -474,6 +509,12 @@ class CustomTextStyles { fontSize: 12, ); + static TextStyle blackW40017 = TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + fontSize: 17, + ); + static TextStyle raisinBlackW40010 = TextStyle( color: ColorConstants.raisinBlack, fontSize: 10, @@ -498,6 +539,12 @@ class CustomTextStyles { fontSize: 11, ); + static TextStyle blackW5008 = TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 8, + ); + static TextStyle blackW50014 = TextStyle( color: Colors.black, fontWeight: FontWeight.w500, @@ -515,4 +562,10 @@ class CustomTextStyles { fontSize: 11, fontWeight: FontWeight.w400, ); + + static TextStyle greyW50014 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: ColorConstants.grey, + ); } diff --git a/lib/utils/vectors.dart b/lib/utils/vectors.dart index 80108e90..36e7dc6d 100644 --- a/lib/utils/vectors.dart +++ b/lib/utils/vectors.dart @@ -98,4 +98,7 @@ class AppVectors { static String icDownloadDisable = '$_basePath/ic_download_disable.svg'; static String icSendDisable = '$_basePath/ic_send_disable.svg'; static String icDeleteDisable = '$_basePath/ic_delete_disable.svg'; + static String icPencil = '$_basePath/ic_pencil.svg'; + static String icMobileImage = '$_basePath/ic_mobile_image.svg'; + static String icTrustDeactivated = '$_basePath/ic_trust_deactivated.svg'; } diff --git a/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart b/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart index 3a9a6b08..28a364c3 100644 --- a/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart +++ b/test/widget_tests/group_contact_screen_test/group_contact_list_test.dart @@ -1,6 +1,6 @@ import 'package:at_common_flutter/at_common_flutter.dart'; import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_initial.dart'; -import 'package:atsign_atmosphere_pro/screens/group_contacts_screen/widgets/group_contact_list_tile.dart'; +import 'package:atsign_atmosphere_pro/screens/common_widgets/contact_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart';