diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1a64db59..dc94c60b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + diff --git a/lib/core/contact_service.dart b/lib/core/contact_service.dart new file mode 100644 index 00000000..ab395a74 --- /dev/null +++ b/lib/core/contact_service.dart @@ -0,0 +1,44 @@ +import 'package:flutter/cupertino.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:fast_contacts/fast_contacts.dart'; +import 'package:hive/hive.dart'; +import 'package:telware_cross_platform/core/models/contact_model.dart'; +import 'package:telware_cross_platform/core/utils.dart'; + +class ContactService { + Future fetchAndStoreContacts() async { + bool isPermissionGranted = await Permission.contacts.isGranted; + if (!isPermissionGranted) { + await Permission.contacts.request(); + } + if (await Permission.contacts.isGranted) { + final contacts = await FastContacts.getAllContacts(); + + // for (var contact in contacts) { + // contact.toMap().forEach((key, value) { + // debugPrint('$key: $value'); + // }); + // } + + var box = await Hive.openBox("contacts-block"); + await box.clear(); + // Store contacts in Hive + for (var contact in contacts) { + if (contact.phones.isEmpty || contact.displayName.isEmpty) continue; + final thumbnail = await FastContacts.getContactImage(contact.id); + final email = + contact.emails.isNotEmpty ? contact.emails[0].toString() : ""; + final phone = formatPhoneNumber(contact.phones[0].number); + box.put( + contact.id, + ContactModelBlock( + name: contact.displayName, + email: email, + phone: phone, + photo: thumbnail, + lastSeen: ""), + ); + } + } + } +} diff --git a/lib/core/models/contact_model.dart b/lib/core/models/contact_model.dart new file mode 100644 index 00000000..bc5a3756 --- /dev/null +++ b/lib/core/models/contact_model.dart @@ -0,0 +1,91 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:typed_data'; + +import 'package:hive/hive.dart'; + +part 'contact_model.g.dart'; + +@HiveType(typeId: 3) +class ContactModelBlock { + @HiveField(0) + final String name; + @HiveField(1) + final String? email; + @HiveField(2) + final Uint8List? photo; + @HiveField(3) + final String phone; + @HiveField(4) + final String lastSeen; + +// + const ContactModelBlock({ + required this.name, + this.email, + this.photo, + required this.phone, + required this.lastSeen, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ContactModelBlock && + runtimeType == other.runtimeType && + name == other.name && + email == other.email && + photo == other.photo && + phone == other.phone && + lastSeen == other.lastSeen); + + @override + int get hashCode => + name.hashCode ^ + email.hashCode ^ + photo.hashCode ^ + phone.hashCode ^ + lastSeen.hashCode; + + @override + String toString() { + return 'ContactModelBlock{ name: $name, email: $email, photo: $photo, phone: $phone, lastSeen: $lastSeen,}'; + } + + ContactModelBlock copyWith({ + String? name, + String? email, + Uint8List? photo, + String? phone, + String? lastSeen, + }) { + return ContactModelBlock( + name: name ?? this.name, + email: email ?? this.email, + photo: photo ?? this.photo, + phone: phone ?? this.phone, + lastSeen: lastSeen ?? this.lastSeen, + ); + } + + Map toMap() { + return { + 'name': name, + 'email': email, + 'photo': photo, + 'phone': phone, + 'lastSeen': lastSeen, + }; + } + + factory ContactModelBlock.fromMap(Map map) { + return ContactModelBlock( + name: map['name'] as String, + email: map['email'] as String, + photo: map['photo'] as Uint8List, + phone: map['phone'] as String, + lastSeen: map['lastSeen'] as String, + ); + } + +// +} diff --git a/lib/core/models/contact_model.g.dart b/lib/core/models/contact_model.g.dart new file mode 100644 index 00000000..b79b1ce0 --- /dev/null +++ b/lib/core/models/contact_model.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contact_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ContactModelBlockAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + ContactModelBlock read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ContactModelBlock( + name: fields[0] as String, + email: fields[1] as String?, + photo: fields[2] as Uint8List?, + phone: fields[3] as String, + lastSeen: fields[4] as String, + ); + } + + @override + void write(BinaryWriter writer, ContactModelBlock obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.email) + ..writeByte(2) + ..write(obj.photo) + ..writeByte(3) + ..write(obj.phone) + ..writeByte(4) + ..write(obj.lastSeen); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ContactModelBlockAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/core/theme/palette.dart b/lib/core/theme/palette.dart index ca3ba28e..701b9312 100644 --- a/lib/core/theme/palette.dart +++ b/lib/core/theme/palette.dart @@ -6,6 +6,8 @@ class Palette { static const Color secondary = Color.fromRGBO(29, 39, 51, 1); static const Color trinary = Color.fromRGBO(33, 45, 59, 1.0); static const Color quaternary = Color.fromRGBO(41, 56, 73, 1); + static const Color tabBar = Color.fromRGBO(36, 45, 57, 1.0); + static const Color scrollBar = Color.fromRGBO(61, 74, 89, 1.0); static const Color accent = Color.fromRGBO(110, 178, 239, 1); static const Color primaryText = Color.fromRGBO(255, 255, 255, 1); static const Color accentText = Color.fromRGBO(125, 139, 153, 1); diff --git a/lib/core/theme/sizes.dart b/lib/core/theme/sizes.dart index 453484f7..b991abf0 100644 --- a/lib/core/theme/sizes.dart +++ b/lib/core/theme/sizes.dart @@ -5,4 +5,4 @@ class Sizes { static const double infoText = 12; static const double iconSize = 22; static const double circleButtonRadius = 16; -} \ No newline at end of file +} diff --git a/lib/core/utils.dart b/lib/core/utils.dart index 709ee320..4e6e89d7 100644 --- a/lib/core/utils.dart +++ b/lib/core/utils.dart @@ -1,5 +1,5 @@ // common utility functions are added here - +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -67,8 +67,13 @@ void showToastMessage(String message) async { } String formatPhoneNumber(String phoneNumber) { - // Ensure the input has the right length and starts with the correct prefix. - if (phoneNumber.length == 13 && phoneNumber.startsWith("+20")) { + // remove all spaces + phoneNumber = phoneNumber.replaceAll(" ", ""); + + if (!phoneNumber.startsWith("+20")) { + phoneNumber = "+20${phoneNumber.substring(1)}"; + } + if (phoneNumber.length == 13) { return "${phoneNumber.substring(0, 3)} " // +20 "${phoneNumber.substring(3, 6)} " // 109 "${phoneNumber.substring(6)}"; // 3401932 @@ -76,6 +81,17 @@ String formatPhoneNumber(String phoneNumber) { return phoneNumber; // Return the original if it doesn't match the format. } +// Helper function to generate random pastel-like colors +Color getRandomColor() { + final Random random = Random(); + return Color.fromARGB( + 255, + 50 + random.nextInt(100), // Red (200-255) + 50 + random.nextInt(100), // Green (200-255) + 50 + random.nextInt(100), // Blue (200-255) + ); +} + String toKebabCase(String input) { // Convert to lowercase and replace spaces with hyphens String kebabCased = input.toLowerCase().replaceAll(RegExp(r'[\s]+'), '-'); diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index 0c8e2182..6f8f949b 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -171,8 +171,9 @@ class _SignUpScreenState extends ConsumerState { if (someNotFilled) { vibrate(); } else { - showConfirmationDialog( - context, emailController, _controllerPlus, signUp, onEdit); + // todo show the dialog (marwan) + // showConfirmationDialog( + // context, emailController, _controllerPlus, signUp, onEdit); } } diff --git a/lib/features/auth/view/widget/auth_sub_text_button.dart b/lib/features/auth/view/widget/auth_sub_text_button.dart index 2a43f4c3..9115f4b9 100644 --- a/lib/features/auth/view/widget/auth_sub_text_button.dart +++ b/lib/features/auth/view/widget/auth_sub_text_button.dart @@ -11,6 +11,7 @@ class AuthSubTextButton extends StatelessWidget { this.padding = const EdgeInsets.all(0), this.fontSize = Sizes.infoText, this.buttonKey, + this.color, }); final VoidCallback onPressed; @@ -18,6 +19,7 @@ class AuthSubTextButton extends StatelessWidget { final String label; final EdgeInsetsGeometry padding; final double fontSize; + final Color? color; @override Widget build(BuildContext context) { @@ -31,7 +33,7 @@ class AuthSubTextButton extends StatelessWidget { ), child: TitleElement( name: label, - color: Palette.accent, + color: color ?? Palette.accent, fontSize: fontSize, fontWeight: FontWeight.bold, padding: padding, diff --git a/lib/features/auth/view/widget/confirmation_dialog.dart b/lib/features/auth/view/widget/confirmation_dialog.dart index 1d771097..5d1062ad 100644 --- a/lib/features/auth/view/widget/confirmation_dialog.dart +++ b/lib/features/auth/view/widget/confirmation_dialog.dart @@ -3,92 +3,93 @@ import 'package:flutter/material.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart'; import 'package:telware_cross_platform/core/theme/sizes.dart'; -import 'package:webview_flutter_plus/webview_flutter_plus.dart'; import 'auth_sub_text_button.dart'; -void showConfirmationDialog( - BuildContext context, - TextEditingController emailController, - WebViewControllerPlus controllerPlus, - VoidCallback onConfirm, - VoidCallback onEdit) { +void showConfirmationDialog({ + required BuildContext context, + required String title, + FontWeight? titleFontWeight, + Color? titleColor, + double? titleFontSize, + required String subtitle, + FontWeight? subtitleFontWeight, + Color? subtitleColor, + double? subtitleFontSize, + double? contentGap, + required String confirmText, + EdgeInsetsGeometry? confirmPadding, + Color? confirmColor, + required String cancelText, + EdgeInsetsGeometry? cancelPadding, + Color? cancelColor, + required VoidCallback onConfirm, + required VoidCallback onCancel, + MainAxisAlignment? actionsAlignment, +}) { showDialog( context: context, barrierDismissible: true, // Allow dismissing the dialog by tapping outside builder: (BuildContext context) { return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0), // Blur effect - child: Padding( - padding: const EdgeInsets.only(top: 100), + filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0), // Blur effect + child: AlertDialog( + backgroundColor: Palette.trinary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + actionsAlignment: MainAxisAlignment.spaceBetween, + contentPadding: const EdgeInsets.only(top: 16), + content: IntrinsicHeight( + // width: 1000, + // height: 80, child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - AlertDialog( - backgroundColor: Palette.background, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - actionsAlignment: MainAxisAlignment.spaceBetween, - contentPadding: const EdgeInsets.only(top: 16), - content: SizedBox( - width: 1000, // 90% of screen width - height: 60, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const TitleElement( - name: 'Is this the correct email address?', - color: Palette.accentText, - fontSize: Sizes.secondaryText, - padding: EdgeInsets.only(right: 70, bottom: 10), - ), - TitleElement( - name: emailController.text, - color: Palette.primaryText, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ], - ), - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AuthSubTextButton( - onPressed: onEdit, - fontSize: Sizes.secondaryText, - label: 'Edit', - ), - AuthSubTextButton( - onPressed: onConfirm, - fontSize: Sizes.secondaryText, - label: 'Yes', - ), - ], - ) - ], + TitleElement( + name: title, + color: titleColor ?? Palette.accentText, + fontSize: titleFontSize ?? Sizes.secondaryText, + fontWeight: titleFontWeight, + textAlign: TextAlign.left, ), - Transform.scale( - scale: 1.5, // Adjust the scale factor as needed - child: Padding( - padding: const EdgeInsets.only(left: 70, top: 30), - child: SizedBox( - height: 200, - width: 300, - child: WebViewWidget( - controller: controllerPlus, - ), - ), - ), + TitleElement( + name: subtitle, + color: subtitleColor ?? Palette.primaryText, + fontSize: subtitleFontSize ?? 16, + fontWeight: subtitleFontWeight ?? FontWeight.bold, + textAlign: TextAlign.left, ), + SizedBox(height: contentGap ?? 10), ], ), - )); + ), + actions: [ + Row( + mainAxisAlignment: + actionsAlignment ?? MainAxisAlignment.spaceBetween, + children: [ + AuthSubTextButton( + onPressed: onCancel, + fontSize: Sizes.secondaryText, + label: cancelText, + color: cancelColor, + padding: cancelPadding ?? const EdgeInsets.all(0), + ), + AuthSubTextButton( + onPressed: onConfirm, + fontSize: Sizes.secondaryText, + label: confirmText, + color: confirmColor, + padding: confirmPadding ?? const EdgeInsets.all(0), + ), + ], + ) + ], + ), + ); }, ); } diff --git a/lib/features/user/view/screens/block_user.dart b/lib/features/user/view/screens/block_user.dart index d496bdb0..04daca4f 100644 --- a/lib/features/user/view/screens/block_user.dart +++ b/lib/features/user/view/screens/block_user.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; -import 'package:telware_cross_platform/core/theme/dimensions.dart'; -import 'package:telware_cross_platform/features/user/view/widget/settings_section.dart'; -import 'package:telware_cross_platform/features/user/view/widget/toolbar_widget.dart'; +import 'package:telware_cross_platform/core/contact_service.dart'; +import 'package:telware_cross_platform/features/user/view/widget/empty_chats.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/features/user/view/widget/user_chats.dart'; +import 'package:hive/hive.dart'; +import 'package:telware_cross_platform/core/models/contact_model.dart'; +import 'package:flexible_scrollbar/flexible_scrollbar.dart'; + +import 'package:telware_cross_platform/features/auth/view/widget/confirmation_dialog.dart'; class BlockUserScreen extends StatefulWidget { static const String route = '/block-user'; @@ -14,42 +18,130 @@ class BlockUserScreen extends StatefulWidget { State createState() => _BlockUserScreen(); } -class _BlockUserScreen extends State { - late List> blockSections; +class _BlockUserScreen extends State + with TickerProviderStateMixin { + final ContactService _contactService = ContactService(); + late List> userChats; + late List> userContacts; + late AnimationController _duckController; + late TabController _tabController; + final ScrollController scrollController = ScrollController(); + + void blockConfirmationDialog(String user) { + showConfirmationDialog( + context: context, + title: 'Block user', + titleFontWeight: FontWeight.bold, + titleColor: Palette.primaryText, + titleFontSize: 18.0, + subtitle: 'Are you sure you want to block $user?', + subtitleFontWeight: FontWeight.normal, + subtitleFontSize: 16.0, + contentGap: 20.0, + confirmText: 'Block user', + confirmColor: const Color.fromRGBO(238, 104, 111, 1), + confirmPadding: const EdgeInsets.only(left: 40.0), + cancelText: 'Cancel', + cancelColor: const Color.fromRGBO(100, 181, 239, 1), + onConfirm: () => { + Navigator.of(context).pop(), + // Close the dialog + Navigator.of(context).pop(), + // Return to Blocked Users screen which is the previous screen. + }, + onCancel: () => {Navigator.of(context).pop()}, + actionsAlignment: MainAxisAlignment.end, + ); + } + + Future _initializeContacts() async { + try { + await _contactService.fetchAndStoreContacts(); + var box = Hive.box('contacts-block'); + for (var contact in box.values) { + userContacts[0]["options"].add({ + "text": contact.name, + "imageMemory": contact.photo, + "subtext": contact.phone, + }); + } + for (var option in userContacts[0]["options"]) { + option["color"] = Palette.primaryText; + option["fontSize"] = 15.0; + option["fontWeight"] = FontWeight.w500; + option["avatar"] = true; + option["subtextFontSize"] = 14.0; + option["imageWidth"] = 47.0; + option["imageHeight"] = 47.0; + option["onTap"] = () => blockConfirmationDialog(option["text"]); + } + setState(() {}); + } catch (error) { + debugPrint('Error fetching contacts: $error'); + } + } @override void initState() { super.initState(); - blockSections = [ + _initializeContacts(); + _duckController = AnimationController(vsync: this); + _duckController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _duckController.stop(); + } + }); + _tabController = TabController(vsync: this, length: 2); + userChats = >[ { - "options": [ + "options": >[ { "trailing": "1:22 AM", "text": 'Marwan Mohammed', "imagePath": 'assets/imgs/marwan.jpg', "subtext": ".", - "dummy": 0.0, }, { "trailing": "12:21 AM", "text": 'Ahmed Alaa', "imagePath": 'assets/imgs/ahmed.jpeg', "subtext": "Check out my new project for watching movies", - "dummy": 0.0, }, { "trailing": "12:02 AM", "text": 'Bishoy Wadea', "imagePath": 'assets/imgs/bishoy.jpeg', "subtext": "Find stickers in the catalog ⬇️", - "dummy": 0.0, }, ], }, ]; - for (var option in blockSections[0]["options"]) { + userContacts = >[ + { + "padding": const EdgeInsets.fromLTRB(11, 16, 11, 20), + "options": >[ + // { + // "text": 'Marwan Mohammed', + // "imagePath": 'assets/imgs/marwan.jpg', + // "subtext": "Last seen recently", + // }, + // { + // "text": 'Ahmed Alaa', + // "imagePath": 'assets/imgs/ahmed.jpeg', + // "subtext": "Last seen at 5:46 PM", + // }, + // { + // "text": 'Bishoy Wadea', + // "imagePath": 'assets/imgs/bishoy.jpeg', + // "subtext": "Last seen Aug 19 at 5:46 PM", + // }, + ], + }, + ]; + + for (var option in userChats[0]["options"]) { option["trailingFontSize"] = 13.0; - option["trailingHeight"] = 40.0; + option["trailingPadding"] = const EdgeInsets.only(bottom: 20.0); option["trailingColor"] = Palette.accentText; option["color"] = Palette.primaryText; option["fontSize"] = 18.0; @@ -57,51 +149,117 @@ class _BlockUserScreen extends State { option["fontWeight"] = FontWeight.w500; option["imageWidth"] = 55.0; option["imageHeight"] = 55.0; + option["onTap"] = () => blockConfirmationDialog(option["text"]); } } + @override + void dispose() { + _duckController.dispose(); + _tabController.dispose(); + super.dispose(); + } + + void _restartAnimation() { + _duckController.reset(); + _duckController.forward(); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Palette.secondary, - appBar: const ToolbarWidget( - title: "Block User", - ), - body: SingleChildScrollView( - child: Column( - children: [ - ...List.generate( - blockSections.length, - (index) { - final section = blockSections[index]; - final title = section["title"] ?? ""; - final trailingFontSize = section["trailingFontSize"]; - final lineHeight = section["lineHeight"]; - final padding = section["padding"]; - final options = section["options"]; - final trailing = section["trailing"] ?? ""; - final titleFontSize = section["titleFontSize"]; - return Column( - children: [ - SettingsSection( - titleFontSize: titleFontSize, - title: title, - padding: padding, - trailingFontSize: trailingFontSize, - trailingLineHeight: lineHeight, - settingsOptions: options, - trailing: trailing, - ), - const SizedBox(height: Dimensions.sectionGaps), - ], - ); - }, + appBar: AppBar( + backgroundColor: Palette.tabBar, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); // Navigate back when pressed + }, + ), + title: const Text( + "Blocked Users", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + bottom: TabBar( + controller: _tabController, + dividerHeight: 0, + padding: const EdgeInsets.only(top: 4), + indicator: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(3), + topRight: Radius.circular(3), ), - Lottie.asset('assets/tgs/EasterDuck.tgs', - width: 50, height: 50, decoder: LottieComposition.decodeGZip), + border: Border( + top: BorderSide( + color: Palette.primary, + width: 4.0, + ), + ), + ), + indicatorWeight: 0, + indicatorPadding: const EdgeInsets.only(top: 44), + labelStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Palette.primary, + ), + unselectedLabelColor: Palette.accentText, + labelPadding: const EdgeInsets.only(top: 2), + tabs: const [ + Tab(text: 'CHATS'), + Tab(text: 'CONTACTS'), ], ), ), + body: Column( + children: [ + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + userChats[0]["options"].length == 0 + ? EmptyChats( + padding: const EdgeInsets.only(top: 50), + duckController: _duckController, + onTap: _restartAnimation) + : SingleChildScrollView( + child: UserChats(chatSections: userChats), + ), + FlexibleScrollbar( + alwaysVisible: true, + controller: scrollController, + scrollThumbBuilder: (ScrollbarInfo info) { + return AnimatedContainer( + width: 5, + height: 31, + margin: const EdgeInsets.only( + right: 10, + top: 98, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + color: Palette.scrollBar, + ), + duration: const Duration(milliseconds: 300), + ); + }, + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: const EdgeInsets.only(top: 85), + child: UserChats(chatSections: userContacts), + ), + ), + ), + ], + ), + ), + ], + ), ); } } diff --git a/lib/features/user/view/screens/blocked_users.dart b/lib/features/user/view/screens/blocked_users.dart index bdf5ea9a..4ed701a3 100644 --- a/lib/features/user/view/screens/blocked_users.dart +++ b/lib/features/user/view/screens/blocked_users.dart @@ -49,6 +49,9 @@ class _BlockedUsersScreen extends State { ).then((int? result) { if (result == 0) { //todo Handle unblock action + if (mounted) { + Navigator.pushNamed(context, '/block-user'); + } } }); } @@ -74,37 +77,31 @@ class _BlockedUsersScreen extends State { } ], "trailing": - "Blocked users can't send you messages or add you to groups. " - "They will not see your profile photos, stories, online and last" - " seen status." + "Blocked users can't send you messages or add you to groups. They will not see your profile photos, stories, online and last seen status." }, { "title": "blocked users", "titleFontSize": 15.0, - "options": [ + "options": >[ { "text": 'Marwan Mohammed', "imagePath": 'assets/imgs/marwan.jpg', "subtext": "+201093401932", - "dummy": 0.0, }, { "text": 'Ahmed Alaa', "imagePath": 'assets/imgs/ahmed.jpeg', "subtext": "+201093401932", - "dummy": 0.0, }, { "text": 'Bishoy Wadea ', "imagePath": 'assets/imgs/bishoy.jpeg', "subtext": "+201093401932", - "dummy": 0.0, }, { "text": 'Moamen Hefny', "imagePath": 'assets/imgs/moamen.jpeg', "subtext": "+201093401932", - "dummy": 0.0, }, ], }, diff --git a/lib/features/user/view/widget/avatar_generator.dart b/lib/features/user/view/widget/avatar_generator.dart new file mode 100644 index 00000000..12cbf6c8 --- /dev/null +++ b/lib/features/user/view/widget/avatar_generator.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class AvatarGenerator extends StatelessWidget { + final String? name; + final Color backgroundColor; + final double size; + + const AvatarGenerator({ + super.key, + this.name, + required this.backgroundColor, + this.size = 100.0, // Default size of the avatar + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: backgroundColor, + gradient: LinearGradient( + colors: [ + Color.lerp(backgroundColor, Colors.white, 0.3) ?? Colors.black, + backgroundColor, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + _getInitials(name), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + // Extract the initials from the given name + String _getInitials(String? name) { + if (name == null || name.isEmpty) return ''; + List nameParts = name.trim().split(' '); + String initials = nameParts + .map((part) { + String cleanedPart = + part.replaceAll(RegExp(r'[^a-zA-Z\u0600-\u06FF]'), ''); + return cleanedPart.isNotEmpty ? cleanedPart[0] : ''; + }) + .take(2) + .join(); + return initials.toUpperCase(); + } +} diff --git a/lib/features/user/view/widget/empty_chats.dart b/lib/features/user/view/widget/empty_chats.dart new file mode 100644 index 00000000..d118d535 --- /dev/null +++ b/lib/features/user/view/widget/empty_chats.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:telware_cross_platform/core/theme/sizes.dart'; +import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart'; +import 'package:telware_cross_platform/core/theme/palette.dart'; + +class EmptyChats extends StatelessWidget { + const EmptyChats({ + super.key, + this.height, + required this.duckController, + this.onTap, + this.padding, + }); + + final AnimationController duckController; + final double? height; + final GestureTapCallback? onTap; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsets.all(0), + child: SizedBox( + height: height ?? MediaQuery.of(context).size.height, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GestureDetector( + onTap: onTap, + child: Lottie.asset('assets/tgs/EasterDuck.tgs', + controller: duckController, onLoaded: (composition) { + duckController.duration = composition.duration; + duckController.forward(); + }, + width: 100, + height: 100, + decoder: LottieComposition.decodeGZip), + ), + const TitleElement( + name: 'Welcome to Telegram', + color: Palette.primaryText, + fontSize: Sizes.primaryText - 2, + fontWeight: FontWeight.bold, + padding: EdgeInsets.only(bottom: 0, top: 10), + ), + const TitleElement( + name: + 'Start messaging by tapping the pencil button in the bottom right corner.', + color: Palette.accentText, + fontSize: Sizes.secondaryText - 2, + padding: EdgeInsets.only(bottom: 0, top: 5), + width: 350.0), + ], + ), + ), + )); + } +} diff --git a/lib/features/user/view/widget/settings_option_widget.dart b/lib/features/user/view/widget/settings_option_widget.dart index 50b65f80..43e1c5ff 100644 --- a/lib/features/user/view/widget/settings_option_widget.dart +++ b/lib/features/user/view/widget/settings_option_widget.dart @@ -1,10 +1,16 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/core/utils.dart'; +import '../../../user/view/widget/avatar_generator.dart'; + class SettingsOptionWidget extends StatelessWidget { final IconData? icon; final GlobalKey? trailingIconKey; final String? imagePath; + final Uint8List? imageMemory; final double imageWidth; final double imageHeight; final double imageRadius; @@ -17,11 +23,12 @@ class SettingsOptionWidget extends StatelessWidget { final FontWeight? fontWeight; final double? subtextFontSize; final double? trailingFontSize; - final double? trailingHeight; + final EdgeInsets? trailingPadding; final Color iconColor; final Color color; final Color? trailingColor; final bool showDivider; + final bool? avatar; final VoidCallback? onTap; const SettingsOptionWidget({ @@ -30,6 +37,8 @@ class SettingsOptionWidget extends StatelessWidget { this.icon, required this.text, this.imagePath, + this.imageMemory, + this.avatar, this.imageWidth = 45, this.imageHeight = 45, this.imageRadius = 50, @@ -43,7 +52,7 @@ class SettingsOptionWidget extends StatelessWidget { this.trailing = "", this.trailingIcon, this.trailingIconAction, - this.trailingHeight, + this.trailingPadding, this.trailingColor = Palette.primary, this.showDivider = true, this.onTap, @@ -51,90 +60,115 @@ class SettingsOptionWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), - child: Row( - children: [ - if (icon != null) ...[ - Icon(icon, key: key != null ? ValueKey("${(key as ValueKey).value}-icon") - : null, color: iconColor), - const SizedBox(width: 16), - ] else if (imagePath != null) ...[ - Padding( - padding: const EdgeInsets.only(right: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(imageRadius), - child: Image.asset( - imagePath!, - width: imageWidth, - height: imageHeight, - fit: BoxFit.cover, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, + key: key != null + ? ValueKey("${(key as ValueKey).value}-icon") + : null, + color: iconColor), + const SizedBox(width: 16), + ] else if (imagePath != null) ...[ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(imageRadius), + child: Image.asset( + imagePath!, + width: imageWidth, + height: imageHeight, + fit: BoxFit.cover, + ), ), ), - ), - ], - Expanded( - child: Column( - children: [ - ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text, - style: TextStyle( - color: color, - fontSize: fontSize, - fontWeight: fontWeight ?? FontWeight.normal, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - if (subtext.isNotEmpty) ...[ + ] else if (imageMemory != null) ...[ + Padding( + padding: const EdgeInsets.only(right: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(imageRadius), + child: Image.memory( + imageMemory!, + width: imageWidth, + height: imageHeight, + fit: BoxFit.cover, + ), + ), + ), + ] else if (avatar != null) ...[ + Padding( + padding: const EdgeInsets.only(right: 16), + child: AvatarGenerator( + name: text, + backgroundColor: getRandomColor(), + size: imageWidth, + ), + ), + ], + Expanded( + child: Column( + children: [ + ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - subtext, + text, style: TextStyle( - color: Palette.accentText, - fontSize: subtextFontSize ?? fontSize * 0.8, + color: color, + fontSize: fontSize, + fontWeight: fontWeight ?? FontWeight.normal, ), overflow: TextOverflow.ellipsis, maxLines: 1, - ) - ] - ], - ), - trailing: trailing.isNotEmpty - ? SizedBox( - height: trailingHeight, - child: Text( - trailing, - key: key != null ? - ValueKey("${(key as ValueKey).value}-trailing") : null, - style: TextStyle( - fontSize: trailingFontSize ?? fontSize * 0.9, - color: trailingColor ?? Palette.primary, - ), - ), - ) - : trailingIcon != null - ? IconButton( - key: trailingIconKey, - icon: Icon( - trailingIcon, - color: trailingColor ?? Palette.primary, ), - onPressed: trailingIconAction, - ) - : null, - onTap: onTap, + if (subtext != "") ...[ + Text( + subtext, + style: TextStyle( + color: Palette.accentText, + fontSize: subtextFontSize ?? fontSize * 0.8, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ) + ] + ], ), - if (showDivider) const Divider(), - ], - ), - ) - ], - ), - ); + trailing: trailing.isNotEmpty + ? Padding( + padding: trailingPadding ?? const EdgeInsets.all(0), + child: Text( + trailing, + key: key != null + ? ValueKey( + "${(key as ValueKey).value}-trailing") + : null, + style: TextStyle( + fontSize: trailingFontSize ?? fontSize * 0.9, + color: trailingColor ?? Palette.primary, + ), + ), + ) + : trailingIcon != null + ? IconButton( + key: trailingIconKey, + icon: Icon( + trailingIcon, + color: trailingColor ?? Palette.primary, + ), + onPressed: trailingIconAction, + ) + : null, + onTap: onTap, + ), + if (showDivider) const Divider(), + ], + ), + ) + ], + )); } } diff --git a/lib/features/user/view/widget/settings_section.dart b/lib/features/user/view/widget/settings_section.dart index 13d14b79..dc0ff64e 100644 --- a/lib/features/user/view/widget/settings_section.dart +++ b/lib/features/user/view/widget/settings_section.dart @@ -43,7 +43,9 @@ class SettingsSection extends StatelessWidget { children: [ if (title != "") SectionTitleWidget( - key: title != "" ? ValueKey("${toKebabCase(title)}-section-title") : null, + key: title != "" + ? ValueKey("${toKebabCase(title)}-section-title") + : null, title: title, fontSize: titleFontSize ?? 14, ), @@ -55,17 +57,23 @@ class SettingsSection extends StatelessWidget { (index) { final option = settingsOptions[index]; final type = option["type"] ?? ""; - final key = option["key"] != null ? ValueKey("${option["key"]}") : null; + final key = option["key"] != null + ? ValueKey("${option["key"]}") + : null; final String route = option["routes"] ?? ""; final bool lockedRoute = route == 'locked'; - final onTap = lockedRoute ? () => showToastMessage("Coming Soon...") : - route != "" ? () => _navigateTo(context, route) - : null; + final onTap = lockedRoute + ? () => showToastMessage("Coming Soon...") + : route != "" + ? () => _navigateTo(context, route) + : option["onTap"]; return SettingsOptionWidget( key: key, icon: option["icon"], trailingIconKey: option["iconKey"], imagePath: option["imagePath"], + imageMemory: option["imageMemory"], + avatar: option["avatar"], imageHeight: option["imageHeight"] ?? 45, imageWidth: option["imageWidth"] ?? 45, imageRadius: option["imageRadius"] ?? 25, @@ -79,7 +87,7 @@ class SettingsSection extends StatelessWidget { fontWeight: option["fontWeight"], subtextFontSize: option["subtextFontSize"], trailingFontSize: option["trailingFontSize"], - trailingHeight: option["trailingHeight"], + trailingPadding: option["trailingPadding"], iconColor: option["iconColor"] ?? Palette.accentText, color: option["color"] ?? Palette.primaryText, diff --git a/lib/features/user/view/widget/user_chats.dart b/lib/features/user/view/widget/user_chats.dart new file mode 100644 index 00000000..1f84c820 --- /dev/null +++ b/lib/features/user/view/widget/user_chats.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:telware_cross_platform/core/theme/dimensions.dart'; +import 'package:telware_cross_platform/features/user/view/widget/settings_section.dart'; + +class UserChats extends StatelessWidget { + final List> chatSections; + + const UserChats({super.key, required this.chatSections}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ...List.generate( + chatSections.length, + (index) { + final section = chatSections[index]; + final title = section["title"] ?? ""; + final trailingFontSize = section["trailingFontSize"]; + final lineHeight = section["lineHeight"]; + final padding = section["padding"]; + final options = section["options"]; + final trailing = section["trailing"] ?? ""; + final titleFontSize = section["titleFontSize"]; + return Column( + children: [ + SettingsSection( + titleFontSize: titleFontSize, + title: title, + padding: padding, + trailingFontSize: trailingFontSize, + trailingLineHeight: lineHeight, + settingsOptions: options, + trailing: trailing, + ), + const SizedBox(height: Dimensions.sectionGaps), + ], + ); + }, + ), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index f04a70f2..d87bd313 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:telware_cross_platform/core/view/screen/splash_screen.dart'; import 'package:telware_cross_platform/features/auth/view/screens/log_in_screen.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; import 'package:telware_cross_platform/core/models/user_model.dart'; +import 'core/models/contact_model.dart'; import 'features/auth/view/screens/sign_up_screen.dart'; import 'features/auth/view/screens/verification_screen.dart'; @@ -31,8 +32,10 @@ Future init() async { Hive.registerAdapter(UserModelAdapter()); Hive.registerAdapter(ContactModelAdapter()); Hive.registerAdapter(StoryModelAdapter()); + Hive.registerAdapter(ContactModelBlockAdapter()); await Hive.initFlutter(); await Hive.openBox('contacts'); + await Hive.openBox('contacts-block'); await Hive.openBox('auth-token'); await Hive.openBox('auth-user'); await dotenv.load(fileName: "lib/.env"); @@ -67,8 +70,9 @@ class _TelWareState extends ConsumerState { VerificationScreen.route: (context) => const VerificationScreen(), HomeScreen.route: (context) => const HomeScreen(), SettingsScreen.route: (context) => const SettingsScreen(), - ChangeNumberScreen.route : (context) => const ChangeNumberScreen(), - ChangeNumberFormScreen.route: (context) => const ChangeNumberFormScreen(), + ChangeNumberScreen.route: (context) => const ChangeNumberScreen(), + ChangeNumberFormScreen.route: (context) => + const ChangeNumberFormScreen(), ProfileInfoScreen.route: (context) => const ProfileInfoScreen(), BlockUserScreen.route: (context) => const BlockUserScreen(), BlockedUsersScreen.route: (context) => const BlockedUsersScreen(), diff --git a/pubspec.yaml b/pubspec.yaml index 6a5adadf..efc691d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,9 @@ dependencies: webview_flutter_plus: ^0.4.7 flutter_dotenv: ^5.2.1 gif: ^2.3.0 - lottie: ^3.0.0 + lottie: ^3.1.3 + fast_contacts: ^4.0.0 + flexible_scrollbar: ^0.1.3 mockito: ^5.0.0 dev_dependencies: @@ -95,7 +97,7 @@ flutter_launcher_icons: min_sdk_android: 21 # android min sdk min:16, default 21 adaptive_icon_background: "assets/icon/background.png" adaptive_icon_foreground: "assets/icon/foreground.png" - + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -112,6 +114,7 @@ flutter: - assets/imgs/ - assets/icon/ - assets/gifs/ + - assets/tgs/ - assets/webpages/ - lib/.env # - images/a_dot_ham.jpeg